a11y: semantic headings (UI33) + command-palette listbox semantics (UI27)

UI33 — render titles as real headings so Narrator heading-navigation works:
the shared ScreenHeader title is now <Subtitle2 as="h1"> (one h1 per screen in
one place — Downloads/History/Library/Settings/Terminal); DownloadsView "Queue
(N)" and all 11 settings cards are <Subtitle2 as="h2"> sections; Onboarding
"Welcome" is <Title2 as="h1">. A minimal h1–h6 margin reset in base.css zeroes
the UA heading margin (Fluent's typography classes out-specify the element
selector, so the visual ramp is unchanged) so layout matches the former spans.

UI27 — give the command palette proper combobox/listbox semantics: the input is
role="combobox" with aria-controls/aria-activedescendant/aria-autocomplete, the
list is role="listbox", and each row is role="option" with aria-selected, so the
active row is announced to screen readers via active-descendant. Focus stays on
the input (standard combobox pattern), so the rows became non-focusable divs;
onMouseEnter is kept to unify pointer + keyboard highlight on one sel state.

Also corrected UI30's checkbox in the audit (already implemented via the shared
SegmentedControl radiogroup; only the marker lagged).

typecheck + 253 tests + eslint + prettier + production build all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 08:02:54 -04:00
parent 36699531cf
commit 4aec32e3c2
17 changed files with 69 additions and 27 deletions
+16 -2
View File
@@ -952,9 +952,16 @@ are an undefined spacing/radius/type scale and a drift between Fluent components
### Hover, focus, disabled, keyboard & accessibility
- [ ] **UI27 — Command palette uses JS hover, not CSS.** Items highlight via `onMouseEnter` setting
- [x] **UI27 — Command palette uses JS hover, not CSS.** Items highlight via `onMouseEnter` setting
`itemActive` (no `:hover`), unlike every other list, and expose no `aria-selected`/active-descendant,
so the highlight is invisible to screen readers. **Standard:** CSS `:hover` + listbox/option semantics.
*Fixed in [CommandPalette.tsx](src/renderer/src/components/CommandPalette.tsx): the input is now a
`role="combobox"` with `aria-controls`/`aria-activedescendant`/`aria-autocomplete="list"`, the list is a
`role="listbox"`, and each row is a `role="option"` with `aria-selected` — so the active row is announced
to Narrator via active-descendant (the standard combobox pattern; focus stays on the input, so the rows
became non-focusable `div`s rather than `button`s). `onMouseEnter` is kept deliberately: it unifies the
pointer and keyboard highlight onto one `sel` state, which — now that `aria-activedescendant` tracks it —
is more correct than a pure CSS `:hover` (which wouldn't move the announced selection).*
- [x] **UI28 — Command palette input has no focus ring.** The search `<input>` sets `outline: 'none'`
with no replacement. **Standard:** a visible focus ring (Fluent stroke token). *(a11y)*
- [x] **UI29 — Fragmented focus treatment.** Fluent ring (Fluent controls) vs custom border (Select)
@@ -968,9 +975,16 @@ are an undefined spacing/radius/type scale and a drift between Fluent components
so that control can't show a disabled state while Fluent controls can. **Standard:** add `disabled` + styling.
- [ ] **UI32 — Native-control dark mode is uneven.** The DownloadBar `datetime-local` sets
`colorScheme: 'light dark'` explicitly; the `Select` relies on the root `colorScheme`. **Standard:** set `colorScheme` consistently on both.
- [ ] **UI33 — No semantic headings.** Titles render as `Subtitle2`/`Title2` (styled spans), so there's
- [x] **UI33 — No semantic headings.** Titles render as `Subtitle2`/`Title2` (styled spans), so there's
no h1h6 hierarchy for screen-reader heading navigation (landmarks `<nav>`/`<main>` exist; headings
don't). **Standard:** render titles as real headings (`as="h1"`/`"h2"` or `role="heading" aria-level`). *(a11y)*
*Fixed by rendering the Fluent typography titles as real headings (the `as` prop keeps the visual ramp):
the shared [ScreenHeader](src/renderer/src/components/ui/Screen.tsx) title is now `<Subtitle2 as="h1">`,
giving every screen (Downloads, History, Library, Settings, Terminal) one `h1` in one place; the
DownloadsView "Queue (N)" and all 11 settings cards are `<Subtitle2 as="h2">` sections beneath it; and the
Onboarding "Welcome" title is `<Title2 as="h1">`. A minimal `h1h6` margin reset in
[base.css](src/renderer/src/assets/base.css) zeroes the UA heading margin (Fluent's typography classes
out-specify the element selector, so the visual ramp is unchanged) so the layout matches the former spans.*
### Top cohesion-breakers (the "not one product" shortlist)