feat(popover): migrate the dialog surfaces onto the platform (R8 S4c)
The column filter panel, the nav's account panel and the header's sync read-out are mixed content with their own tab order rather than lists of choices, so they migrate as `dialog` popovers — trapping and returning focus, dismissing through the shared layer stack instead of their own document listeners. The column filter needed a structural change: its open state lived in the header row while the panel rendered per column, which cannot work when the platform anchors a panel to its own trigger. Funnel and panel are now one module-level component (module-level still matters — declaring it inline once remounted the popover on every keystroke, which broke the caret and IME composition). That retires three hand-rolled focus chores at once: capturing the trigger element at open time because a per-column ref nulled out before focus could return, the effect that stopped Escape stranding focus on <body>, and the useDismiss registration that had to include the trigger so its own mousedown did not close-then-reopen the panel. The filter also stops being one undifferentiated panel: a text filter is a dialog whose input takes initial focus, while the select and multi filters are real menus with roving focus — select as radios, multi as checkboxes. `menuitemcheckbox` is new to the platform, and the two existing multi-select toggle lists (channel tags, add-to-playlist) move onto it as well: several tags or playlists can be on at once, so they were never one exclusive choice. Role-only change, no interaction difference. Verified in-browser: all three anchor exactly where their hand-rolled placement did, Escape closes and returns focus to the trigger, the text filter keeps focus and caret position while typing narrows the table, and the multi filter opens as a checkbox menu with roving focus.
This commit is contained in:
@@ -197,10 +197,11 @@ interface PopoverListContextValue {
|
||||
}
|
||||
const PopoverListContext = createContext<PopoverListContextValue | null>(null);
|
||||
|
||||
type ItemRole = "menuitem" | "menuitemradio" | "option";
|
||||
type ItemRole = "menuitem" | "menuitemradio" | "menuitemcheckbox" | "option";
|
||||
|
||||
/** One keyboard-navigable option inside a menu/listbox `PopoverSurface`. Self-registers for roving
|
||||
* focus + typeahead and applies the correct ARIA. Use `menuitemradio`+`checked` for a toggle,
|
||||
* focus + typeahead and applies the correct ARIA. Use `menuitemradio`+`checked` when the items are
|
||||
* one exclusive choice, `menuitemcheckbox`+`checked` when several can be on at once,
|
||||
* `option`+`selected` for a listbox choice, plain `menuitem` for an action.
|
||||
*
|
||||
* The active (roving) item carries `data-active` — style the highlight off THAT, not `:focus-visible`:
|
||||
@@ -237,7 +238,7 @@ export function PopoverItem({
|
||||
const active = ctx?.activeIndex === index;
|
||||
|
||||
const ariaProps =
|
||||
role === "menuitemradio"
|
||||
role === "menuitemradio" || role === "menuitemcheckbox"
|
||||
? { role, "aria-checked": !!checked }
|
||||
: role === "option"
|
||||
? { role, "aria-selected": !!selected }
|
||||
|
||||
Reference in New Issue
Block a user