fix(popover): R10 weak-ring — checked rows get the full highlight fill

A hovered/roving CHECKED row got only a thin ring (R9 split), reading as a weak
afterthought next to an unchecked row's vivid accent fill. Now selected rows get the
SAME bg-accent/35 fill + a STRONGER ring (ring-2 accent/80 vs the unselected ring-1
accent/60); the child checkmark/checkbox stays legible on the tint. The one idiom that
DID rely on the R9 ring-only — the table-filter options, whose selected item paints its
own solid bg-accent ROW — opts back in via a new `keepSelectedPaint` PopoverItem prop,
so its solid paint isn't downgraded to a 35% tint. Scopes the R9 exception to the one
menu that needs it instead of penalising every checkbox menu.
This commit is contained in:
2026-08-09 17:46:13 +02:00
parent eee7443e6f
commit 5c5c045ed9
2 changed files with 25 additions and 8 deletions
+23 -8
View File
@@ -250,15 +250,20 @@ type ItemRole = "menuitem" | "menuitemradio" | "menuitemcheckbox" | "option";
* focus-visible heuristic, so a `:focus-visible` highlight can stay invisible until the first arrow * 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 * 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.) */ * color-mix — so the recipe no longer needs a hand-written color-mix arbitrary value.) */
// The roving-focus highlight, SPLIT by selection state (R9). A NON-selected row gets the familiar // The roving-focus highlight. A hovered/roving row gets the SAME strong accent fill whether or not it
// accent fill + inset ring. A SELECTED/checked row gets ring-ONLY, so the consumer's own resting // is selected — a checked row no longer reads as a weak ring-only afterthought (R10 weak-ring fix). The
// selected paint (a bg-accent fill, text-accent, a checkbox) SURVIVES being highlighted instead of // selected state stays legible ON the fill because these menus mark "selected" with a CHILD element (a
// being downgraded to a 35% tint or having its text recoloured (the R10-S0 review findings). The two // checkmark / checkbox) that paints over the row tint; the selected row just gets a stronger ring
// can't be merged onto one recipe: the fill and the selected paint both drive background/color on the // (ring-2, accent/80) than an unselected one (ring-1, accent/60) to keep the two apart.
// same element, so the highlight must yield to the selected state rather than overwrite it. // EXCEPTION: a menu whose SELECTED item paints its OWN row background (a solid bg-accent — the
// table-filter options) passes `keepSelectedPaint`, so the highlight yields to ring-only there; a fill
// would downgrade that solid paint to a 35% tint (the R9 concern, now scoped to just that idiom).
const HIGHLIGHT_UNSELECTED = 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"; "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"; const HIGHLIGHT_SELECTED =
"data-active:bg-accent/35 data-active:text-fg data-active:ring-2 data-active:ring-inset data-active:ring-accent/80";
const HIGHLIGHT_SELECTED_RING_ONLY =
"data-active:ring-2 data-active:ring-inset data-active:ring-accent/70";
export function PopoverItem({ export function PopoverItem({
children, children,
@@ -266,6 +271,7 @@ export function PopoverItem({
role = "menuitem", role = "menuitem",
checked, checked,
selected, selected,
keepSelectedPaint,
disabled, disabled,
className, className,
/** Typeahead match string; defaults to the text child. Set explicitly when children aren't plain text. */ /** Typeahead match string; defaults to the text child. Set explicitly when children aren't plain text. */
@@ -277,6 +283,9 @@ export function PopoverItem({
role?: ItemRole; role?: ItemRole;
checked?: boolean; checked?: boolean;
selected?: boolean; selected?: boolean;
/** Set when the SELECTED item paints its OWN row background (a solid bg-accent, not a child checkmark):
* the active highlight then yields to ring-only so the fill doesn't downgrade that solid paint. */
keepSelectedPaint?: boolean;
disabled?: boolean; disabled?: boolean;
className?: string; className?: string;
label?: string; label?: string;
@@ -327,7 +336,13 @@ export function PopoverItem({
tabIndex={focusActive ? 0 : -1} tabIndex={focusActive ? 0 : -1}
data-active={visualActive ? "true" : undefined} data-active={visualActive ? "true" : undefined}
disabled={disabled} disabled={disabled}
className={`outline-hidden ${checked || selected ? HIGHLIGHT_SELECTED : HIGHLIGHT_UNSELECTED}${className ? ` ${className}` : ""}`} className={`outline-hidden ${
checked || selected
? keepSelectedPaint
? HIGHLIGHT_SELECTED_RING_ONLY
: HIGHLIGHT_SELECTED
: HIGHLIGHT_UNSELECTED
}${className ? ` ${className}` : ""}`}
> >
{children} {children}
</button> </button>
+2
View File
@@ -156,6 +156,7 @@ function FilterFunnel<T>({
<PopoverItem <PopoverItem
role="menuitemradio" role="menuitemradio"
checked={!filterOn} checked={!filterOn}
keepSelectedPaint
onSelect={() => onApply(col.key, "")} onSelect={() => onApply(col.key, "")}
label={t("datatable.all")} label={t("datatable.all")}
className={`text-left text-xs px-2 py-1.5 rounded-md outline-hidden transition ${ className={`text-left text-xs px-2 py-1.5 rounded-md outline-hidden transition ${
@@ -169,6 +170,7 @@ function FilterFunnel<T>({
key={o.value} key={o.value}
role="menuitemradio" role="menuitemradio"
checked={value === o.value} checked={value === o.value}
keepSelectedPaint
onSelect={() => onApply(col.key, o.value)} onSelect={() => onApply(col.key, o.value)}
label={o.label} label={o.label}
className={`text-left text-xs px-2 py-1.5 rounded-md outline-hidden transition ${ className={`text-left text-xs px-2 py-1.5 rounded-md outline-hidden transition ${