fix(e10): review round 3 — settle the focus race in a browser instead of guessing again
Round 3 reviewed round 2's diff. Trend across the loop: 32 findings over the whole epic, 15 over round 1's fixes, 11 over round 2's. What it caught is mostly the same shape as before — a guard narrowed until it was wrong in the other direction — plus one item this branch had now got wrong three different ways. Backspace ownership over the player, third and final answer. Round 1 deferred to nothing (the cheat sheet took the key and the player stopped underneath it). Round 1's fix deferred to `anyLayerAbove` (the player's own gear/tracks popovers push layers too, so it stopped closing them). Round 2's fix named the sheet and palette (the app-wide ErrorDialog is neither, so the player tore down under it). The rule that actually holds is not a list of overlays: the player's own panels answer first, because handleBack's cascade is what closes them, and anything else layered above takes the key. The rail's inline rename is settled by measurement rather than argument. Two rounds reasoned about whether a React passive effect lands before or after floating-ui's return-focus microtask and reached opposite conclusions; a jsdom harness built to decide it reproduced no failure for ANY shape, because focus never enters the menu there — so it was deleted rather than kept as a test that discriminates nothing. Checked in a real browser instead: the field opens and holds focus. The shipped fix works, and the comment now says what was measured rather than what was inferred. The same check found that round 1's OTHER rail fix never worked at all. Escape does run its own handler (typing then Escape leaves the name unchanged, so it cancels rather than blur-commits) and the row button is focusable when focused from outside — yet focus still lands on <body>. Three explanations, none of which survived. So the refocus machinery is reverted to the shape that shipped before this epic and the gap is documented where the next person will hit it, with the evidence and an instruction to instrument before theorising. A fourth guess would have been worse than a known bug. Also: the two comments in Playlists.tsx that still claimed the disabled menu rows explain themselves — untrue since round 2 reverted `inert`, and exactly the comment-vs-code drift CLAUDE.md calls the source of the worst bugs here. And the move-failure toast printed HttpError.detail verbatim, which for the reachable 404 is the backend's bare English "Unknown playlist"; only a structured detail has been through localizeDetail, so only a structured detail reaches the toast now.
This commit is contained in:
@@ -710,14 +710,22 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||
stats: () => setStatsOpen((v) => !v),
|
||||
back: (ev) => {
|
||||
// HTPC-style "stop & back to the feed" — mirrors the mouse Back button (shared cascade).
|
||||
// Opening the cheat sheet (or the palette) over the player and pressing ⌫ used to stop
|
||||
// playback and drop to the Plex grid while the overlay stayed up: Modal stopPropagation()s
|
||||
// Escape ONLY, and its focused close button is not a typing target, so the key sailed
|
||||
// through (E10 review round 1). The first fix deferred to `anyLayerAbove`, which was far
|
||||
// too broad — the player's OWN gear/tracks menus register layers via useDismiss, so ⌫
|
||||
// stopped closing them and did nothing at all (round 2). Only these two app-wide overlays
|
||||
// own Backspace, so name them rather than every layer.
|
||||
if (helpOpenRef.current || paletteOpenRef.current) return;
|
||||
// Who owns ⌫ when something is open over the player. Modal stopPropagation()s Escape ONLY
|
||||
// and its focused close button is not a typing target, so the key reaches us regardless.
|
||||
// Two wrong answers so far: deferring to nothing at all (round 1's bug — the cheat sheet
|
||||
// took ⌫ and the player stopped underneath it), then deferring to `anyLayerAbove` outright
|
||||
// (round 1's fix, round 2's bug — the player's OWN gear/tracks popovers push layers too, so
|
||||
// ⌫ stopped closing them), then naming just the sheet and palette (round 2's fix, round 3's
|
||||
// bug — the app-wide ErrorDialog is neither, so ⌫ tore the player down under it).
|
||||
// The rule that holds: the player's own panels answer first, because `handleBack`'s cascade
|
||||
// is what closes them; anything else layered above us takes the key.
|
||||
const ownPanelOpen =
|
||||
menuOpenRef.current ||
|
||||
infoOpenRef.current ||
|
||||
statsOpenRef.current ||
|
||||
upNextOpenRef.current;
|
||||
if (!ownPanelOpen && layerIdRef.current != null && anyLayerAbove(layerIdRef.current))
|
||||
return;
|
||||
ev.preventDefault();
|
||||
if (skipProgressRef.current != null)
|
||||
cancelAutoSkipRef.current(); // cancel auto-skip, don't go back
|
||||
|
||||
Reference in New Issue
Block a user