fix(plex): the player's ⋯ and tracks lists are real menus, and every close returns the focus

They were hand-built divs: no menu roles, no arrow keys, and Escape left the
focus on <body>. Both run on the popover engine now (media-tone rows), and the
tuning panel is a labelled dialog that takes the focus and hands it back to ⋯.
This commit is contained in:
2026-09-27 14:22:37 +02:00
parent 9bedd96774
commit a975ab627a
8 changed files with 224 additions and 90 deletions
+14 -5
View File
@@ -104,6 +104,10 @@ 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";
// On the always-dark media chrome (a player's menus, `bg-media-chrome` under white text) the theme's
// `text-fg` is dark in the light theme: the highlighted row would go dark-on-dark. White instead.
const MEDIA_HIGHLIGHT =
"data-active:bg-white/15 data-active:text-white data-active:ring-1 data-active:ring-inset data-active:ring-white/40";
export function PopoverItem({
children,
@@ -117,6 +121,7 @@ export function PopoverItem({
elementRef,
/** Typeahead match string; defaults to the text child. Set explicitly when children aren't plain text. */
label,
tone,
...rest
}: {
children: ReactNode;
@@ -134,6 +139,8 @@ export function PopoverItem({
* aria-haspopup/aria-expanded belong on that button — not on a wrapper div around it. */
elementRef?: (node: HTMLButtonElement | null) => void;
label?: string;
/** `media`: the row sits on the always-dark media chrome (a player's menu), not a theme surface. */
tone?: "media";
} & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "onSelect" | "role" | "children" | "className">) {
const ctx = useContext(PopoverListContext);
const virtual = ctx?.virtual ?? false;
@@ -212,11 +219,13 @@ export function PopoverItem({
disabled={disabled}
// touch-target: a menu row is a control primitive, 44 tall on a coarse pointer (A3).
className={`outline-hidden touch-target ${
checked || selected
? keepSelectedPaint
? HIGHLIGHT_SELECTED_RING_ONLY
: HIGHLIGHT_SELECTED
: ROVING_HIGHLIGHT
tone === "media"
? MEDIA_HIGHLIGHT
: checked || selected
? keepSelectedPaint
? HIGHLIGHT_SELECTED_RING_ONLY
: HIGHLIGHT_SELECTED
: ROVING_HIGHLIGHT
}${className ? ` ${className}` : ""}`}
>
{children}
@@ -27,7 +27,6 @@ const PLAIN: Record<string, [number, string]> = {
"PlaylistsRail.tsx": [1, "the load-error retry"],
"PlexCollectionEditor.tsx": [1, "a checkbox row in a picker"],
"PlexInfo.tsx": [1, "the rating filter (a number)"],
"PlexPlayer.tsx": [4, "track picker rows and the ⋯ menu's rows"],
"PlexPlayerSettings.tsx": [2, "setting options"],
"PlexPlaylistAdd.tsx": [1, "a checkbox row in a picker"],
"PlexPlaylistView.tsx": [1, "a show's poster stack (aria-hidden)"],