From 5814afb3b5a7a024cea62499c8e185a57920612f Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 20 Jun 2026 02:18:33 +0200 Subject: [PATCH] Polish workbench keyboard focus --- CHANGELOG.md | 8 ++++ ...est_sprint79_accessibility_focus_polish.py | 43 +++++++++++++++++++ docs/CODEX_EXECUTION_LOG.md | 25 +++++++++++ docs/TODO.md | 1 + frontend/README.md | 2 + frontend/src/App.tsx | 10 +++-- .../inspector/WorkbenchInspector.tsx | 12 ++++-- frontend/src/styles/app.css | 16 +++++++ 8 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 backend/tests/test_sprint79_accessibility_focus_polish.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 96ae15b5..57b62061 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ # Changelog +## Sprint 79 Accessibility focus polish (2026-06-20) + +- Added a shared visible focus-ring contract for primary buttons, workspace navigation, command chips, inspector tabs and dataset action buttons. +- Added explicit ARIA labels to workspace navigation, command chips and overview quick actions. +- Bound inspector tabs to their active tab panels with `aria-controls`, tab ids and `tabpanel` metadata. +- Added static regression coverage for keyboard focus and inspector tab accessibility contracts. +- No API contracts, migrations, backend behavior, provider fetching or AI model behavior changed. + ## Sprint 78 Export preview readability polish (2026-06-20) - Added compact preview summary cards for JSON/GeoJSON export payloads. diff --git a/backend/tests/test_sprint79_accessibility_focus_polish.py b/backend/tests/test_sprint79_accessibility_focus_polish.py new file mode 100644 index 00000000..611563fd --- /dev/null +++ b/backend/tests/test_sprint79_accessibility_focus_polish.py @@ -0,0 +1,43 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def test_global_focus_visible_contracts_are_defined() -> None: + css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8") + + assert "--focus-ring" in css + assert "--focus-ring-soft" in css + assert "button:focus-visible" in css + assert ".nav-item:focus-visible" in css + assert ".command-chip:focus-visible" in css + assert ".inspector-tab:focus-visible" in css + assert ".dataset-action-button:focus-visible" in css + assert "outline: 3px solid var(--focus-ring);" in css + assert "outline-offset: 2px;" in css + + +def test_primary_navigation_has_keyboard_labels() -> None: + app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + + assert 'aria-label={`Open ${item.label} workspace: ${item.description}`}' in app + assert 'aria-label={`Switch to ${item.label} workspace`}' in app + assert 'aria-label="Open data setup workspace"' in app + assert 'aria-label="Inspect map workspace"' in app + assert 'aria-label="Review QA/QC workspace"' in app + assert 'aria-label="Manage exports workspace"' in app + + +def test_inspector_tabs_are_bound_to_tab_panels() -> None: + inspector = (ROOT / "frontend" / "src" / "components" / "inspector" / "WorkbenchInspector.tsx").read_text( + encoding="utf-8" + ) + + assert "const activeTabId = `inspector-tab-${activeTab}`" in inspector + assert "const activePanelId = `inspector-panel-${activeTab}`" in inspector + assert "aria-controls={`inspector-panel-${tab.key}`}" in inspector + assert "id={`inspector-tab-${tab.key}`}" in inspector + assert 'role="tabpanel"' in inspector + assert "id={activePanelId}" in inspector + assert "aria-labelledby={activeTabId}" in inspector diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 5914d983..8ed848d8 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -2903,3 +2903,28 @@ Limitations: Next recommended pass: - Continue with accessibility/keyboard focus polish across primary workbench controls. + +## Sprint 79 Accessibility focus polish (2026-06-20) + +Changed: +- Added a shared visible keyboard focus contract for primary buttons, workspace navigation, command chips, inspector tabs and dataset action buttons. +- Added explicit ARIA labels for workspace sidebar navigation, workspace command chips and overview quick actions. +- Bound inspector tabs to active tab panels with `aria-controls`, tab ids, `tabpanel` role and `aria-labelledby`. +- Added `backend/tests/test_sprint79_accessibility_focus_polish.py`. +- Updated `frontend/README.md`, `docs/TODO.md` and `CHANGELOG.md`. + +Tested: +- Red step: `python -m pytest backend/tests/test_sprint79_accessibility_focus_polish.py -q` failed on missing focus-visible CSS, navigation labels and inspector tab/panel bindings. +- `python -m pytest backend/tests/test_sprint79_accessibility_focus_polish.py backend/tests/test_sprint77_inspector_mobile_polish.py backend/tests/test_sprint53_selection_ergonomics.py backend/tests/test_sprint49_workbench_shell_refactor.py backend/tests/test_sprint47_workbench_interaction_smoke.py -q` (`15 passed`) +- `cd frontend && npm run typecheck` +- `cd frontend && npm run build` +- `bash scripts/run_readiness_check.sh` (`270 passed`; frontend typecheck/build passed; Alembic single head `202606120900`) + +Open: +- Run full readiness, redeploy Tower and verify live runtime smoke after deployment. + +Limitations: +- Frontend accessibility/presentation polish only; no workflow behavior, API contract, persistence, migration, provider fetching or AI/model changes. + +Next recommended pass: +- Continue with form-level validation/readability polish for dense raster/vector operation panels. diff --git a/docs/TODO.md b/docs/TODO.md index 56b597e1..03587345 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -68,6 +68,7 @@ This file now starts with the current implementation status. Older preparation/b - [x] Add Export/System mobile visual polish for export actions and provider capability cards. - [x] Add inspector mobile polish for dataset metadata, raster/vector tools and action groups. - [x] Add export preview readability polish for large JSON/GeoJSON handoff artifacts. +- [x] Add accessibility focus polish for primary workbench keyboard navigation. ## Sprint 8 status diff --git a/frontend/README.md b/frontend/README.md index 10970cbf..e7ae7c8d 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -14,6 +14,8 @@ The right-side inspector also uses mobile-safe metadata and tool panels. Dataset Export Preview shows a compact JSON summary before the payload and keeps large JSON/GeoJSON handoff artifacts inside a scroll-contained preview shell. HTML report artifacts remain download-only. +Primary workbench navigation, overview shortcuts, inspector tabs and dataset action buttons now share visible keyboard focus styling. Inspector tabs are also bound to tab panels with ARIA metadata. + ## Scope implemented - API client layer (`src/services/api`) - Project and area list/create flows diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6c7a8502..1ce721fb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -489,6 +489,7 @@ function App(): JSX.Element { className={item.key === activeWorkspace ? 'nav-item nav-item-active' : 'nav-item'} onClick={() => setActiveWorkspace(item.key)} aria-current={item.key === activeWorkspace ? 'page' : undefined} + aria-label={`Open ${item.label} workspace: ${item.description}`} data-testid={`workspace-nav-${item.key}`} > {item.label} @@ -515,6 +516,7 @@ function App(): JSX.Element { className={item.key === activeWorkspace ? 'command-chip command-chip-active' : 'command-chip'} onClick={() => setActiveWorkspace(item.key)} aria-pressed={item.key === activeWorkspace} + aria-label={`Switch to ${item.label} workspace`} > {item.label} @@ -544,16 +546,16 @@ function App(): JSX.Element {

