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 <[email protected]>
This commit is contained in:
Sondre Engebråten
2026-02-13 00:37:19 +01:00
co-authored by Claude Opus 4.6
parent 80441de62c
commit 8f7dd8bca0
7 changed files with 106 additions and 5 deletions
@@ -77,7 +77,7 @@ export function IssueList({
return (
<ScrollArea className="flex-1" onViewportRef={setViewportElement}>
<div className="p-2 space-y-1">
<div role="listbox" aria-label={t('issues.listLabel')} className="p-2 space-y-1">
{issues.map((issue) => {
const enrichment = enrichments?.[String(issue.number)];
return (
@@ -24,12 +24,21 @@ export const IssueListItem = memo(function IssueListItem({
}: IssueListItemProps) {
return (
<div
role="option"
aria-selected={isSelected}
tabIndex={0}
className={`group p-3 rounded-lg cursor-pointer transition-colors ${
isSelected
? 'bg-accent/50 border border-accent'
: 'hover:bg-muted/50 border border-transparent'
}`}
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClick();
}
}}
>
<div className="flex items-start gap-3">
{isSelectable && (
@@ -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(<IssueList {...defaultProps} />);
expect(screen.getByRole('listbox')).toBeDefined();
});
it('listbox has an accessible label', () => {
render(<IssueList {...defaultProps} />);
const listbox = screen.getByRole('listbox');
expect(listbox.getAttribute('aria-label')).toBe('issues.listLabel');
});
it('each issue item has role="option"', () => {
render(<IssueList {...defaultProps} />);
const options = screen.getAllByRole('option');
expect(options.length).toBe(3);
});
it('selected issue has aria-selected=true', () => {
render(<IssueList {...defaultProps} selectedIssueNumber={2} />);
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');
});
});
@@ -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(<IssueListItem {...baseProps} />);
expect(screen.getByRole('option')).toBeDefined();
});
it('aria-selected is false when not selected', () => {
render(<IssueListItem {...baseProps} isSelected={false} />);
expect(screen.getByRole('option').getAttribute('aria-selected')).toBe('false');
});
it('aria-selected is true when selected', () => {
render(<IssueListItem {...baseProps} isSelected />);
expect(screen.getByRole('option').getAttribute('aria-selected')).toBe('true');
});
it('Enter key triggers onClick', () => {
const onClick = vi.fn();
render(<IssueListItem {...baseProps} onClick={onClick} />);
fireEvent.keyDown(screen.getByRole('option'), { key: 'Enter' });
expect(onClick).toHaveBeenCalledOnce();
});
});
@@ -440,7 +440,8 @@
"issues": {
"loadingMore": "Loading more...",
"scrollForMore": "Scroll for more",
"allLoaded": "All issues loaded"
"allLoaded": "All issues loaded",
"listLabel": "Issues"
},
"enrichment": {
"states": {
@@ -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": {
+24 -1
View File
@@ -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 |
---