@seed-design/react-dismissible-layer 0.0.0-alpha-20260414104312
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/DismissibleLayer-12s-Cz1jvGI2.js +23 -0
- package/lib/DismissibleLayer-12s-ZXi0vrlt.cjs +23 -0
- package/lib/index.cjs +13 -0
- package/lib/index.d.ts +112 -0
- package/lib/index.js +3 -0
- package/lib/layer-stack-12s-Ctg3ipjW.cjs +125 -0
- package/lib/layer-stack-12s-DjRVp1_u.js +112 -0
- package/lib/use-escape-keydown-12s-Bl_her-_.js +31 -0
- package/lib/use-escape-keydown-12s-CNZqs1dI.cjs +31 -0
- package/lib/use-focus-outside-12s-Ch-SoiiA.cjs +48 -0
- package/lib/use-focus-outside-12s-LpFt7UjO.js +48 -0
- package/lib/use-pointer-down-outside-12s-BLctGw0m.js +202 -0
- package/lib/use-pointer-down-outside-12s-nlxC-DrD.cjs +202 -0
- package/lib/useDismissibleLayer-12s-C34wvjXv.js +146 -0
- package/lib/useDismissibleLayer-12s-D4qWttkW.cjs +146 -0
- package/package.json +45 -0
- package/src/DismissibleLayer.tsx +25 -0
- package/src/index.ts +17 -0
- package/src/layer-stack.test.ts +291 -0
- package/src/layer-stack.ts +169 -0
- package/src/use-escape-keydown.ts +31 -0
- package/src/use-focus-outside.ts +62 -0
- package/src/use-pointer-down-outside.ts +272 -0
- package/src/useDismissibleLayer.test.tsx +732 -0
- package/src/useDismissibleLayer.ts +234 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { composeRefs } from '@radix-ui/react-compose-refs';
|
|
4
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
5
|
+
import { forwardRef } from 'react';
|
|
6
|
+
import { u as useDismissibleLayer } from './useDismissibleLayer-12s-C34wvjXv.js';
|
|
7
|
+
import { D as DismissibleParentContext } from './layer-stack-12s-DjRVp1_u.js';
|
|
8
|
+
|
|
9
|
+
const DismissibleLayer = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
10
|
+
const { children, ...options } = props;
|
|
11
|
+
const { dismissibleRef, dismissibleProps, layerNode } = useDismissibleLayer(options);
|
|
12
|
+
return /*#__PURE__*/ jsx(DismissibleParentContext.Provider, {
|
|
13
|
+
value: layerNode,
|
|
14
|
+
children: /*#__PURE__*/ jsx(Slot, {
|
|
15
|
+
ref: composeRefs(ref, dismissibleRef),
|
|
16
|
+
...dismissibleProps,
|
|
17
|
+
children: children
|
|
18
|
+
})
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
DismissibleLayer.displayName = "DismissibleLayer";
|
|
22
|
+
|
|
23
|
+
export { DismissibleLayer as D };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
3
|
+
var reactComposeRefs = require('@radix-ui/react-compose-refs');
|
|
4
|
+
var reactSlot = require('@radix-ui/react-slot');
|
|
5
|
+
var react = require('react');
|
|
6
|
+
var useDismissibleLayer12s = require('./useDismissibleLayer-12s-D4qWttkW.cjs');
|
|
7
|
+
var layerStack12s = require('./layer-stack-12s-Ctg3ipjW.cjs');
|
|
8
|
+
|
|
9
|
+
const DismissibleLayer = /*#__PURE__*/ react.forwardRef((props, ref)=>{
|
|
10
|
+
const { children, ...options } = props;
|
|
11
|
+
const { dismissibleRef, dismissibleProps, layerNode } = useDismissibleLayer12s.useDismissibleLayer(options);
|
|
12
|
+
return /*#__PURE__*/ jsxRuntime.jsx(layerStack12s.DismissibleParentContext.Provider, {
|
|
13
|
+
value: layerNode,
|
|
14
|
+
children: /*#__PURE__*/ jsxRuntime.jsx(reactSlot.Slot, {
|
|
15
|
+
ref: reactComposeRefs.composeRefs(ref, dismissibleRef),
|
|
16
|
+
...dismissibleProps,
|
|
17
|
+
children: children
|
|
18
|
+
})
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
DismissibleLayer.displayName = "DismissibleLayer";
|
|
22
|
+
|
|
23
|
+
exports.DismissibleLayer = DismissibleLayer;
|
package/lib/index.cjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
var useDismissibleLayer12s = require('./useDismissibleLayer-12s-D4qWttkW.cjs');
|
|
2
|
+
var DismissibleLayer12s = require('./DismissibleLayer-12s-ZXi0vrlt.cjs');
|
|
3
|
+
var layerStack12s = require('./layer-stack-12s-Ctg3ipjW.cjs');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
exports.useDismissibleLayer = useDismissibleLayer12s.useDismissibleLayer;
|
|
8
|
+
exports.DismissibleLayer = DismissibleLayer12s.DismissibleLayer;
|
|
9
|
+
exports.LayerStackContext = layerStack12s.LayerStackContext;
|
|
10
|
+
exports.addBranch = layerStack12s.addBranch;
|
|
11
|
+
exports.isTopMost = layerStack12s.isTopMost;
|
|
12
|
+
exports.removeBranch = layerStack12s.removeBranch;
|
|
13
|
+
exports.useLayerStackContext = layerStack12s.useLayerStackContext;
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { CSSProperties } from 'react';
|
|
3
|
+
|
|
4
|
+
interface CascadeDismissDetail {
|
|
5
|
+
/** The layer node that was removed, causing this cascade dismiss. */
|
|
6
|
+
dismissedParent: HTMLElement;
|
|
7
|
+
}
|
|
8
|
+
interface Layer {
|
|
9
|
+
node: HTMLElement;
|
|
10
|
+
dismiss: (detail: CascadeDismissDetail) => void;
|
|
11
|
+
blockPointerEvents?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* The parent layer's node. Used to determine cascade-dismiss scope.
|
|
14
|
+
* Only layers whose parentNode matches the removed layer's node will be
|
|
15
|
+
* cascade-dismissed. Layers without a parentNode are top-level and never
|
|
16
|
+
* cascade-dismissed by sibling layer removal.
|
|
17
|
+
*/
|
|
18
|
+
parentNode?: HTMLElement | null;
|
|
19
|
+
}
|
|
20
|
+
interface LayerStackContextValue {
|
|
21
|
+
layers: Layer[];
|
|
22
|
+
branches: HTMLElement[];
|
|
23
|
+
recentlyRemoved: Set<HTMLElement>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Shared layer stack context. No Provider needed — all consumers share the same
|
|
27
|
+
* default value (mutable arrays/sets). This mirrors Radix's DismissibleLayerContext
|
|
28
|
+
* pattern, with the option to scope via Provider if needed.
|
|
29
|
+
*/
|
|
30
|
+
declare const LayerStackContext: react.Context<LayerStackContextValue>;
|
|
31
|
+
declare function useLayerStackContext(): LayerStackContextValue;
|
|
32
|
+
declare function isTopMost(ctx: LayerStackContextValue, node: HTMLElement): boolean;
|
|
33
|
+
declare function addBranch(ctx: LayerStackContextValue, node: HTMLElement): void;
|
|
34
|
+
declare function removeBranch(ctx: LayerStackContextValue, node: HTMLElement): void;
|
|
35
|
+
|
|
36
|
+
interface UseDismissibleLayerOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Whether the dismissible layer is active. When false, the layer is not
|
|
39
|
+
* registered in the stack and no event listeners are attached.
|
|
40
|
+
*/
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* When true, disables pointer events on elements outside this layer.
|
|
44
|
+
* Used for modal overlays (Dialog, BottomSheet).
|
|
45
|
+
*/
|
|
46
|
+
blockPointerEvents?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Called when escape key is pressed while this layer is topmost.
|
|
49
|
+
* Call `event.preventDefault()` to prevent dismiss.
|
|
50
|
+
*/
|
|
51
|
+
onEscapeKeyDown: (event: KeyboardEvent) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Called when a press occurs outside the layer.
|
|
54
|
+
* Call `event.preventDefault()` to signal that the event is handled.
|
|
55
|
+
*/
|
|
56
|
+
onPressOutside: (event: PointerEvent | TouchEvent) => void;
|
|
57
|
+
/**
|
|
58
|
+
* Called when focus moves outside the layer.
|
|
59
|
+
* Call `event.preventDefault()` to signal that the event is handled.
|
|
60
|
+
*/
|
|
61
|
+
onFocusOutside: (event: FocusEvent) => void;
|
|
62
|
+
/**
|
|
63
|
+
* Called when a parent layer is removed and this layer should close as a consequence.
|
|
64
|
+
* Unlike escape/outside/focus callbacks, this is not preventable.
|
|
65
|
+
*/
|
|
66
|
+
onCascadeDismiss: (detail: CascadeDismissDetail) => void;
|
|
67
|
+
/**
|
|
68
|
+
* Custom function to determine if a target should be treated as "inside".
|
|
69
|
+
* Useful for trigger elements that are outside the layer DOM but should
|
|
70
|
+
* not trigger dismiss (e.g., Menu trigger).
|
|
71
|
+
*/
|
|
72
|
+
exclude?: (target: HTMLElement) => boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Determines when an outside press triggers dismiss.
|
|
75
|
+
*
|
|
76
|
+
* - `"confirm"` (default): Both mouse and touch defer to click.
|
|
77
|
+
* - `"eager"`: Mouse on pointerdown, touch defers to click.
|
|
78
|
+
* - `"drag"`: Mouse on pointerdown, touch on drag (>10px immediate, >5px on touchend).
|
|
79
|
+
*/
|
|
80
|
+
pressBehavior?: "eager" | "confirm" | "drag";
|
|
81
|
+
}
|
|
82
|
+
declare function useDismissibleLayer(options: UseDismissibleLayerOptions): {
|
|
83
|
+
dismissibleRef: (el: HTMLElement | null) => void;
|
|
84
|
+
dismissibleProps: {
|
|
85
|
+
onPointerDownCapture: () => void;
|
|
86
|
+
onFocusCapture: () => void;
|
|
87
|
+
onBlurCapture: () => void;
|
|
88
|
+
style?: undefined;
|
|
89
|
+
};
|
|
90
|
+
isTopLayer: boolean;
|
|
91
|
+
/** The current layer node, for providing DismissibleParentContext to children. */
|
|
92
|
+
layerNode: HTMLElement | null;
|
|
93
|
+
} | {
|
|
94
|
+
dismissibleRef: (el: HTMLElement | null) => void;
|
|
95
|
+
dismissibleProps: {
|
|
96
|
+
onPointerDownCapture: () => void;
|
|
97
|
+
onFocusCapture: () => void;
|
|
98
|
+
onBlurCapture: () => void;
|
|
99
|
+
style: CSSProperties;
|
|
100
|
+
};
|
|
101
|
+
isTopLayer: boolean;
|
|
102
|
+
/** The current layer node, for providing DismissibleParentContext to children. */
|
|
103
|
+
layerNode: HTMLElement | null;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
interface DismissibleLayerProps extends UseDismissibleLayerOptions {
|
|
107
|
+
children: React.ReactNode;
|
|
108
|
+
}
|
|
109
|
+
declare const DismissibleLayer: react.ForwardRefExoticComponent<DismissibleLayerProps & react.RefAttributes<HTMLElement>>;
|
|
110
|
+
|
|
111
|
+
export { DismissibleLayer, LayerStackContext, addBranch, isTopMost, removeBranch, useDismissibleLayer, useLayerStackContext };
|
|
112
|
+
export type { CascadeDismissDetail, DismissibleLayerProps, Layer, LayerStackContextValue, UseDismissibleLayerOptions };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { u as useDismissibleLayer } from './useDismissibleLayer-12s-C34wvjXv.js';
|
|
2
|
+
export { D as DismissibleLayer } from './DismissibleLayer-12s-Cz1jvGI2.js';
|
|
3
|
+
export { f as LayerStackContext, h as addBranch, i as isTopMost, j as removeBranch, u as useLayerStackContext } from './layer-stack-12s-DjRVp1_u.js';
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
var react = require('react');
|
|
3
|
+
|
|
4
|
+
const LAYER_UPDATE_EVENT = "seed:dismissible-update";
|
|
5
|
+
/**
|
|
6
|
+
* Shared layer stack context. No Provider needed — all consumers share the same
|
|
7
|
+
* default value (mutable arrays/sets). This mirrors Radix's DismissibleLayerContext
|
|
8
|
+
* pattern, with the option to scope via Provider if needed.
|
|
9
|
+
*/ const LayerStackContext = react.createContext({
|
|
10
|
+
layers: [],
|
|
11
|
+
branches: [],
|
|
12
|
+
recentlyRemoved: new Set()
|
|
13
|
+
});
|
|
14
|
+
function useLayerStackContext() {
|
|
15
|
+
return react.useContext(LayerStackContext);
|
|
16
|
+
}
|
|
17
|
+
function isTopMost(ctx, node) {
|
|
18
|
+
return ctx.layers.at(-1)?.node === node;
|
|
19
|
+
}
|
|
20
|
+
function contains(parent, child) {
|
|
21
|
+
if (!parent || !child) return false;
|
|
22
|
+
if (!(child instanceof Node)) return false;
|
|
23
|
+
return parent.contains(child);
|
|
24
|
+
}
|
|
25
|
+
function isInNestedLayer(ctx, node, target) {
|
|
26
|
+
const index = ctx.layers.findIndex((l)=>l.node === node);
|
|
27
|
+
if (index === -1) return false;
|
|
28
|
+
const nested = ctx.layers.slice(index + 1);
|
|
29
|
+
if (nested.some((layer)=>contains(layer.node, target))) return true;
|
|
30
|
+
// During layer removal, treat all focus events as "inside" to prevent
|
|
31
|
+
// cascading dismissals from focus transitions (Zag pattern).
|
|
32
|
+
if (ctx.recentlyRemoved.size > 0) return true;
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
function isInBranch(ctx, target) {
|
|
36
|
+
return ctx.branches.some((branch)=>contains(branch, target));
|
|
37
|
+
}
|
|
38
|
+
function isBelowPointerBlockingLayer(ctx, node) {
|
|
39
|
+
const index = ctx.layers.findIndex((l)=>l.node === node);
|
|
40
|
+
const blockingLayers = ctx.layers.filter((l)=>l.blockPointerEvents);
|
|
41
|
+
const highestBlocking = blockingLayers.at(-1);
|
|
42
|
+
if (!highestBlocking) return false;
|
|
43
|
+
const highestBlockingIndex = ctx.layers.indexOf(highestBlocking);
|
|
44
|
+
return index < highestBlockingIndex;
|
|
45
|
+
}
|
|
46
|
+
function getPointerEventsEnabled(ctx, node) {
|
|
47
|
+
const hasBlocking = ctx.layers.some((l)=>l.blockPointerEvents);
|
|
48
|
+
if (!hasBlocking) return true;
|
|
49
|
+
const index = ctx.layers.findIndex((l)=>l.node === node);
|
|
50
|
+
const blockingLayers = ctx.layers.filter((l)=>l.blockPointerEvents);
|
|
51
|
+
const highestBlocking = blockingLayers.at(-1);
|
|
52
|
+
if (!highestBlocking) return true;
|
|
53
|
+
const highestBlockingIndex = ctx.layers.indexOf(highestBlocking);
|
|
54
|
+
return index >= highestBlockingIndex;
|
|
55
|
+
}
|
|
56
|
+
function notifyLayerChange() {
|
|
57
|
+
document.dispatchEvent(new CustomEvent(LAYER_UPDATE_EVENT));
|
|
58
|
+
}
|
|
59
|
+
function addLayer(ctx, layer) {
|
|
60
|
+
// Guard against double-registration (e.g., React strict mode re-running effects).
|
|
61
|
+
const existing = ctx.layers.findIndex((l)=>l.node === layer.node);
|
|
62
|
+
if (existing >= 0) ctx.layers.splice(existing, 1);
|
|
63
|
+
ctx.layers.push(layer);
|
|
64
|
+
notifyLayerChange();
|
|
65
|
+
}
|
|
66
|
+
function removeLayer(ctx, node) {
|
|
67
|
+
const index = ctx.layers.findIndex((l)=>l.node === node);
|
|
68
|
+
if (index < 0) return;
|
|
69
|
+
// Track recently removed to handle focus race conditions during cleanup.
|
|
70
|
+
// This prevents parent layers from incorrectly dismissing when focus
|
|
71
|
+
// moves from a closing nested layer.
|
|
72
|
+
ctx.recentlyRemoved.add(node);
|
|
73
|
+
queueMicrotask(()=>ctx.recentlyRemoved.delete(node));
|
|
74
|
+
// Cascade-dismiss only layers that declare this node as their parent.
|
|
75
|
+
// This prevents sibling layers (independent menus, dialogs, etc.) from
|
|
76
|
+
// being incorrectly dismissed when an unrelated layer closes.
|
|
77
|
+
// Transitive children are handled by their own removeLayer calls.
|
|
78
|
+
const children = ctx.layers.filter((l)=>l.parentNode === node);
|
|
79
|
+
for (const child of children){
|
|
80
|
+
child.dismiss({
|
|
81
|
+
dismissedParent: node
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Re-compute index: cascade-dismiss may have recursively removed layers,
|
|
85
|
+
// shifting the target's position in the array. In React, parents mount
|
|
86
|
+
// before children so children are always at higher indices — but this is
|
|
87
|
+
// a pure data structure with no React dependency, so we defend against
|
|
88
|
+
// arbitrary insertion order.
|
|
89
|
+
const freshIndex = ctx.layers.findIndex((l)=>l.node === node);
|
|
90
|
+
if (freshIndex >= 0) ctx.layers.splice(freshIndex, 1);
|
|
91
|
+
notifyLayerChange();
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* React context that propagates a parent dismissible layer's node down the
|
|
95
|
+
* React tree (including through portals). Nested dismissible layers read this
|
|
96
|
+
* to register as children of the parent, enabling correct cascade-dismiss
|
|
97
|
+
* behavior without affecting unrelated sibling layers.
|
|
98
|
+
*
|
|
99
|
+
* Follows the same pattern as base-ui's FloatingNodeContext.
|
|
100
|
+
*/ const DismissibleParentContext = react.createContext(null);
|
|
101
|
+
function useDismissibleParentNode() {
|
|
102
|
+
return react.useContext(DismissibleParentContext);
|
|
103
|
+
}
|
|
104
|
+
function addBranch(ctx, node) {
|
|
105
|
+
ctx.branches.push(node);
|
|
106
|
+
}
|
|
107
|
+
function removeBranch(ctx, node) {
|
|
108
|
+
const index = ctx.branches.indexOf(node);
|
|
109
|
+
if (index >= 0) ctx.branches.splice(index, 1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
exports.DismissibleParentContext = DismissibleParentContext;
|
|
113
|
+
exports.LAYER_UPDATE_EVENT = LAYER_UPDATE_EVENT;
|
|
114
|
+
exports.LayerStackContext = LayerStackContext;
|
|
115
|
+
exports.addBranch = addBranch;
|
|
116
|
+
exports.addLayer = addLayer;
|
|
117
|
+
exports.getPointerEventsEnabled = getPointerEventsEnabled;
|
|
118
|
+
exports.isBelowPointerBlockingLayer = isBelowPointerBlockingLayer;
|
|
119
|
+
exports.isInBranch = isInBranch;
|
|
120
|
+
exports.isInNestedLayer = isInNestedLayer;
|
|
121
|
+
exports.isTopMost = isTopMost;
|
|
122
|
+
exports.removeBranch = removeBranch;
|
|
123
|
+
exports.removeLayer = removeLayer;
|
|
124
|
+
exports.useDismissibleParentNode = useDismissibleParentNode;
|
|
125
|
+
exports.useLayerStackContext = useLayerStackContext;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
|
|
4
|
+
const LAYER_UPDATE_EVENT = "seed:dismissible-update";
|
|
5
|
+
/**
|
|
6
|
+
* Shared layer stack context. No Provider needed — all consumers share the same
|
|
7
|
+
* default value (mutable arrays/sets). This mirrors Radix's DismissibleLayerContext
|
|
8
|
+
* pattern, with the option to scope via Provider if needed.
|
|
9
|
+
*/ const LayerStackContext = createContext({
|
|
10
|
+
layers: [],
|
|
11
|
+
branches: [],
|
|
12
|
+
recentlyRemoved: new Set()
|
|
13
|
+
});
|
|
14
|
+
function useLayerStackContext() {
|
|
15
|
+
return useContext(LayerStackContext);
|
|
16
|
+
}
|
|
17
|
+
function isTopMost(ctx, node) {
|
|
18
|
+
return ctx.layers.at(-1)?.node === node;
|
|
19
|
+
}
|
|
20
|
+
function contains(parent, child) {
|
|
21
|
+
if (!parent || !child) return false;
|
|
22
|
+
if (!(child instanceof Node)) return false;
|
|
23
|
+
return parent.contains(child);
|
|
24
|
+
}
|
|
25
|
+
function isInNestedLayer(ctx, node, target) {
|
|
26
|
+
const index = ctx.layers.findIndex((l)=>l.node === node);
|
|
27
|
+
if (index === -1) return false;
|
|
28
|
+
const nested = ctx.layers.slice(index + 1);
|
|
29
|
+
if (nested.some((layer)=>contains(layer.node, target))) return true;
|
|
30
|
+
// During layer removal, treat all focus events as "inside" to prevent
|
|
31
|
+
// cascading dismissals from focus transitions (Zag pattern).
|
|
32
|
+
if (ctx.recentlyRemoved.size > 0) return true;
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
function isInBranch(ctx, target) {
|
|
36
|
+
return ctx.branches.some((branch)=>contains(branch, target));
|
|
37
|
+
}
|
|
38
|
+
function isBelowPointerBlockingLayer(ctx, node) {
|
|
39
|
+
const index = ctx.layers.findIndex((l)=>l.node === node);
|
|
40
|
+
const blockingLayers = ctx.layers.filter((l)=>l.blockPointerEvents);
|
|
41
|
+
const highestBlocking = blockingLayers.at(-1);
|
|
42
|
+
if (!highestBlocking) return false;
|
|
43
|
+
const highestBlockingIndex = ctx.layers.indexOf(highestBlocking);
|
|
44
|
+
return index < highestBlockingIndex;
|
|
45
|
+
}
|
|
46
|
+
function getPointerEventsEnabled(ctx, node) {
|
|
47
|
+
const hasBlocking = ctx.layers.some((l)=>l.blockPointerEvents);
|
|
48
|
+
if (!hasBlocking) return true;
|
|
49
|
+
const index = ctx.layers.findIndex((l)=>l.node === node);
|
|
50
|
+
const blockingLayers = ctx.layers.filter((l)=>l.blockPointerEvents);
|
|
51
|
+
const highestBlocking = blockingLayers.at(-1);
|
|
52
|
+
if (!highestBlocking) return true;
|
|
53
|
+
const highestBlockingIndex = ctx.layers.indexOf(highestBlocking);
|
|
54
|
+
return index >= highestBlockingIndex;
|
|
55
|
+
}
|
|
56
|
+
function notifyLayerChange() {
|
|
57
|
+
document.dispatchEvent(new CustomEvent(LAYER_UPDATE_EVENT));
|
|
58
|
+
}
|
|
59
|
+
function addLayer(ctx, layer) {
|
|
60
|
+
// Guard against double-registration (e.g., React strict mode re-running effects).
|
|
61
|
+
const existing = ctx.layers.findIndex((l)=>l.node === layer.node);
|
|
62
|
+
if (existing >= 0) ctx.layers.splice(existing, 1);
|
|
63
|
+
ctx.layers.push(layer);
|
|
64
|
+
notifyLayerChange();
|
|
65
|
+
}
|
|
66
|
+
function removeLayer(ctx, node) {
|
|
67
|
+
const index = ctx.layers.findIndex((l)=>l.node === node);
|
|
68
|
+
if (index < 0) return;
|
|
69
|
+
// Track recently removed to handle focus race conditions during cleanup.
|
|
70
|
+
// This prevents parent layers from incorrectly dismissing when focus
|
|
71
|
+
// moves from a closing nested layer.
|
|
72
|
+
ctx.recentlyRemoved.add(node);
|
|
73
|
+
queueMicrotask(()=>ctx.recentlyRemoved.delete(node));
|
|
74
|
+
// Cascade-dismiss only layers that declare this node as their parent.
|
|
75
|
+
// This prevents sibling layers (independent menus, dialogs, etc.) from
|
|
76
|
+
// being incorrectly dismissed when an unrelated layer closes.
|
|
77
|
+
// Transitive children are handled by their own removeLayer calls.
|
|
78
|
+
const children = ctx.layers.filter((l)=>l.parentNode === node);
|
|
79
|
+
for (const child of children){
|
|
80
|
+
child.dismiss({
|
|
81
|
+
dismissedParent: node
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// Re-compute index: cascade-dismiss may have recursively removed layers,
|
|
85
|
+
// shifting the target's position in the array. In React, parents mount
|
|
86
|
+
// before children so children are always at higher indices — but this is
|
|
87
|
+
// a pure data structure with no React dependency, so we defend against
|
|
88
|
+
// arbitrary insertion order.
|
|
89
|
+
const freshIndex = ctx.layers.findIndex((l)=>l.node === node);
|
|
90
|
+
if (freshIndex >= 0) ctx.layers.splice(freshIndex, 1);
|
|
91
|
+
notifyLayerChange();
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* React context that propagates a parent dismissible layer's node down the
|
|
95
|
+
* React tree (including through portals). Nested dismissible layers read this
|
|
96
|
+
* to register as children of the parent, enabling correct cascade-dismiss
|
|
97
|
+
* behavior without affecting unrelated sibling layers.
|
|
98
|
+
*
|
|
99
|
+
* Follows the same pattern as base-ui's FloatingNodeContext.
|
|
100
|
+
*/ const DismissibleParentContext = createContext(null);
|
|
101
|
+
function useDismissibleParentNode() {
|
|
102
|
+
return useContext(DismissibleParentContext);
|
|
103
|
+
}
|
|
104
|
+
function addBranch(ctx, node) {
|
|
105
|
+
ctx.branches.push(node);
|
|
106
|
+
}
|
|
107
|
+
function removeBranch(ctx, node) {
|
|
108
|
+
const index = ctx.branches.indexOf(node);
|
|
109
|
+
if (index >= 0) ctx.branches.splice(index, 1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export { DismissibleParentContext as D, LAYER_UPDATE_EVENT as L, isInBranch as a, isInNestedLayer as b, isBelowPointerBlockingLayer as c, useDismissibleParentNode as d, addLayer as e, LayerStackContext as f, getPointerEventsEnabled as g, addBranch as h, isTopMost as i, removeBranch as j, removeLayer as r, useLayerStackContext as u };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, useEffect } from 'react';
|
|
3
|
+
import { i as isTopMost } from './layer-stack-12s-DjRVp1_u.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tracks escape keydown on the document (capture phase).
|
|
7
|
+
* Only fires callback when the given node is the topmost layer.
|
|
8
|
+
*/ function useEscapeKeydown(node, ctx, onEscapeKeyDown) {
|
|
9
|
+
const callbackRef = useRef(onEscapeKeyDown);
|
|
10
|
+
callbackRef.current = onEscapeKeyDown;
|
|
11
|
+
useEffect(()=>{
|
|
12
|
+
if (!node) return;
|
|
13
|
+
const handler = (event)=>{
|
|
14
|
+
if (event.key !== "Escape") return;
|
|
15
|
+
if (event.isComposing) return;
|
|
16
|
+
if (!isTopMost(ctx, node)) return;
|
|
17
|
+
callbackRef.current(event);
|
|
18
|
+
};
|
|
19
|
+
document.addEventListener("keydown", handler, {
|
|
20
|
+
capture: true
|
|
21
|
+
});
|
|
22
|
+
return ()=>document.removeEventListener("keydown", handler, {
|
|
23
|
+
capture: true
|
|
24
|
+
});
|
|
25
|
+
}, [
|
|
26
|
+
node,
|
|
27
|
+
ctx
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { useEscapeKeydown as u };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
var react = require('react');
|
|
3
|
+
var layerStack12s = require('./layer-stack-12s-Ctg3ipjW.cjs');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tracks escape keydown on the document (capture phase).
|
|
7
|
+
* Only fires callback when the given node is the topmost layer.
|
|
8
|
+
*/ function useEscapeKeydown(node, ctx, onEscapeKeyDown) {
|
|
9
|
+
const callbackRef = react.useRef(onEscapeKeyDown);
|
|
10
|
+
callbackRef.current = onEscapeKeyDown;
|
|
11
|
+
react.useEffect(()=>{
|
|
12
|
+
if (!node) return;
|
|
13
|
+
const handler = (event)=>{
|
|
14
|
+
if (event.key !== "Escape") return;
|
|
15
|
+
if (event.isComposing) return;
|
|
16
|
+
if (!layerStack12s.isTopMost(ctx, node)) return;
|
|
17
|
+
callbackRef.current(event);
|
|
18
|
+
};
|
|
19
|
+
document.addEventListener("keydown", handler, {
|
|
20
|
+
capture: true
|
|
21
|
+
});
|
|
22
|
+
return ()=>document.removeEventListener("keydown", handler, {
|
|
23
|
+
capture: true
|
|
24
|
+
});
|
|
25
|
+
}, [
|
|
26
|
+
node,
|
|
27
|
+
ctx
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
exports.useEscapeKeydown = useEscapeKeydown;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
var react = require('react');
|
|
3
|
+
var layerStack12s = require('./layer-stack-12s-Ctg3ipjW.cjs');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detects focus events outside a React subtree.
|
|
7
|
+
* Uses `onFocusCapture`/`onBlurCapture` for React-tree-based detection.
|
|
8
|
+
*
|
|
9
|
+
* Ported from Radix's useFocusOutside with layer stack integration.
|
|
10
|
+
*/ function useFocusOutside(node, ctx, options) {
|
|
11
|
+
const callbackRef = react.useRef(options.onFocusOutside);
|
|
12
|
+
callbackRef.current = options.onFocusOutside;
|
|
13
|
+
const excludeRef = react.useRef(options.exclude);
|
|
14
|
+
excludeRef.current = options.exclude;
|
|
15
|
+
const isFocusInsideReactTreeRef = react.useRef(false);
|
|
16
|
+
react.useEffect(()=>{
|
|
17
|
+
if (!node || !options.enabled) return;
|
|
18
|
+
const ownerDocument = node.ownerDocument ?? document;
|
|
19
|
+
const handleFocus = (event)=>{
|
|
20
|
+
const target = event.target;
|
|
21
|
+
if (target && !isFocusInsideReactTreeRef.current) {
|
|
22
|
+
if (layerStack12s.isInBranch(ctx, target)) return;
|
|
23
|
+
if (layerStack12s.isInNestedLayer(ctx, node, target)) return;
|
|
24
|
+
if (target instanceof HTMLElement && excludeRef.current?.(target)) return;
|
|
25
|
+
callbackRef.current(event);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
ownerDocument.addEventListener("focusin", handleFocus);
|
|
29
|
+
return ()=>{
|
|
30
|
+
ownerDocument.removeEventListener("focusin", handleFocus);
|
|
31
|
+
isFocusInsideReactTreeRef.current = false;
|
|
32
|
+
};
|
|
33
|
+
}, [
|
|
34
|
+
node,
|
|
35
|
+
ctx,
|
|
36
|
+
options.enabled
|
|
37
|
+
]);
|
|
38
|
+
return {
|
|
39
|
+
onFocusCapture: ()=>{
|
|
40
|
+
isFocusInsideReactTreeRef.current = true;
|
|
41
|
+
},
|
|
42
|
+
onBlurCapture: ()=>{
|
|
43
|
+
isFocusInsideReactTreeRef.current = false;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
exports.useFocusOutside = useFocusOutside;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, useEffect } from 'react';
|
|
3
|
+
import { a as isInBranch, b as isInNestedLayer } from './layer-stack-12s-DjRVp1_u.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detects focus events outside a React subtree.
|
|
7
|
+
* Uses `onFocusCapture`/`onBlurCapture` for React-tree-based detection.
|
|
8
|
+
*
|
|
9
|
+
* Ported from Radix's useFocusOutside with layer stack integration.
|
|
10
|
+
*/ function useFocusOutside(node, ctx, options) {
|
|
11
|
+
const callbackRef = useRef(options.onFocusOutside);
|
|
12
|
+
callbackRef.current = options.onFocusOutside;
|
|
13
|
+
const excludeRef = useRef(options.exclude);
|
|
14
|
+
excludeRef.current = options.exclude;
|
|
15
|
+
const isFocusInsideReactTreeRef = useRef(false);
|
|
16
|
+
useEffect(()=>{
|
|
17
|
+
if (!node || !options.enabled) return;
|
|
18
|
+
const ownerDocument = node.ownerDocument ?? document;
|
|
19
|
+
const handleFocus = (event)=>{
|
|
20
|
+
const target = event.target;
|
|
21
|
+
if (target && !isFocusInsideReactTreeRef.current) {
|
|
22
|
+
if (isInBranch(ctx, target)) return;
|
|
23
|
+
if (isInNestedLayer(ctx, node, target)) return;
|
|
24
|
+
if (target instanceof HTMLElement && excludeRef.current?.(target)) return;
|
|
25
|
+
callbackRef.current(event);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
ownerDocument.addEventListener("focusin", handleFocus);
|
|
29
|
+
return ()=>{
|
|
30
|
+
ownerDocument.removeEventListener("focusin", handleFocus);
|
|
31
|
+
isFocusInsideReactTreeRef.current = false;
|
|
32
|
+
};
|
|
33
|
+
}, [
|
|
34
|
+
node,
|
|
35
|
+
ctx,
|
|
36
|
+
options.enabled
|
|
37
|
+
]);
|
|
38
|
+
return {
|
|
39
|
+
onFocusCapture: ()=>{
|
|
40
|
+
isFocusInsideReactTreeRef.current = true;
|
|
41
|
+
},
|
|
42
|
+
onBlurCapture: ()=>{
|
|
43
|
+
isFocusInsideReactTreeRef.current = false;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { useFocusOutside as u };
|