From c11e2822e9904c0af3b4f0fe3c10f4d96b694b25 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 8 Aug 2026 04:51:02 +0200 Subject: [PATCH] =?UTF-8?q?feat(a11y):=20R9=20focus=20foundation=20?= =?UTF-8?q?=E2=80=94=20app-wide=20focus-visible=20fallback=20+=20nav=20rai?= =?UTF-8?q?l=20focus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- frontend/src/components/AnchoredPopover.tsx | 2 +- frontend/src/components/NavSidebar.tsx | 13 +++++++------ frontend/src/index.css | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/AnchoredPopover.tsx b/frontend/src/components/AnchoredPopover.tsx index f4c173fe..bdce815e 100644 --- a/frontend/src/components/AnchoredPopover.tsx +++ b/frontend/src/components/AnchoredPopover.tsx @@ -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} diff --git a/frontend/src/components/NavSidebar.tsx b/frontend/src/components/NavSidebar.tsx index 15c7d638..1f496586 100644 --- a/frontend/src/components/NavSidebar.tsx +++ b/frontend/src/components/NavSidebar.tsx @@ -168,7 +168,8 @@ export default function NavSidebar({ .filter((p) => sys.has(p)) .map((p) => ({ page: p, ...META(p) })); - const rowBase = "w-full flex items-center gap-3 rounded-lg px-2.5 py-2 text-sm transition"; + const rowBase = + "selectable w-full flex items-center gap-3 rounded-lg px-2.5 py-2 text-sm transition"; const name = me.display_name ?? me.email.split("@")[0]; // Role is surfaced as a plain title tooltip on the account row (no visual badge). Demo isn't a // real role (it's a flag on a `user` row), so surface it first. @@ -240,7 +241,7 @@ export default function NavSidebar({ {!slim && ( @@ -289,7 +290,7 @@ export default function NavSidebar({ onClick={onOpenAbout} title={t("header.account.about")} aria-label={t("header.account.about")} - className="p-2 rounded-lg text-muted hover:text-fg hover:bg-card transition" + className="p-2 rounded-lg text-muted hover:text-fg hover:bg-card transition outline-hidden focus-visible:outline-2 focus-visible:outline-accent/60" > @@ -297,7 +298,7 @@ export default function NavSidebar({ onClick={logout} title={t("header.account.signOut")} aria-label={t("header.account.signOut")} - className="p-2 rounded-lg text-muted hover:text-fg hover:bg-card transition" + className="p-2 rounded-lg text-muted hover:text-fg hover:bg-card transition outline-hidden focus-visible:outline-2 focus-visible:outline-accent/60" > @@ -322,7 +323,7 @@ export default function NavSidebar({ ref={acctPop.refs.setReference} {...acctPop.getReferenceProps()} title={slim ? `${name} · ${roleLabel}` : undefined} - className={`${rowBase} selectable ${slim ? "justify-center px-0" : ""} text-muted hover:text-fg hover:bg-card`} + className={`${rowBase} ${slim ? "justify-center px-0" : ""} text-muted hover:text-fg hover:bg-card`} >