import { useEffect, useRef } from "react"; // R8 S3 — one shared registry of open "dismissable layers" (modals, popovers, players), ordered by // open time. It replaces the three mutually-unaware Escape systems that each guessed at ordering with // its own `stopPropagation`: Modal's private `modalStack`, the popovers' document listeners, and the // two players' `modalCount()` checks. Now EVERY layer keeps its own key handler but coordinates // through this one stack — a layer acts on Escape only when it is topmost, and reads `anyAbove()` to // know whether to defer. Because the decision is a stack lookup, not event-propagation order, the fix // is robust to which listener happens to run first (the old stopPropagation was not). // // Notably this fixes the documented PlexPlayer bug: its menus register here, so the player sees a menu // as a layer ABOVE it and defers, letting the menu's own Escape close it first — the player's // auto-skip-cancel Escape then lands on the next press, instead of being swallowed by a blanket // stopPropagation. (Popover-vs-popover ordering is still floating-ui's ; this stack adds // the cross-TYPE ordering the tree can't see.) let stack: number[] = []; let nextId = 1; export function pushLayer(): number { const id = nextId++; stack.push(id); return id; } export function removeLayer(id: number): void { stack = stack.filter((x) => x !== id); } export function isTopmost(id: number): boolean { return stack.length > 0 && stack[stack.length - 1] === id; } /** True when at least one layer sits above `id` — the "should I defer?" predicate for a layer with a * richer key handler than a plain close (the players). */ export function anyLayerAbove(id: number): boolean { const i = stack.indexOf(id); return i !== -1 && i < stack.length - 1; } /** Register an open layer for as long as `active` is true, and get live predicates to read inside your * own key handler. Pass `onEscape` to also install a topmost-only Escape listener (the common case — * modals, popovers). Omit it for a layer that owns a bigger key handler and only needs `anyAbove()` * to decide whether to defer (the players). */ export function useLayer( active: boolean, onEscape?: () => void, ): { isTopmost: () => boolean; anyAbove: () => boolean } { const idRef = useRef(null); const escRef = useRef(onEscape); escRef.current = onEscape; useEffect(() => { if (!active) return; const id = pushLayer(); idRef.current = id; let onKey: ((e: KeyboardEvent) => void) | undefined; if (escRef.current) { onKey = (e: KeyboardEvent) => { if (e.key === "Escape" && isTopmost(id)) escRef.current?.(); }; document.addEventListener("keydown", onKey); } return () => { if (onKey) document.removeEventListener("keydown", onKey); removeLayer(id); idRef.current = null; }; // Re-register only when open/closed toggles; onEscape is read fresh via escRef. }, [active]); return { isTopmost: () => idRef.current != null && isTopmost(idRef.current), anyAbove: () => idRef.current != null && anyLayerAbove(idRef.current), }; }