- - - -
diff --git a/frontend/src/components/inspector/WorkbenchInspector.tsx b/frontend/src/components/inspector/WorkbenchInspector.tsx index 6c985949..bc0db326 100644 --- a/frontend/src/components/inspector/WorkbenchInspector.tsx +++ b/frontend/src/components/inspector/WorkbenchInspector.tsx @@ -86,6 +86,8 @@ export function WorkbenchInspector({ ) const latestQualityCheck = qualityChecks[0] ?? null const latestPersistedExport = exports[0] ?? null + const activeTabId = `inspector-tab-${activeTab}` + const activePanelId = `inspector-panel-${activeTab}` const tabs: Array<{ key: InspectorTab; label: string }> = [ { key: 'context', label: 'Context' }, @@ -111,6 +113,8 @@ export function WorkbenchInspector({ onClick={() => setActiveTab(tab.key)} role="tab" aria-selected={activeTab === tab.key} + aria-controls={`inspector-panel-${tab.key}`} + id={`inspector-tab-${tab.key}`} data-testid={`inspector-tab-${tab.key}`} > {tab.label} @@ -119,7 +123,7 @@ export function WorkbenchInspector({ {activeTab === 'context' ? ( -
+

Project context

@@ -157,7 +161,7 @@ export function WorkbenchInspector({ ) : null} {activeTab === 'dataset' ? ( -
+