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' ? (
-
+
Data catalog
@@ -174,7 +178,7 @@ export function WorkbenchInspector({
) : null}
{activeTab === 'quality' ? (
-
+
Latest QA/QC
@@ -205,7 +209,7 @@ export function WorkbenchInspector({
) : null}
{activeTab === 'ai' ? (
-
+
Detection run
diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css
index 37450b54..756f52b0 100644
--- a/frontend/src/styles/app.css
+++ b/frontend/src/styles/app.css
@@ -13,6 +13,8 @@
--accent: #0f766e;
--accent-strong: #115e59;
--accent-soft: #e3f4ef;
+ --focus-ring: #0f766e;
+ --focus-ring-soft: rgba(15, 118, 110, 0.2);
--warning: #b45309;
--danger: #991b1b;
--shadow: 0 12px 32px rgba(33, 48, 41, 0.08);
@@ -82,6 +84,20 @@ button:disabled {
opacity: 0.52;
}
+button:focus-visible,
+input:focus-visible,
+select:focus-visible,
+textarea:focus-visible,
+.nav-item:focus-visible,
+.command-chip:focus-visible,
+.inspector-tab:focus-visible,
+.dataset-action-button:focus-visible {
+ outline: 3px solid var(--focus-ring);
+ outline-offset: 2px;
+ border-color: var(--focus-ring);
+ box-shadow: 0 0 0 5px var(--focus-ring-soft);
+}
+
input,
select,
textarea {