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:
@@ -34,6 +34,7 @@ import {
|
||||
type Placement,
|
||||
type UseRoleProps,
|
||||
} from "@floating-ui/react";
|
||||
import { useLayer } from "../lib/layerStack";
|
||||
|
||||
// R8 popover platform. ONE anchored-popover primitive over @floating-ui/react, replacing the ~9
|
||||
// hand-rolled positioning engines (measured flip / width clamp / rAF scroll-follow / scroller-relative
|
||||
@@ -136,7 +137,11 @@ export function useAnchoredPopover(opts: AnchoredPopoverOptions) {
|
||||
handleClose: safePolygon(),
|
||||
});
|
||||
const focus = useFocus(context, { enabled: isHover });
|
||||
const dismiss = useDismiss(context);
|
||||
// Escape for an interactive (non-hover) popover is dispatched through the shared `layerStack`
|
||||
// (below) so it coordinates with Modals and players, not just other floating nodes — so turn
|
||||
// floating-ui's own Escape off there. Outside-press stays with floating-ui (tree-aware). Hover
|
||||
// tooltips keep floating-ui's Escape and are NOT layers (they mustn't make a player defer).
|
||||
const dismiss = useDismiss(context, { escapeKey: isHover });
|
||||
const role = useRole(context, { role: opts.role ?? (isHover ? "tooltip" : "menu") });
|
||||
const listNav = useListNavigation(context, {
|
||||
enabled: isNav,
|
||||
@@ -157,6 +162,10 @@ export function useAnchoredPopover(opts: AnchoredPopoverOptions) {
|
||||
|
||||
const interactions = useInteractions([click, hover, focus, dismiss, role, listNav, typeahead]);
|
||||
|
||||
// Register the open interactive popover as a dismissable layer (topmost-only Escape, and so a
|
||||
// player/modal underneath defers to it). Hover tooltips are excluded on purpose.
|
||||
useLayer(opts.open && !isHover, () => opts.onOpenChange(false));
|
||||
|
||||
return {
|
||||
nodeId,
|
||||
mode,
|
||||
|
||||
Reference in New Issue
Block a user