feat(a11y): R9 focus foundation — glass shadow fix, focus-visible ring, highlight/selected split
S1 — recipe foundation: - Move .glass/.glass-card/.glass-menu into @layer components so a JSX shadow-* utility overrides the glass box-shadow again; the ~9 dead shadow-* on glass surfaces (modals, menus, save-bar, chat) now paint their intended drop-shadow. Perf-mode and :hover overrides stay unlayered so they still win. - Add a .selectable recipe: the hover tint menu/list rows already use, PLUS the app-wide focus-visible ring that was missing on non-input controls (the biggest a11y gap). - Split the popover roving highlight from the selected state (AnchoredPopover): a selected/checked PopoverItem now gets a ring ONLY, so its resting paint (bg-accent fill / text-accent / checkbox) survives being highlighted instead of downgrading to a 35% tint or being recoloured. Fixes the 4 review findings deferred from R10-S0 generically; drops the leftover per-consumer data-active ring in tableHeader. S2 — worst offenders: - Account-menu trigger + switch/add/sign-out rows adopt .selectable (visible keyboard focus). - Saved-views rows: group-focus-within reveals the hover-only action buttons for keyboard users; aria-current on the active row; focus-visible ring on every button. - The 3 invisible-focus inputs get an accent focus ring: SearchBar + ShareDialog via focus-within on the glass pill/row, AboutCell via focus-visible on the trigger. Deferred to a later R9 sprint: the full unification of all ~14 selection idioms, a shared <NavItem>, and .is-current/.chip recipes (no consumer yet in this pass). Verified: tsc/eslint/prettier/knip/vitest (79) green; browser computed-style checks confirm the glass shadow now wins, the focus outlines render (2px accent), and the split renders (selected ring-only, unselected fill).
This commit is contained in:
@@ -250,11 +250,15 @@ type ItemRole = "menuitem" | "menuitemradio" | "menuitemcheckbox" | "option";
|
||||
* focus-visible heuristic, so a `:focus-visible` highlight can stay invisible until the first arrow
|
||||
* key. (Since the Tailwind v4 migration `bg-accent/NN` works — v4 does the opacity modifier via
|
||||
* color-mix — so the recipe no longer needs a hand-written color-mix arbitrary value.) */
|
||||
// Applied to EVERY PopoverItem so the roving-focus row is clearly visible across all schemes: an
|
||||
// accent fill plus a 1px inset accent ring for a defined edge. (The previous per-consumer 24% fill
|
||||
// alone read as barely-there — a real UAT finding, 2026-08-08.)
|
||||
const ACTIVE_HIGHLIGHT =
|
||||
// The roving-focus highlight, SPLIT by selection state (R9). A NON-selected row gets the familiar
|
||||
// accent fill + inset ring. A SELECTED/checked row gets ring-ONLY, so the consumer's own resting
|
||||
// selected paint (a bg-accent fill, text-accent, a checkbox) SURVIVES being highlighted instead of
|
||||
// being downgraded to a 35% tint or having its text recoloured (the R10-S0 review findings). The two
|
||||
// can't be merged onto one recipe: the fill and the selected paint both drive background/color on the
|
||||
// same element, so the highlight must yield to the selected state rather than overwrite it.
|
||||
const HIGHLIGHT_UNSELECTED =
|
||||
"data-active:bg-accent/35 data-active:text-fg data-active:ring-1 data-active:ring-inset data-active:ring-accent/60";
|
||||
const HIGHLIGHT_SELECTED = "data-active:ring-2 data-active:ring-inset data-active:ring-accent/70";
|
||||
|
||||
export function PopoverItem({
|
||||
children,
|
||||
@@ -323,7 +327,7 @@ export function PopoverItem({
|
||||
tabIndex={focusActive ? 0 : -1}
|
||||
data-active={visualActive ? "true" : undefined}
|
||||
disabled={disabled}
|
||||
className={`${ACTIVE_HIGHLIGHT}${className ? ` ${className}` : ""}`}
|
||||
className={`${checked || selected ? HIGHLIGHT_SELECTED : HIGHLIGHT_UNSELECTED}${className ? ` ${className}` : ""}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user