milestone 23: restore the focus ring on select options, check layer order, close preflight gaps

This commit is contained in:
2026-08-12 23:27:28 +02:00
parent 9d5120cbad
commit 0ea2e2905a
4 changed files with 75 additions and 18 deletions
+14 -14
View File
@@ -465,21 +465,21 @@ That last clause is the check that keeps the scope honest, and it holds:
`react-aria-components` is imported only by the four `ui/` wrappers. A RAC
`Button` in a feature file would still be a breach.
### The focus-ring floor does not reach a listbox option
### The focus-ring floor was briefly amended on a false premise
`Select.tsx` gives the trigger the shared focus ring, but the popover's options
show keyboard focus as a background inversion — `primary` behind `primaryText`
— with the outline removed, and the listbox itself has no ring. That is the
selection-highlight convention for a listbox and a strong-contrast indicator,
but it is not ruling 3's 2px focus-token outline, and the code is in breach as
the ruling is written. Two things make the ring the wrong mechanism here rather
than a missing one: an option inside a scrolling popover clips its own outline,
and RAC drives option focus through its `isFocused` render prop, so a
`:focus-visible` rule — which is how the shared `focusRing` is written — may
never match the element at all. The floor is amended to cover controls that
take DOM focus; a listbox option instead requires a focus indicator with a
contrast change against both schemes, which the inversion provides. Every other
interactive element, the trigger included, keeps the ring.
S2 left the Select's options with their outline removed, showing keyboard focus
as a background inversion alone. This section previously amended ruling 3 to
excuse that, arguing RAC drives option focus through an `isFocused` render prop
so a `:focus-visible` rule could never match. That argument is wrong.
`useSelectableItem` gives options a roving `tabIndex` and focuses the option's
own DOM node (`react-aria/dist/private/selection/useSelectableItem.mjs:64`),
which `ListBoxItem` spreads onto its element — so `:focus-visible` matches and
the ring was simply missing. The amendment is withdrawn and ruling 3 stands
unchanged. `Select.tsx` now composes the shared inset ring onto every option:
inset because an option flush against a scrolling popover clips an outset
outline, and recoloured to `primaryText` on the highlighted row because the
focus token is the same blue that row paints behind it. The inversion stays as
the listbox convention; it is no longer the only indicator.
### Two notes for S3