Accessibility audit: keyboard, focus, tab semantics, contrast, live regions#94
Open
thalida wants to merge 13 commits into
Open
Accessibility audit: keyboard, focus, tab semantics, contrast, live regions#94thalida wants to merge 13 commits into
thalida wants to merge 13 commits into
Conversation
- RangePair: keyboard focus-visible ring now shows on the thumb instead of being suppressed entirely (was outline:none on the whole overlay input). - almanac-fact nav rows: keep the global :focus-visible ring instead of replacing it with only a faint hover wash. - Bump --cc-text-muted lightness so placeholders and segmented-select inactive labels clear 4.5:1 on the sidebar surface (was 4.45:1). Refs #79 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Explore file tree rows were <div onClick> only: unreachable and unusable by keyboard. Rebuild them as a real ARIA tree: - role=tree/treeitem/group, aria-expanded on dirs, aria-selected on the active node, and an aria-label on the tree. - Roving tabindex (the selected item, or the first when none) plus full keyboard nav: Up/Down move, Left/Right collapse/expand or step in/out, Home/End jump, Enter/Space activate. - Keyboard focus ring drawn on the row band only, not around the wrapping subtree. Refs #79 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keydown bubbled to ancestor <li role=treeitem> handlers, which re-ran and pulled focus back to a folder's first child, so arrow-down couldn't move past the first item inside any nested folder. Only the focused item (the event target) now handles its own keys. Refs #79 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- New useDialogFocus hook: on open it moves focus into the dialog, traps Tab within it, marks background siblings inert (out of tab order + a11y tree), and restores focus to the opener on close. Applied to Debug, Shortcuts, and (in dismissible/modal mode) Projects views. - ProjectsView gains role=dialog + aria-modal when shown over a city. - Shortcuts dialog now labelled by its visible title (fixes Label-in-Name: aria-label 'Keyboard shortcuts' vs visible 'Keyboard & Mouse'). - Add a skip-to-content link + sr-only utility; main is focus-targetable. Adds axe-core as a dev dep for the a11y test harness. Refs #79 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An interactive control (the reset button) nested inside <summary> is unreliable for keyboard/AT (Chrome flags all 47). Rebuild Section and Subgroup as button-driven disclosures: the header is a flex row with a real <button aria-expanded> toggle and the reset button as a sibling, not nested. - Outer element keeps its class (.controls-section / .theme-subgroup) as a <div>, and children stay direct children, so every structural selector (dividers, nested-subgroup indent, first-of-type) is unchanged; an .is-open class hides the body when collapsed, mirroring a closed <details>. - Chevron rotates off aria-expanded; ControlsPane collapses sections by remounting them (nonce) instead of toggling details.open. Refs #79 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Every settings control (color/number/slider/toggle/range-pair, plus the HueMap and TierWidths per-row sliders) now takes an id, and its ThemeRow label associates via htmlFor. Fixes the 271 'form field needs id or name' and the stacked-row case where the label bound to the reset button, not the control. RangePair thumbs get distinct 'X minimum'/'X maximum' names. - NewProjectForm: associate the source label (htmlFor+id), wire the validation error via aria-describedby + role=alert, drop the redundant aria-label. - ThemeRow tooltip uses a colon, not an em-dash. - New tests/a11y/audit.test.tsx: runs axe-core across ControlsPane, ProjectsView, both modals, and TreePane, plus structural guards (no interactive-in-summary, no id-less fields, no orphan labels, no positive tabindex) so these can't silently regress. Refs #79 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The subgroup toggle button carries font:inherit, which overrode the .text-label font-size when those classes sat on the button — leaving the labels uppercase (text-transform isn't part of the font shorthand) but blown up to body size. Move text-label onto the label span, matching Section, so subgroup headers render at the small label size again. Refs #79 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PaneTabs: roving tabindex (only the active tab is a Tab stop) plus arrow-key navigation with automatic activation, Home/End, and focus following selection - the interaction the tablist role advertises. A panelId prop wires aria-controls on each tab and stable tab ids; InfoPane and ControlsPane mark their body as the matching role=tabpanel (aria-labelledby the active tab, via a new Pane bodyProps passthrough). SegmentedSelect: was styled buttons in a role=group with the selection only in CSS. Now a real role=radiogroup / role=radio with aria-checked, a group name (the field label), roving tabindex, and arrow-key selection. Refs #79 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The paneRef only existed to query <details> for collapse; that path is gone (nonce remount), so remove the ref and its Pane wiring, and de-stale the Pane paneRef doc comment. Refs #79 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The city <canvas> gets role=img + a description of what it shows (WCAG 1.1.1), since it's otherwise a nameless graphic to assistive tech. - New SelectionAnnouncer: a visually-hidden polite live region (mounted in App) that mirrors the picker and speaks the selected file/dir/commit/gem, so selecting in the 3D scene is announced (WCAG 4.1.3). Refs #79 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The two sidebars get distinct aria-labels (Explore / Selection details) so the complementary landmarks are distinguishable; the activity bar is a labelled <nav>. - Search results are real <button>s (native Enter/Space, button role) instead of tabindex li's, and a polite live region announces the result count / no- match as you type. - Footer build-status is a role=status live region (rebuild/idle/error now announced); its decorative dot is aria-hidden. - Breadcrumb separator is aria-hidden. Refs #79 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- CopyButton announces 'Copied' via a visually-hidden live region (was a color-only flash). - BranchSelect: associate the visible Branch label with the select (htmlFor + id, dropping the duplicate aria-label), and announce 'Resolving branches' via role=status. - CommitPane message load/error and FilePreviewPane load are announced via role=status (the file body is left off the live region so it isn't read out wholesale). Refs #79 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The <embed> PDF preview had no accessible name; give it a title, and label the video/audio elements by filename. Refs #79 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #79.
Full accessibility audit across the app, then fixes prioritizing keyboard, focus, contrast, and tab semantics. Audited against current
main(so this reflects what #78/#83/#87 already handled, not the pre-#78 state).What the audit found
Verified with rigorous tooling, not eyeballing:
axe-core(Chrome's audit engine) run against the real composed surfaces (settings, source picker, modals, tree), now committed astests/a11y/audit.test.tsxso it can't regress.The recent PRs had already handled a lot (global
:focus-visiblering, landmarks, most icon-button labels, and contrast — which passes AA across all presets). The real gaps were keyboard operability, focus management, and status announcements.Fixes
Keyboard + focus
<div onClick>only (mouse-dead). Rebuilt as a real ARIA tree:role=tree/treeitem/group,aria-expanded/aria-selected, roving tabindex, and full arrow / Home / End / Enter / Space navigation.useDialogFocushook adds all three; ProjectsView gainsrole=dialog/aria-modalin modal mode.sr-onlyutility;<main>is focus-targetable.Contrast
--cc-text-mutedso placeholders + segmented-select labels clear 4.5:1 on the sidebar surface (was 4.45).Tab / control semantics
PaneTabsis now keyboard-navigable (roving tabindex + arrow keys, automatic activation) witharia-controls/role=tabpanelwiring (Info + Controls panes).SegmentedSelectis a realradiogroup/radiowitharia-checked+ arrow-key selection (was styled buttons with CSS-only state).<summary>(settings section/subgroup reset buttons) are gone:<details>/<summary>replaced with a button-driven disclosure (aria-expandedtoggle + reset button as siblings). Structure preserved;.is-openhandles collapse.Labels + landmarks + live regions
id+htmlForlabel association (fixes the "form field needs id/name" and the stacked-row case where the label bound to the reset button).aria-labels; the activity bar is a labelled<nav>.<button>s + a result-count live region; the canvas gets a text alternative (role=img) and city selection is announced via aSelectionAnnouncerlive region; footer build-status, branch resolving, commit/file-preview loads, and CopyButton "Copied" all announce viarole=status.Verification
just test(2518 frontend tests, incl. new tree keyboard, dialog focus, tab keyboard, segmented radio, selection-announcer, and axe audit tests),just lint(eslint + tsc + prettier) all green. Backend untouched.Split out as a follow-up: keyboard-resizable sidebars (#93,
nice-to-have). Full 3D keyboard navigation of the city is intentionally out of scope — the canvas has a text alternative and the tree + search give a keyboard path to select files.🤖 Generated with Claude Code