Replace the three mutually-unaware Escape systems — Modal's private modalStack, the popovers' document listeners with a blanket stopPropagation, and the two players' modalCount() checks — with one shared lib/layerStack.ts. Every open dismissable layer registers in open order; a layer acts on Escape only when it is topmost, and reads anyLayerAbove() to decide whether to defer. Coordination is now a stack lookup, not event-propagation order, so it no longer depends on which listener happens to fire first. - Modal + useDismiss register via the stack; useDismiss drops the blanket stopPropagation (its documented side effect: it swallowed a player's own Escape while any popover was open). - Both players register as a layer and defer via anyLayerAbove() instead of modalCount(); PlayerModal checks it before its maximise/fullscreen steps. A player MENU is a layer too, so it closes first and the next Escape reaches the auto-skip-cancel — fixing the documented PlexPlayer bug. - AnchoredPopover routes non-hover Escape through the stack (floating-ui's own escapeKey off there) and registers as a layer, so a player/modal underneath defers once S4 migrates the in-player menus. FloatingTree still owns popover-vs-popover ordering; the stack adds the cross-type ordering.
39 lines
1.8 KiB
TypeScript
39 lines
1.8 KiB
TypeScript
import { useEffect, type RefObject } from "react";
|
|
import { useLayer } from "./layerStack";
|
|
|
|
/** While `active`, close (call `onClose`) on Escape or a mousedown outside ALL of the given
|
|
* element(s). The reusable half of the popover/dropdown pattern that several components each
|
|
* hand-rolled; positioning stays with the caller (it varies per popover). Pass the panel ref
|
|
* (and optionally a trigger ref so clicking the trigger doesn't immediately re-close).
|
|
*
|
|
* Escape is dispatched through the shared `layerStack` (R8 S3): the popover registers as a layer and
|
|
* closes only when it is topmost, so a Modal or another popover opened above it takes the Escape
|
|
* first, and a player below defers to it. This replaces the old blanket `stopPropagation` — which
|
|
* swallowed a player's own Escape (e.g. PlexPlayer's auto-skip-cancel) whenever any popover was open. */
|
|
export function useDismiss(
|
|
active: boolean,
|
|
onClose: () => void,
|
|
refs: RefObject<HTMLElement | null> | RefObject<HTMLElement | null>[],
|
|
): void {
|
|
// Registration + topmost-only Escape.
|
|
useLayer(active, onClose);
|
|
|
|
// Outside-press stays per-instance (the refs/positioning are the caller's): close on a mousedown
|
|
// that lands outside every registered element.
|
|
useEffect(() => {
|
|
if (!active) return;
|
|
const list = Array.isArray(refs) ? refs : [refs];
|
|
const onDown = (e: MouseEvent) => {
|
|
const target = e.target as Node;
|
|
if (!list.some((r) => r.current?.contains(target))) onClose();
|
|
};
|
|
document.addEventListener("mousedown", onDown);
|
|
return () => {
|
|
document.removeEventListener("mousedown", onDown);
|
|
};
|
|
// Matches the original effect: (re)subscribe only when open/closed toggles. Refs are stable and
|
|
// onClose is read fresh from this run's closure.
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [active]);
|
|
}
|