Polish workbench keyboard focus
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
@@ -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}
|
||||
</button>
|
||||
@@ -544,16 +546,16 @@ function App(): JSX.Element {
|
||||
</p>
|
||||
</div>
|
||||
<div className="quick-action-grid">
|
||||
<button type="button" onClick={() => setActiveWorkspace('data')}>
|
||||
<button type="button" onClick={() => setActiveWorkspace('data')} aria-label="Open data setup workspace">
|
||||
Open data setup
|
||||
</button>
|
||||
<button type="button" onClick={() => setActiveWorkspace('map')}>
|
||||
<button type="button" onClick={() => setActiveWorkspace('map')} aria-label="Inspect map workspace">
|
||||
Inspect map
|
||||
</button>
|
||||
<button type="button" onClick={() => setActiveWorkspace('analysis')}>
|
||||
<button type="button" onClick={() => setActiveWorkspace('analysis')} aria-label="Review QA/QC workspace">
|
||||
Review QA/QC
|
||||
</button>
|
||||
<button type="button" onClick={() => setActiveWorkspace('exports')}>
|
||||
<button type="button" onClick={() => setActiveWorkspace('exports')} aria-label="Manage exports workspace">
|
||||
Manage exports
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
|
||||
{activeTab === 'context' ? (
|
||||
<div className="inspector-tab-panel">
|
||||
<div className="inspector-tab-panel" role="tabpanel" id={activePanelId} aria-labelledby={activeTabId}>
|
||||
<div className="inspector-card">
|
||||
<h3>Project context</h3>
|
||||
<InspectorField label="Project" value={selectedProject?.name} />
|
||||
@@ -157,7 +161,7 @@ export function WorkbenchInspector({
|
||||
) : null}
|
||||
|
||||
{activeTab === 'dataset' ? (
|
||||
<div className="inspector-tab-panel">
|
||||
<div className="inspector-tab-panel" role="tabpanel" id={activePanelId} aria-labelledby={activeTabId}>
|
||||
<div className="inspector-action-bar">
|
||||
<button type="button" className="secondary-action" onClick={onOpenDataWorkspace}>
|
||||
Data catalog
|
||||
@@ -174,7 +178,7 @@ export function WorkbenchInspector({
|
||||
) : null}
|
||||
|
||||
{activeTab === 'quality' ? (
|
||||
<div className="inspector-tab-panel">
|
||||
<div className="inspector-tab-panel" role="tabpanel" id={activePanelId} aria-labelledby={activeTabId}>
|
||||
<div className="inspector-card">
|
||||
<h3>Latest QA/QC</h3>
|
||||
<InspectorField label="Check type" value={latestQualityCheck?.check_type} />
|
||||
@@ -205,7 +209,7 @@ export function WorkbenchInspector({
|
||||
) : null}
|
||||
|
||||
{activeTab === 'ai' ? (
|
||||
<div className="inspector-tab-panel">
|
||||
<div className="inspector-tab-panel" role="tabpanel" id={activePanelId} aria-labelledby={activeTabId}>
|
||||
<div className="inspector-card">
|
||||
<h3>Detection run</h3>
|
||||
<InspectorField label="Selected run" value={selectedDetectionRunId} />
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user