feat(overlay): shared layer-stack for topmost-only Escape across modals, popovers, players (R8 S3)
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.
This commit is contained in:
@@ -3,20 +3,12 @@ import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { X } from "lucide-react";
|
||||
import { useBackToClose } from "../lib/history";
|
||||
import { pushLayer, removeLayer, isTopmost } from "../lib/layerStack";
|
||||
|
||||
// Stack of open modals so ESC only closes the topmost one — e.g. an error dialog over the
|
||||
// tag editor: pressing ESC dismisses just the error and returns to the editor underneath.
|
||||
let modalStack: number[] = [];
|
||||
let nextModalId = 1;
|
||||
|
||||
/** How many <Modal>s are currently open. The players (which use their own window-level Escape
|
||||
* listeners, not this stack) check it so their Escape defers to a Modal opened above them —
|
||||
* otherwise one Escape closes the player UNDER the dialog and then the dialog too (U-C). */
|
||||
export function modalCount(): number {
|
||||
return modalStack.length;
|
||||
}
|
||||
|
||||
// Small centered modal shell (portaled to <body>): backdrop + ESC + scroll-lock close.
|
||||
// Small centered modal shell (portaled to <body>): backdrop + ESC + scroll-lock close. Each open
|
||||
// modal is a layer in the shared `layerStack` so ESC only closes the topmost one — e.g. an error
|
||||
// dialog over the tag editor dismisses just the error and returns to the editor underneath, and a
|
||||
// player below defers to it (R8 S3).
|
||||
export default function Modal({
|
||||
title,
|
||||
onClose,
|
||||
@@ -41,10 +33,9 @@ export default function Modal({
|
||||
const onCloseRef = useRef(onClose);
|
||||
onCloseRef.current = onClose;
|
||||
useEffect(() => {
|
||||
const id = nextModalId++;
|
||||
modalStack.push(id);
|
||||
const id = pushLayer();
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && modalStack[modalStack.length - 1] === id) {
|
||||
if (e.key === "Escape" && isTopmost(id)) {
|
||||
e.stopPropagation();
|
||||
onCloseRef.current();
|
||||
}
|
||||
@@ -54,7 +45,7 @@ export default function Modal({
|
||||
document.body.style.overflow = "hidden";
|
||||
return () => {
|
||||
window.removeEventListener("keydown", onKey);
|
||||
modalStack = modalStack.filter((x) => x !== id);
|
||||
removeLayer(id);
|
||||
document.body.style.overflow = prev;
|
||||
};
|
||||
}, []);
|
||||
|
||||
Reference in New Issue
Block a user