Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@
## 2026-01-22 - Replacing Text Characters with Semantic Icons
**Learning:** In `AppSidebar.tsx`, a text character "▼" was used for the expand/collapse arrow. Screen readers might announce this as "Black Down-Pointing Triangle" or similar, which is distracting. Text characters also don't scale or style as consistently as SVGs.
**Action:** Replace decorative text characters with `CyberIcon` (or proper SVG icons) and add `aria-hidden="true"` to ensure they are treated as visual decoration only.

## 2026-01-24 - Keyboard Visibility for Hover Actions
**Learning:** In `GoalsList.tsx`, action buttons were hidden using `opacity-0 group-hover:opacity-100`, making them invisible to keyboard users who tabbed into the card. Focus does not trigger hover styles by default.
**Action:** Always add `group-focus-within:opacity-100` alongside `group-hover:opacity-100` for containers of interactive elements to ensure they become visible when a user focuses on them.
6 changes: 3 additions & 3 deletions src/components/goals/GoalsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,13 @@ export function GoalsList({ goals, onEdit, onDelete, onPlan, onStatusChange, onA
</div>
</div>

<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Button variant="ghost" size="sm" onClick={() => onEdit(goal)} className="h-8 w-8 p-0 hover:bg-violet-500/20">
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity">
<Button variant="ghost" size="sm" onClick={() => onEdit(goal)} className="h-8 w-8 p-0 hover:bg-violet-500/20" aria-label="Editar meta">
<CyberIcon name="edit" className="h-4 w-4" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0 text-destructive hover:text-destructive hover:bg-destructive/10">
<Button variant="ghost" size="sm" className="h-8 w-8 p-0 text-destructive hover:text-destructive hover:bg-destructive/10" aria-label="Excluir meta">
<CyberIcon name="trash" className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
Expand Down
60 changes: 60 additions & 0 deletions src/tests/components/GoalsList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { render, screen } from '@testing-library/react';
import { GoalsList } from '../../components/goals/GoalsList';
import { Goal } from '../../hooks/useGoals';
import React from 'react';
import { vi, describe, it, expect } from 'vitest';

// Mock CyberIcon to avoid rendering issues and focus on logic
vi.mock('@/components/ui/cyber-icons', () => ({
CyberIcon: ({ name }: { name: string }) => <span data-testid={`icon-${name}`}>{name}</span>,
}));

// Mock ElectricBorder to avoid complex rendering
vi.mock('@/components/ui/electric-border', () => ({
ElectricBorder: ({ children, className }: { children: React.ReactNode; className?: string }) => (
<div className={className} data-testid="electric-border">{children}</div>
),
}));

describe('GoalsList', () => {
const mockGoal: Goal = {
id: '1',
title: 'Test Goal',
description: 'Test Description',
category: 'personal',
status: 'active',
progress_percentage: 50,
created_at: new Date().toISOString(),
goal_milestones: [],
goal_checkins: [],
};

const defaultProps = {
goals: [mockGoal],
onEdit: vi.fn(),
onDelete: vi.fn(),
onPlan: vi.fn(),
onStatusChange: vi.fn(),
onAddTask: vi.fn(),
};

it('renders goals with accessible actions', () => {
render(<GoalsList {...defaultProps} />);

// Check if goal title is rendered
expect(screen.getByText('Test Goal')).toBeInTheDocument();

// Check for Edit button accessibility
const editButton = screen.getByRole('button', { name: /editar meta/i });
expect(editButton).toBeInTheDocument();

// Check for Delete button accessibility (it's inside AlertDialogTrigger)
const deleteButton = screen.getByRole('button', { name: /excluir meta/i });
expect(deleteButton).toBeInTheDocument();

// Check for container visibility class
// We find the container by finding the edit button and looking at its parent
const buttonContainer = editButton.parentElement;
expect(buttonContainer).toHaveClass('group-focus-within:opacity-100');
});
});
Loading