From 8f7dd8bca0dc21d0f955328ea38dcf2ec03d42ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sondre=20Engebr=C3=A5ten?= Date: Fri, 13 Feb 2026 00:37:19 +0100 Subject: [PATCH] feat(triage): GAP-25 Phase F ARIA listbox + project complete (41/41) Add ARIA listbox pattern to IssueList/IssueListItem for keyboard navigation and screen reader support. Mark GAP-26 as SKIPPED (not in PRD, already covered by GAP-30). Co-Authored-By: Claude Opus 4.6 --- .../github-issues/components/IssueList.tsx | 2 +- .../components/IssueListItem.tsx | 9 +++++ .../__tests__/IssueList.integration.test.tsx | 36 +++++++++++++++++++ .../__tests__/IssueListItem.test.tsx | 33 ++++++++++++++++- .../src/shared/i18n/locales/en/common.json | 3 +- .../src/shared/i18n/locales/fr/common.json | 3 +- docs/gap-tracker.md | 25 ++++++++++++- 7 files changed, 106 insertions(+), 5 deletions(-) diff --git a/apps/frontend/src/renderer/components/github-issues/components/IssueList.tsx b/apps/frontend/src/renderer/components/github-issues/components/IssueList.tsx index b997e604..767aa499 100644 --- a/apps/frontend/src/renderer/components/github-issues/components/IssueList.tsx +++ b/apps/frontend/src/renderer/components/github-issues/components/IssueList.tsx @@ -77,7 +77,7 @@ export function IssueList({ return ( -
+
{issues.map((issue) => { const enrichment = enrichments?.[String(issue.number)]; return ( diff --git a/apps/frontend/src/renderer/components/github-issues/components/IssueListItem.tsx b/apps/frontend/src/renderer/components/github-issues/components/IssueListItem.tsx index 113fabb3..299fae8b 100644 --- a/apps/frontend/src/renderer/components/github-issues/components/IssueListItem.tsx +++ b/apps/frontend/src/renderer/components/github-issues/components/IssueListItem.tsx @@ -24,12 +24,21 @@ export const IssueListItem = memo(function IssueListItem({ }: IssueListItemProps) { return (
{ + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onClick(); + } + }} >
{isSelectable && ( diff --git a/apps/frontend/src/renderer/components/github-issues/components/__tests__/IssueList.integration.test.tsx b/apps/frontend/src/renderer/components/github-issues/components/__tests__/IssueList.integration.test.tsx index d8225f84..d055260b 100644 --- a/apps/frontend/src/renderer/components/github-issues/components/__tests__/IssueList.integration.test.tsx +++ b/apps/frontend/src/renderer/components/github-issues/components/__tests__/IssueList.integration.test.tsx @@ -133,3 +133,39 @@ describe('IssueList enrichment integration', () => { expect(checkboxes[2].checked).toBe(false); // issue 3 }); }); + +describe('IssueList ARIA listbox', () => { + const defaultProps = { + issues: [makeIssue(1), makeIssue(2), makeIssue(3)], + selectedIssueNumber: null, + isLoading: false, + error: null, + onSelectIssue: vi.fn(), + onInvestigate: vi.fn(), + }; + + it('items container has role="listbox"', () => { + render(); + expect(screen.getByRole('listbox')).toBeDefined(); + }); + + it('listbox has an accessible label', () => { + render(); + const listbox = screen.getByRole('listbox'); + expect(listbox.getAttribute('aria-label')).toBe('issues.listLabel'); + }); + + it('each issue item has role="option"', () => { + render(); + const options = screen.getAllByRole('option'); + expect(options.length).toBe(3); + }); + + it('selected issue has aria-selected=true', () => { + render(); + const options = screen.getAllByRole('option'); + expect(options[0].getAttribute('aria-selected')).toBe('false'); + expect(options[1].getAttribute('aria-selected')).toBe('true'); + expect(options[2].getAttribute('aria-selected')).toBe('false'); + }); +}); diff --git a/apps/frontend/src/renderer/components/github-issues/components/__tests__/IssueListItem.test.tsx b/apps/frontend/src/renderer/components/github-issues/components/__tests__/IssueListItem.test.tsx index fb93f491..3ed72fd7 100644 --- a/apps/frontend/src/renderer/components/github-issues/components/__tests__/IssueListItem.test.tsx +++ b/apps/frontend/src/renderer/components/github-issues/components/__tests__/IssueListItem.test.tsx @@ -2,7 +2,7 @@ * @vitest-environment jsdom */ import { describe, it, expect, vi } from 'vitest'; -import { render, screen } from '@testing-library/react'; +import { render, screen, fireEvent } from '@testing-library/react'; import { IssueListItem } from '../IssueListItem'; vi.mock('react-i18next', () => ({ @@ -54,3 +54,34 @@ describe('IssueListItem compact mode', () => { expect(screen.getByText('#42')).toBeDefined(); }); }); + +describe('IssueListItem ARIA', () => { + const baseProps = { + issue: mockIssue, + isSelected: false, + onClick: vi.fn(), + onInvestigate: vi.fn(), + }; + + it('has role="option"', () => { + render(); + expect(screen.getByRole('option')).toBeDefined(); + }); + + it('aria-selected is false when not selected', () => { + render(); + expect(screen.getByRole('option').getAttribute('aria-selected')).toBe('false'); + }); + + it('aria-selected is true when selected', () => { + render(); + expect(screen.getByRole('option').getAttribute('aria-selected')).toBe('true'); + }); + + it('Enter key triggers onClick', () => { + const onClick = vi.fn(); + render(); + fireEvent.keyDown(screen.getByRole('option'), { key: 'Enter' }); + expect(onClick).toHaveBeenCalledOnce(); + }); +}); diff --git a/apps/frontend/src/shared/i18n/locales/en/common.json b/apps/frontend/src/shared/i18n/locales/en/common.json index debfd294..e07386e7 100644 --- a/apps/frontend/src/shared/i18n/locales/en/common.json +++ b/apps/frontend/src/shared/i18n/locales/en/common.json @@ -440,7 +440,8 @@ "issues": { "loadingMore": "Loading more...", "scrollForMore": "Scroll for more", - "allLoaded": "All issues loaded" + "allLoaded": "All issues loaded", + "listLabel": "Issues" }, "enrichment": { "states": { diff --git a/apps/frontend/src/shared/i18n/locales/fr/common.json b/apps/frontend/src/shared/i18n/locales/fr/common.json index 128712af..ae865173 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/common.json +++ b/apps/frontend/src/shared/i18n/locales/fr/common.json @@ -440,7 +440,8 @@ "issues": { "loadingMore": "Chargement...", "scrollForMore": "Défiler pour plus", - "allLoaded": "Toutes les issues chargées" + "allLoaded": "Toutes les issues chargées", + "listLabel": "Issues" }, "enrichment": { "states": { diff --git a/docs/gap-tracker.md b/docs/gap-tracker.md index b039e89d..9e672c74 100644 --- a/docs/gap-tracker.md +++ b/docs/gap-tracker.md @@ -3,7 +3,7 @@ **Branch:** `terminal/enhancement-issues-tab` **Created:** 2026-02-12 **Total Gaps:** 41 confirmed (from triple-verified audit) -**Status:** 40 / 41 complete +**Status:** 41 / 41 complete --- @@ -540,6 +540,27 @@ Each gap has: ID, description, status, files to modify, doc reference, test stat - **Depends on:** None - **Commit:** Phase E batch +## TIER 5 — Low Priority + +### GAP-25: IssueList ARIA listbox +- **Status:** `DONE` +- **Priority:** NICE-TO-HAVE +- **Scope:** Small +- **Doc ref:** WCAG 2.1 Listbox pattern +- **Files modified:** `components/IssueList.tsx`, `components/IssueListItem.tsx`, `en/common.json`, `fr/common.json` +- **Fix:** Added role="listbox" + aria-label (i18n issues.listLabel) to IssueList container div. Added role="option" + aria-selected + tabIndex={0} + onKeyDown (Enter/Space) to IssueListItem. +- **Tests:** 4 new IssueList integration tests (listbox role, aria-label, option count, aria-selected). 4 new IssueListItem tests (role=option, aria-selected false/true, Enter key). 18 total. +- **Test status:** `PASS` +- **Depends on:** None +- **Commit:** Phase F batch + +### GAP-26: transitions.json retention +- **Status:** `SKIPPED` +- **Priority:** NICE-TO-HAVE +- **Scope:** N/A +- **Doc ref:** Not in PRD +- **Reason:** Not specified in any PRD. Transitions are already persisted via appendTransition in enrichment handlers (GAP-30). No additional work needed. + --- ## Progress Log @@ -570,6 +591,8 @@ Each gap has: ID, description, status, files to modify, doc reference, test stat | 2026-02-13 | GAP-41 | DONE — Bulk label sync: IPC channel + handler + preload + hook method, 2 new tests | Phase E | | 2026-02-13 | GAP-42 | DONE — Color swatches in LabelSyncSettings (7 workflow labels with colors), 2 new tests | Phase E | | 2026-02-13 | GAP-43 | DONE — Write/Preview tabs in CommentForm with ReactMarkdown, i18n keys EN+FR, 8 tests | Phase E | +| 2026-02-13 | GAP-25 | DONE — ARIA listbox on IssueList + role=option + aria-selected + keyboard on IssueListItem, 8 new tests | Phase F | +| 2026-02-13 | GAP-26 | SKIPPED — Not in PRD, transitions already persisted via GAP-30 | N/A | ---