feat(a11y): R9 focus foundation — app-wide focus-visible fallback + nav rail focus

- Global :focus-visible fallback (@layer base): one accent outline for keyboard users on every
  button/link/[role=button]/[role=tab]/summary that doesn't define its own. Non-input controls had no
  visible keyboard focus app-wide; this closes the void with a single rule. More specific focus styles
  (.selectable, an input's focus:ring, a menu item's data-active highlight) override it.
- PopoverItem now carries outline-hidden so menu items keep their data-active highlight without also
  drawing the global outline (no double ring).
- Nav rail rows adopt .selectable (via rowBase); rail chrome buttons (logo/pin/about/sign-out) get a
  focus-visible ring; the settings row already uses rowBase. Deduped the account trigger's selectable.

Deferred to R10: the chip/pill selected-PAINT unification (a .chip/.pill/.is-current repaint of the
~14 selection idioms) — the R10 token redefinition repaints those surfaces anyway, at less risk than a
hand migration now. R9's job (the focus/a11y foundation) is complete.

Verified: tsc/eslint/prettier/knip/vitest (79) green; browser computed-style confirms the global ring
renders (2px accent, offset 2px) on a plain button, .selectable overrides it with an inset ring, and
an outline-hidden menu button draws no global outline.
This commit is contained in:
2026-08-08 04:51:02 +02:00
parent 51f9c8d0ed
commit c11e2822e9
3 changed files with 23 additions and 7 deletions
+1 -1
View File
@@ -327,7 +327,7 @@ export function PopoverItem({
tabIndex={focusActive ? 0 : -1}
data-active={visualActive ? "true" : undefined}
disabled={disabled}
className={`${checked || selected ? HIGHLIGHT_SELECTED : HIGHLIGHT_UNSELECTED}${className ? ` ${className}` : ""}`}
className={`outline-hidden ${checked || selected ? HIGHLIGHT_SELECTED : HIGHLIGHT_UNSELECTED}${className ? ` ${className}` : ""}`}
>
{children}
</button>