@seed-design/react-dismissible-layer 0.0.0-alpha-20260511052324 → 1.0.0
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 → DismissibleLayer-12s-BtXreIO0.js} +2 -2
- package/lib/{DismissibleLayer-12s-ZXi0vrlt.cjs → DismissibleLayer-12s-ClcF8WmX.cjs} +2 -2
- package/lib/index.cjs +3 -3
- package/lib/index.d.ts +0 -19
- package/lib/index.js +3 -3
- package/lib/{layer-stack-12s-DjRVp1_u.js → layer-stack-12s-BYEm6Mii.js} +1 -19
- package/lib/{layer-stack-12s-Ctg3ipjW.cjs → layer-stack-12s-VHbg4L96.cjs} +0 -20
- package/lib/{use-escape-keydown-12s-Bl_her-_.js → use-escape-keydown-12s-DHQq12u5.js} +1 -1
- package/lib/{use-escape-keydown-12s-CNZqs1dI.cjs → use-escape-keydown-12s-XPuBxyd9.cjs} +1 -1
- package/lib/{use-focus-outside-12s-LpFt7UjO.js → use-focus-outside-12s--rnlvSkk.js} +1 -1
- package/lib/{use-focus-outside-12s-Ch-SoiiA.cjs → use-focus-outside-12s-C_884sbw.cjs} +1 -1
- package/lib/{use-pointer-down-outside-12s-nlxC-DrD.cjs → use-pointer-down-outside-12s-Bj7Jt4l5.cjs} +1 -2
- package/lib/{use-pointer-down-outside-12s-BLctGw0m.js → use-pointer-down-outside-12s-Bk1SE4lm.js} +1 -2
- package/lib/{useDismissibleLayer-12s-D4qWttkW.cjs → useDismissibleLayer-12s-Bt4Fi-Z1.cjs} +7 -48
- package/lib/{useDismissibleLayer-12s-C34wvjXv.js → useDismissibleLayer-12s-sWGJcPBy.js} +7 -48
- package/package.json +1 -1
- package/src/layer-stack.test.ts +3 -36
- package/src/layer-stack.ts +0 -27
- package/src/use-pointer-down-outside.ts +1 -8
- package/src/useDismissibleLayer.test.tsx +0 -98
- package/src/useDismissibleLayer.ts +3 -53
|
@@ -3,8 +3,8 @@ import { jsx } from 'react/jsx-runtime';
|
|
|
3
3
|
import { composeRefs } from '@radix-ui/react-compose-refs';
|
|
4
4
|
import { Slot } from '@radix-ui/react-slot';
|
|
5
5
|
import { forwardRef } from 'react';
|
|
6
|
-
import { u as useDismissibleLayer } from './useDismissibleLayer-12s-
|
|
7
|
-
import { D as DismissibleParentContext } from './layer-stack-12s-
|
|
6
|
+
import { u as useDismissibleLayer } from './useDismissibleLayer-12s-sWGJcPBy.js';
|
|
7
|
+
import { D as DismissibleParentContext } from './layer-stack-12s-BYEm6Mii.js';
|
|
8
8
|
|
|
9
9
|
const DismissibleLayer = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
10
10
|
const { children, ...options } = props;
|
|
@@ -3,8 +3,8 @@ var jsxRuntime = require('react/jsx-runtime');
|
|
|
3
3
|
var reactComposeRefs = require('@radix-ui/react-compose-refs');
|
|
4
4
|
var reactSlot = require('@radix-ui/react-slot');
|
|
5
5
|
var react = require('react');
|
|
6
|
-
var useDismissibleLayer12s = require('./useDismissibleLayer-12s-
|
|
7
|
-
var layerStack12s = require('./layer-stack-12s-
|
|
6
|
+
var useDismissibleLayer12s = require('./useDismissibleLayer-12s-Bt4Fi-Z1.cjs');
|
|
7
|
+
var layerStack12s = require('./layer-stack-12s-VHbg4L96.cjs');
|
|
8
8
|
|
|
9
9
|
const DismissibleLayer = /*#__PURE__*/ react.forwardRef((props, ref)=>{
|
|
10
10
|
const { children, ...options } = props;
|
package/lib/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
var useDismissibleLayer12s = require('./useDismissibleLayer-12s-
|
|
2
|
-
var DismissibleLayer12s = require('./DismissibleLayer-12s-
|
|
3
|
-
var layerStack12s = require('./layer-stack-12s-
|
|
1
|
+
var useDismissibleLayer12s = require('./useDismissibleLayer-12s-Bt4Fi-Z1.cjs');
|
|
2
|
+
var DismissibleLayer12s = require('./DismissibleLayer-12s-ClcF8WmX.cjs');
|
|
3
|
+
var layerStack12s = require('./layer-stack-12s-VHbg4L96.cjs');
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
package/lib/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { CSSProperties } from 'react';
|
|
3
2
|
|
|
4
3
|
interface CascadeDismissDetail {
|
|
5
4
|
/** The layer node that was removed, causing this cascade dismiss. */
|
|
@@ -8,7 +7,6 @@ interface CascadeDismissDetail {
|
|
|
8
7
|
interface Layer {
|
|
9
8
|
node: HTMLElement;
|
|
10
9
|
dismiss: (detail: CascadeDismissDetail) => void;
|
|
11
|
-
blockPointerEvents?: boolean;
|
|
12
10
|
/**
|
|
13
11
|
* The parent layer's node. Used to determine cascade-dismiss scope.
|
|
14
12
|
* Only layers whose parentNode matches the removed layer's node will be
|
|
@@ -39,11 +37,6 @@ interface UseDismissibleLayerOptions {
|
|
|
39
37
|
* registered in the stack and no event listeners are attached.
|
|
40
38
|
*/
|
|
41
39
|
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
40
|
/**
|
|
48
41
|
* Called when escape key is pressed while this layer is topmost.
|
|
49
42
|
* Call `event.preventDefault()` to prevent dismiss.
|
|
@@ -85,18 +78,6 @@ declare function useDismissibleLayer(options: UseDismissibleLayerOptions): {
|
|
|
85
78
|
onPointerDownCapture: () => void;
|
|
86
79
|
onFocusCapture: () => void;
|
|
87
80
|
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
81
|
};
|
|
101
82
|
isTopLayer: boolean;
|
|
102
83
|
/** The current layer node, for providing DismissibleParentContext to children. */
|
package/lib/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { u as useDismissibleLayer } from './useDismissibleLayer-12s-
|
|
2
|
-
export { D as DismissibleLayer } from './DismissibleLayer-12s-
|
|
3
|
-
export {
|
|
1
|
+
export { u as useDismissibleLayer } from './useDismissibleLayer-12s-sWGJcPBy.js';
|
|
2
|
+
export { D as DismissibleLayer } from './DismissibleLayer-12s-BtXreIO0.js';
|
|
3
|
+
export { e as LayerStackContext, f as addBranch, i as isTopMost, g as removeBranch, u as useLayerStackContext } from './layer-stack-12s-BYEm6Mii.js';
|
|
@@ -35,24 +35,6 @@ function isInNestedLayer(ctx, node, target) {
|
|
|
35
35
|
function isInBranch(ctx, target) {
|
|
36
36
|
return ctx.branches.some((branch)=>contains(branch, target));
|
|
37
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
38
|
function notifyLayerChange() {
|
|
57
39
|
document.dispatchEvent(new CustomEvent(LAYER_UPDATE_EVENT));
|
|
58
40
|
}
|
|
@@ -109,4 +91,4 @@ function removeBranch(ctx, node) {
|
|
|
109
91
|
if (index >= 0) ctx.branches.splice(index, 1);
|
|
110
92
|
}
|
|
111
93
|
|
|
112
|
-
export { DismissibleParentContext as D, LAYER_UPDATE_EVENT as L, isInBranch as a, isInNestedLayer as b,
|
|
94
|
+
export { DismissibleParentContext as D, LAYER_UPDATE_EVENT as L, isInBranch as a, isInNestedLayer as b, useDismissibleParentNode as c, addLayer as d, LayerStackContext as e, addBranch as f, removeBranch as g, isTopMost as i, removeLayer as r, useLayerStackContext as u };
|
|
@@ -35,24 +35,6 @@ function isInNestedLayer(ctx, node, target) {
|
|
|
35
35
|
function isInBranch(ctx, target) {
|
|
36
36
|
return ctx.branches.some((branch)=>contains(branch, target));
|
|
37
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
38
|
function notifyLayerChange() {
|
|
57
39
|
document.dispatchEvent(new CustomEvent(LAYER_UPDATE_EVENT));
|
|
58
40
|
}
|
|
@@ -114,8 +96,6 @@ exports.LAYER_UPDATE_EVENT = LAYER_UPDATE_EVENT;
|
|
|
114
96
|
exports.LayerStackContext = LayerStackContext;
|
|
115
97
|
exports.addBranch = addBranch;
|
|
116
98
|
exports.addLayer = addLayer;
|
|
117
|
-
exports.getPointerEventsEnabled = getPointerEventsEnabled;
|
|
118
|
-
exports.isBelowPointerBlockingLayer = isBelowPointerBlockingLayer;
|
|
119
99
|
exports.isInBranch = isInBranch;
|
|
120
100
|
exports.isInNestedLayer = isInNestedLayer;
|
|
121
101
|
exports.isTopMost = isTopMost;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useRef, useEffect } from 'react';
|
|
3
|
-
import { a as isInBranch, b as isInNestedLayer } from './layer-stack-12s-
|
|
3
|
+
import { a as isInBranch, b as isInNestedLayer } from './layer-stack-12s-BYEm6Mii.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Detects focus events outside a React subtree.
|
package/lib/{use-pointer-down-outside-12s-nlxC-DrD.cjs → use-pointer-down-outside-12s-Bj7Jt4l5.cjs}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
var react = require('react');
|
|
3
|
-
var layerStack12s = require('./layer-stack-12s-
|
|
3
|
+
var layerStack12s = require('./layer-stack-12s-VHbg4L96.cjs');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Detects pointer-down events outside a React subtree.
|
|
@@ -26,7 +26,6 @@ var layerStack12s = require('./layer-stack-12s-Ctg3ipjW.cjs');
|
|
|
26
26
|
const ownerDocument = node.ownerDocument ?? document;
|
|
27
27
|
function isOutsideLayer(target) {
|
|
28
28
|
if (!layerStack12s.isTopMost(ctx, node)) return false;
|
|
29
|
-
if (layerStack12s.isBelowPointerBlockingLayer(ctx, node)) return false;
|
|
30
29
|
if (layerStack12s.isInBranch(ctx, target)) return false;
|
|
31
30
|
if (layerStack12s.isInNestedLayer(ctx, node, target)) return false;
|
|
32
31
|
return true;
|
package/lib/{use-pointer-down-outside-12s-BLctGw0m.js → use-pointer-down-outside-12s-Bk1SE4lm.js}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useRef, useEffect } from 'react';
|
|
3
|
-
import { i as isTopMost,
|
|
3
|
+
import { i as isTopMost, a as isInBranch, b as isInNestedLayer } from './layer-stack-12s-BYEm6Mii.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Detects pointer-down events outside a React subtree.
|
|
@@ -26,7 +26,6 @@ import { i as isTopMost, c as isBelowPointerBlockingLayer, a as isInBranch, b as
|
|
|
26
26
|
const ownerDocument = node.ownerDocument ?? document;
|
|
27
27
|
function isOutsideLayer(target) {
|
|
28
28
|
if (!isTopMost(ctx, node)) return false;
|
|
29
|
-
if (isBelowPointerBlockingLayer(ctx, node)) return false;
|
|
30
29
|
if (isInBranch(ctx, target)) return false;
|
|
31
30
|
if (isInNestedLayer(ctx, node, target)) return false;
|
|
32
31
|
return true;
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
var react = require('react');
|
|
3
|
-
var layerStack12s = require('./layer-stack-12s-
|
|
4
|
-
var useEscapeKeydown12s = require('./use-escape-keydown-12s-
|
|
5
|
-
var usePointerDownOutside12s = require('./use-pointer-down-outside-12s-
|
|
6
|
-
var useFocusOutside12s = require('./use-focus-outside-12s-
|
|
3
|
+
var layerStack12s = require('./layer-stack-12s-VHbg4L96.cjs');
|
|
4
|
+
var useEscapeKeydown12s = require('./use-escape-keydown-12s-XPuBxyd9.cjs');
|
|
5
|
+
var usePointerDownOutside12s = require('./use-pointer-down-outside-12s-Bj7Jt4l5.cjs');
|
|
6
|
+
var useFocusOutside12s = require('./use-focus-outside-12s-C_884sbw.cjs');
|
|
7
7
|
|
|
8
8
|
const NOOP = ()=>{};
|
|
9
|
-
// Module-level storage for the original body pointer-events value.
|
|
10
|
-
// Follows Radix's pattern: save once when the first blocking layer mounts,
|
|
11
|
-
// restore when the last blocking layer unmounts. This decouples save/restore
|
|
12
|
-
// from React's bottom-up cleanup order.
|
|
13
|
-
let originalBodyPointerEvents;
|
|
14
9
|
function useDismissibleLayer(options) {
|
|
15
|
-
const { enabled,
|
|
10
|
+
const { enabled, onEscapeKeyDown, onPressOutside, onFocusOutside, onCascadeDismiss, exclude, pressBehavior } = options;
|
|
16
11
|
const ctx = layerStack12s.useLayerStackContext();
|
|
17
12
|
const parentNode = layerStack12s.useDismissibleParentNode();
|
|
18
13
|
const [node, setNode] = react.useState(null);
|
|
@@ -33,7 +28,6 @@ function useDismissibleLayer(options) {
|
|
|
33
28
|
const layer = {
|
|
34
29
|
node,
|
|
35
30
|
dismiss: (detail)=>onCascadeDismissRef.current?.(detail),
|
|
36
|
-
blockPointerEvents,
|
|
37
31
|
parentNode
|
|
38
32
|
};
|
|
39
33
|
layerStack12s.addLayer(ctx, layer);
|
|
@@ -43,38 +37,10 @@ function useDismissibleLayer(options) {
|
|
|
43
37
|
}, [
|
|
44
38
|
node,
|
|
45
39
|
enabled,
|
|
46
|
-
blockPointerEvents,
|
|
47
40
|
ctx,
|
|
48
41
|
parentNode
|
|
49
42
|
]);
|
|
50
|
-
// --
|
|
51
|
-
// Follows Radix's pattern: save original value once when the first blocking
|
|
52
|
-
// layer mounts, restore when the last unmounts. The module-level variable
|
|
53
|
-
// decouples save/restore from React's bottom-up cleanup order.
|
|
54
|
-
react.useEffect(()=>{
|
|
55
|
-
if (!node || !enabled || !blockPointerEvents) return;
|
|
56
|
-
const ownerDocument = node.ownerDocument ?? document;
|
|
57
|
-
const blockingLayers = ctx.layers.filter((l)=>l.blockPointerEvents);
|
|
58
|
-
if (blockingLayers.length === 1) {
|
|
59
|
-
// First blocking layer — save the original value and block
|
|
60
|
-
originalBodyPointerEvents = ownerDocument.body.style.pointerEvents;
|
|
61
|
-
ownerDocument.body.style.pointerEvents = "none";
|
|
62
|
-
}
|
|
63
|
-
return ()=>{
|
|
64
|
-
// Check count BEFORE this layer is removed from ctx.layers.
|
|
65
|
-
// At cleanup time, this layer is still in the array.
|
|
66
|
-
const remainingBlocking = ctx.layers.filter((l)=>l.blockPointerEvents && l.node !== node);
|
|
67
|
-
if (remainingBlocking.length === 0) {
|
|
68
|
-
ownerDocument.body.style.pointerEvents = originalBodyPointerEvents;
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
}, [
|
|
72
|
-
node,
|
|
73
|
-
enabled,
|
|
74
|
-
blockPointerEvents,
|
|
75
|
-
ctx
|
|
76
|
-
]);
|
|
77
|
-
// -- Subscribe to layer changes for style updates --
|
|
43
|
+
// -- Subscribe to layer changes to keep isTopLayer fresh --
|
|
78
44
|
react.useEffect(()=>{
|
|
79
45
|
if (!enabled) return;
|
|
80
46
|
const handler = ()=>forceRender({});
|
|
@@ -111,12 +77,6 @@ function useDismissibleLayer(options) {
|
|
|
111
77
|
exclude,
|
|
112
78
|
onFocusOutside: handleFocusOutside
|
|
113
79
|
});
|
|
114
|
-
// -- Compute pointer-events style --
|
|
115
|
-
const hasBlocking = ctx.layers.some((l)=>l.blockPointerEvents);
|
|
116
|
-
const pointerEventsEnabled = node ? layerStack12s.getPointerEventsEnabled(ctx, node) : true;
|
|
117
|
-
const style = hasBlocking ? {
|
|
118
|
-
pointerEvents: pointerEventsEnabled ? "auto" : "none"
|
|
119
|
-
} : {};
|
|
120
80
|
const topLayer = node ? layerStack12s.isTopMost(ctx, node) : true;
|
|
121
81
|
if (!enabled) {
|
|
122
82
|
return {
|
|
@@ -135,8 +95,7 @@ function useDismissibleLayer(options) {
|
|
|
135
95
|
dismissibleProps: {
|
|
136
96
|
onPointerDownCapture: pressOutsideProps.onPointerDownCapture,
|
|
137
97
|
onFocusCapture: focusOutsideProps.onFocusCapture,
|
|
138
|
-
onBlurCapture: focusOutsideProps.onBlurCapture
|
|
139
|
-
style
|
|
98
|
+
onBlurCapture: focusOutsideProps.onBlurCapture
|
|
140
99
|
},
|
|
141
100
|
isTopLayer: topLayer,
|
|
142
101
|
/** The current layer node, for providing DismissibleParentContext to children. */ layerNode: node
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
3
|
-
import { u as useLayerStackContext,
|
|
4
|
-
import { u as useEscapeKeydown } from './use-escape-keydown-12s-
|
|
5
|
-
import { u as usePointerDownOutside } from './use-pointer-down-outside-12s-
|
|
6
|
-
import { u as useFocusOutside } from './use-focus-outside-12s
|
|
3
|
+
import { u as useLayerStackContext, c as useDismissibleParentNode, d as addLayer, r as removeLayer, L as LAYER_UPDATE_EVENT, i as isTopMost } from './layer-stack-12s-BYEm6Mii.js';
|
|
4
|
+
import { u as useEscapeKeydown } from './use-escape-keydown-12s-DHQq12u5.js';
|
|
5
|
+
import { u as usePointerDownOutside } from './use-pointer-down-outside-12s-Bk1SE4lm.js';
|
|
6
|
+
import { u as useFocusOutside } from './use-focus-outside-12s--rnlvSkk.js';
|
|
7
7
|
|
|
8
8
|
const NOOP = ()=>{};
|
|
9
|
-
// Module-level storage for the original body pointer-events value.
|
|
10
|
-
// Follows Radix's pattern: save once when the first blocking layer mounts,
|
|
11
|
-
// restore when the last blocking layer unmounts. This decouples save/restore
|
|
12
|
-
// from React's bottom-up cleanup order.
|
|
13
|
-
let originalBodyPointerEvents;
|
|
14
9
|
function useDismissibleLayer(options) {
|
|
15
|
-
const { enabled,
|
|
10
|
+
const { enabled, onEscapeKeyDown, onPressOutside, onFocusOutside, onCascadeDismiss, exclude, pressBehavior } = options;
|
|
16
11
|
const ctx = useLayerStackContext();
|
|
17
12
|
const parentNode = useDismissibleParentNode();
|
|
18
13
|
const [node, setNode] = useState(null);
|
|
@@ -33,7 +28,6 @@ function useDismissibleLayer(options) {
|
|
|
33
28
|
const layer = {
|
|
34
29
|
node,
|
|
35
30
|
dismiss: (detail)=>onCascadeDismissRef.current?.(detail),
|
|
36
|
-
blockPointerEvents,
|
|
37
31
|
parentNode
|
|
38
32
|
};
|
|
39
33
|
addLayer(ctx, layer);
|
|
@@ -43,38 +37,10 @@ function useDismissibleLayer(options) {
|
|
|
43
37
|
}, [
|
|
44
38
|
node,
|
|
45
39
|
enabled,
|
|
46
|
-
blockPointerEvents,
|
|
47
40
|
ctx,
|
|
48
41
|
parentNode
|
|
49
42
|
]);
|
|
50
|
-
// --
|
|
51
|
-
// Follows Radix's pattern: save original value once when the first blocking
|
|
52
|
-
// layer mounts, restore when the last unmounts. The module-level variable
|
|
53
|
-
// decouples save/restore from React's bottom-up cleanup order.
|
|
54
|
-
useEffect(()=>{
|
|
55
|
-
if (!node || !enabled || !blockPointerEvents) return;
|
|
56
|
-
const ownerDocument = node.ownerDocument ?? document;
|
|
57
|
-
const blockingLayers = ctx.layers.filter((l)=>l.blockPointerEvents);
|
|
58
|
-
if (blockingLayers.length === 1) {
|
|
59
|
-
// First blocking layer — save the original value and block
|
|
60
|
-
originalBodyPointerEvents = ownerDocument.body.style.pointerEvents;
|
|
61
|
-
ownerDocument.body.style.pointerEvents = "none";
|
|
62
|
-
}
|
|
63
|
-
return ()=>{
|
|
64
|
-
// Check count BEFORE this layer is removed from ctx.layers.
|
|
65
|
-
// At cleanup time, this layer is still in the array.
|
|
66
|
-
const remainingBlocking = ctx.layers.filter((l)=>l.blockPointerEvents && l.node !== node);
|
|
67
|
-
if (remainingBlocking.length === 0) {
|
|
68
|
-
ownerDocument.body.style.pointerEvents = originalBodyPointerEvents;
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
}, [
|
|
72
|
-
node,
|
|
73
|
-
enabled,
|
|
74
|
-
blockPointerEvents,
|
|
75
|
-
ctx
|
|
76
|
-
]);
|
|
77
|
-
// -- Subscribe to layer changes for style updates --
|
|
43
|
+
// -- Subscribe to layer changes to keep isTopLayer fresh --
|
|
78
44
|
useEffect(()=>{
|
|
79
45
|
if (!enabled) return;
|
|
80
46
|
const handler = ()=>forceRender({});
|
|
@@ -111,12 +77,6 @@ function useDismissibleLayer(options) {
|
|
|
111
77
|
exclude,
|
|
112
78
|
onFocusOutside: handleFocusOutside
|
|
113
79
|
});
|
|
114
|
-
// -- Compute pointer-events style --
|
|
115
|
-
const hasBlocking = ctx.layers.some((l)=>l.blockPointerEvents);
|
|
116
|
-
const pointerEventsEnabled = node ? getPointerEventsEnabled(ctx, node) : true;
|
|
117
|
-
const style = hasBlocking ? {
|
|
118
|
-
pointerEvents: pointerEventsEnabled ? "auto" : "none"
|
|
119
|
-
} : {};
|
|
120
80
|
const topLayer = node ? isTopMost(ctx, node) : true;
|
|
121
81
|
if (!enabled) {
|
|
122
82
|
return {
|
|
@@ -135,8 +95,7 @@ function useDismissibleLayer(options) {
|
|
|
135
95
|
dismissibleProps: {
|
|
136
96
|
onPointerDownCapture: pressOutsideProps.onPointerDownCapture,
|
|
137
97
|
onFocusCapture: focusOutsideProps.onFocusCapture,
|
|
138
|
-
onBlurCapture: focusOutsideProps.onBlurCapture
|
|
139
|
-
style
|
|
98
|
+
onBlurCapture: focusOutsideProps.onBlurCapture
|
|
140
99
|
},
|
|
141
100
|
isTopLayer: topLayer,
|
|
142
101
|
/** The current layer node, for providing DismissibleParentContext to children. */ layerNode: node
|
package/package.json
CHANGED
package/src/layer-stack.test.ts
CHANGED
|
@@ -7,8 +7,6 @@ import {
|
|
|
7
7
|
isTopMost,
|
|
8
8
|
isInNestedLayer,
|
|
9
9
|
isInBranch,
|
|
10
|
-
isBelowPointerBlockingLayer,
|
|
11
|
-
getPointerEventsEnabled,
|
|
12
10
|
type LayerStackContextValue,
|
|
13
11
|
type Layer,
|
|
14
12
|
} from "./layer-stack";
|
|
@@ -58,12 +56,12 @@ describe("layer-stack", () => {
|
|
|
58
56
|
|
|
59
57
|
it("replaces existing layer when same node is added twice", () => {
|
|
60
58
|
const node = createNode();
|
|
61
|
-
const first = createLayer(node
|
|
62
|
-
const second = createLayer(node
|
|
59
|
+
const first = createLayer(node);
|
|
60
|
+
const second = createLayer(node);
|
|
63
61
|
addLayer(ctx, first);
|
|
64
62
|
addLayer(ctx, second);
|
|
65
63
|
expect(ctx.layers).toHaveLength(1);
|
|
66
|
-
expect(ctx.layers[0].
|
|
64
|
+
expect(ctx.layers[0].dismiss).toBe(second.dismiss);
|
|
67
65
|
});
|
|
68
66
|
});
|
|
69
67
|
|
|
@@ -257,35 +255,4 @@ describe("layer-stack", () => {
|
|
|
257
255
|
expect(ctx.branches).toHaveLength(0);
|
|
258
256
|
});
|
|
259
257
|
});
|
|
260
|
-
describe("pointer blocking", () => {
|
|
261
|
-
it("isBelowPointerBlockingLayer returns false when no blocking layers", () => {
|
|
262
|
-
const node = createNode();
|
|
263
|
-
addLayer(ctx, createLayer(node));
|
|
264
|
-
expect(isBelowPointerBlockingLayer(ctx, node)).toBe(false);
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it("isBelowPointerBlockingLayer returns true when below a blocking layer", () => {
|
|
268
|
-
const dialog = createNode("dialog");
|
|
269
|
-
const menu = createNode("menu");
|
|
270
|
-
addLayer(ctx, createLayer(dialog, { blockPointerEvents: true }));
|
|
271
|
-
addLayer(ctx, createLayer(menu, { blockPointerEvents: true }));
|
|
272
|
-
expect(isBelowPointerBlockingLayer(ctx, dialog)).toBe(true);
|
|
273
|
-
expect(isBelowPointerBlockingLayer(ctx, menu)).toBe(false);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it("getPointerEventsEnabled returns true when no blocking layers", () => {
|
|
277
|
-
const node = createNode();
|
|
278
|
-
addLayer(ctx, createLayer(node));
|
|
279
|
-
expect(getPointerEventsEnabled(ctx, node)).toBe(true);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it("getPointerEventsEnabled returns false for layers below the highest blocking layer", () => {
|
|
283
|
-
const dialog = createNode("dialog");
|
|
284
|
-
const menu = createNode("menu");
|
|
285
|
-
addLayer(ctx, createLayer(dialog, { blockPointerEvents: true }));
|
|
286
|
-
addLayer(ctx, createLayer(menu, { blockPointerEvents: true }));
|
|
287
|
-
expect(getPointerEventsEnabled(ctx, dialog)).toBe(false);
|
|
288
|
-
expect(getPointerEventsEnabled(ctx, menu)).toBe(true);
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
258
|
});
|
package/src/layer-stack.ts
CHANGED
|
@@ -10,7 +10,6 @@ export interface CascadeDismissDetail {
|
|
|
10
10
|
export interface Layer {
|
|
11
11
|
node: HTMLElement;
|
|
12
12
|
dismiss: (detail: CascadeDismissDetail) => void;
|
|
13
|
-
blockPointerEvents?: boolean;
|
|
14
13
|
/**
|
|
15
14
|
* The parent layer's node. Used to determine cascade-dismiss scope.
|
|
16
15
|
* Only layers whose parentNode matches the removed layer's node will be
|
|
@@ -75,32 +74,6 @@ export function isInBranch(ctx: LayerStackContextValue, target: EventTarget | nu
|
|
|
75
74
|
return ctx.branches.some((branch) => contains(branch, target));
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
export function isBelowPointerBlockingLayer(
|
|
79
|
-
ctx: LayerStackContextValue,
|
|
80
|
-
node: HTMLElement,
|
|
81
|
-
): boolean {
|
|
82
|
-
const index = ctx.layers.findIndex((l) => l.node === node);
|
|
83
|
-
const blockingLayers = ctx.layers.filter((l) => l.blockPointerEvents);
|
|
84
|
-
const highestBlocking = blockingLayers.at(-1);
|
|
85
|
-
if (!highestBlocking) return false;
|
|
86
|
-
|
|
87
|
-
const highestBlockingIndex = ctx.layers.indexOf(highestBlocking);
|
|
88
|
-
return index < highestBlockingIndex;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function getPointerEventsEnabled(ctx: LayerStackContextValue, node: HTMLElement): boolean {
|
|
92
|
-
const hasBlocking = ctx.layers.some((l) => l.blockPointerEvents);
|
|
93
|
-
if (!hasBlocking) return true;
|
|
94
|
-
|
|
95
|
-
const index = ctx.layers.findIndex((l) => l.node === node);
|
|
96
|
-
const blockingLayers = ctx.layers.filter((l) => l.blockPointerEvents);
|
|
97
|
-
const highestBlocking = blockingLayers.at(-1);
|
|
98
|
-
if (!highestBlocking) return true;
|
|
99
|
-
|
|
100
|
-
const highestBlockingIndex = ctx.layers.indexOf(highestBlocking);
|
|
101
|
-
return index >= highestBlockingIndex;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
77
|
export function notifyLayerChange() {
|
|
105
78
|
document.dispatchEvent(new CustomEvent(LAYER_UPDATE_EVENT));
|
|
106
79
|
}
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef } from "react";
|
|
4
|
-
import {
|
|
5
|
-
isBelowPointerBlockingLayer,
|
|
6
|
-
isInBranch,
|
|
7
|
-
isInNestedLayer,
|
|
8
|
-
isTopMost,
|
|
9
|
-
type LayerStackContextValue,
|
|
10
|
-
} from "./layer-stack";
|
|
4
|
+
import { isInBranch, isInNestedLayer, isTopMost, type LayerStackContextValue } from "./layer-stack";
|
|
11
5
|
|
|
12
6
|
export interface UsePointerDownOutsideOptions {
|
|
13
7
|
enabled: boolean;
|
|
@@ -74,7 +68,6 @@ export function usePointerDownOutside(
|
|
|
74
68
|
|
|
75
69
|
function isOutsideLayer(target: HTMLElement): boolean {
|
|
76
70
|
if (!isTopMost(ctx, node!)) return false;
|
|
77
|
-
if (isBelowPointerBlockingLayer(ctx, node!)) return false;
|
|
78
71
|
if (isInBranch(ctx, target)) return false;
|
|
79
72
|
if (isInNestedLayer(ctx, node!, target)) return false;
|
|
80
73
|
return true;
|
|
@@ -631,102 +631,4 @@ describe("useDismissibleLayer", () => {
|
|
|
631
631
|
expect(onFocusOutside).toHaveBeenCalledTimes(1);
|
|
632
632
|
});
|
|
633
633
|
});
|
|
634
|
-
|
|
635
|
-
describe("pointer event blocking", () => {
|
|
636
|
-
it("restores body.style.pointerEvents after nested blocking layers unmount", async () => {
|
|
637
|
-
const original = document.body.style.pointerEvents;
|
|
638
|
-
|
|
639
|
-
function Harness() {
|
|
640
|
-
const [showBoth, setShowBoth] = React.useState(true);
|
|
641
|
-
|
|
642
|
-
return (
|
|
643
|
-
<>
|
|
644
|
-
{showBoth && (
|
|
645
|
-
<DismissibleBox
|
|
646
|
-
options={{ enabled: true, blockPointerEvents: true }}
|
|
647
|
-
data-testid="outer"
|
|
648
|
-
>
|
|
649
|
-
<DismissibleBox
|
|
650
|
-
options={{ enabled: true, blockPointerEvents: true }}
|
|
651
|
-
data-testid="inner"
|
|
652
|
-
/>
|
|
653
|
-
</DismissibleBox>
|
|
654
|
-
)}
|
|
655
|
-
<button type="button" data-testid="unmount" onClick={() => setShowBoth(false)}>
|
|
656
|
-
Unmount
|
|
657
|
-
</button>
|
|
658
|
-
</>
|
|
659
|
-
);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const { getByTestId } = render(<Harness />);
|
|
663
|
-
|
|
664
|
-
// Both blocking layers are mounted → body should be "none"
|
|
665
|
-
expect(document.body.style.pointerEvents).toBe("none");
|
|
666
|
-
|
|
667
|
-
// Unmount both layers — React cleans up children before parents
|
|
668
|
-
await act(async () => getByTestId("unmount").click());
|
|
669
|
-
|
|
670
|
-
// body.style.pointerEvents must be restored
|
|
671
|
-
expect(document.body.style.pointerEvents).toBe(original);
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
it("restores body.style.pointerEvents when blocking layers mount sequentially then unmount together", async () => {
|
|
675
|
-
const original = document.body.style.pointerEvents;
|
|
676
|
-
|
|
677
|
-
function Harness() {
|
|
678
|
-
const [showFirst, setShowFirst] = React.useState(false);
|
|
679
|
-
const [showSecond, setShowSecond] = React.useState(false);
|
|
680
|
-
|
|
681
|
-
return (
|
|
682
|
-
<>
|
|
683
|
-
{showFirst && (
|
|
684
|
-
<DismissibleBox
|
|
685
|
-
options={{ enabled: true, blockPointerEvents: true }}
|
|
686
|
-
data-testid="first"
|
|
687
|
-
/>
|
|
688
|
-
)}
|
|
689
|
-
{showSecond && (
|
|
690
|
-
<DismissibleBox
|
|
691
|
-
options={{ enabled: true, blockPointerEvents: true }}
|
|
692
|
-
data-testid="second"
|
|
693
|
-
/>
|
|
694
|
-
)}
|
|
695
|
-
<button type="button" data-testid="mount-first" onClick={() => setShowFirst(true)}>
|
|
696
|
-
Mount First
|
|
697
|
-
</button>
|
|
698
|
-
<button type="button" data-testid="mount-second" onClick={() => setShowSecond(true)}>
|
|
699
|
-
Mount Second
|
|
700
|
-
</button>
|
|
701
|
-
<button
|
|
702
|
-
type="button"
|
|
703
|
-
data-testid="unmount-all"
|
|
704
|
-
onClick={() => {
|
|
705
|
-
setShowFirst(false);
|
|
706
|
-
setShowSecond(false);
|
|
707
|
-
}}
|
|
708
|
-
>
|
|
709
|
-
Unmount All
|
|
710
|
-
</button>
|
|
711
|
-
</>
|
|
712
|
-
);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
const { getByTestId } = render(<Harness />);
|
|
716
|
-
|
|
717
|
-
// Mount first blocking layer
|
|
718
|
-
await act(async () => getByTestId("mount-first").click());
|
|
719
|
-
expect(document.body.style.pointerEvents).toBe("none");
|
|
720
|
-
|
|
721
|
-
// Mount second blocking layer
|
|
722
|
-
await act(async () => getByTestId("mount-second").click());
|
|
723
|
-
expect(document.body.style.pointerEvents).toBe("none");
|
|
724
|
-
|
|
725
|
-
// Unmount both at once
|
|
726
|
-
await act(async () => getByTestId("unmount-all").click());
|
|
727
|
-
|
|
728
|
-
// body.style.pointerEvents must be restored
|
|
729
|
-
expect(document.body.style.pointerEvents).toBe(original);
|
|
730
|
-
});
|
|
731
|
-
});
|
|
732
634
|
});
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useCallback, useEffect, useRef, useState
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import {
|
|
5
5
|
addLayer,
|
|
6
6
|
removeLayer,
|
|
7
7
|
isTopMost,
|
|
8
|
-
getPointerEventsEnabled,
|
|
9
8
|
useLayerStackContext,
|
|
10
9
|
useDismissibleParentNode,
|
|
11
10
|
LAYER_UPDATE_EVENT,
|
|
@@ -22,12 +21,6 @@ export interface UseDismissibleLayerOptions {
|
|
|
22
21
|
*/
|
|
23
22
|
enabled: boolean;
|
|
24
23
|
|
|
25
|
-
/**
|
|
26
|
-
* When true, disables pointer events on elements outside this layer.
|
|
27
|
-
* Used for modal overlays (Dialog, BottomSheet).
|
|
28
|
-
*/
|
|
29
|
-
blockPointerEvents?: boolean;
|
|
30
|
-
|
|
31
24
|
/**
|
|
32
25
|
* Called when escape key is pressed while this layer is topmost.
|
|
33
26
|
* Call `event.preventDefault()` to prevent dismiss.
|
|
@@ -71,16 +64,9 @@ export interface UseDismissibleLayerOptions {
|
|
|
71
64
|
|
|
72
65
|
const NOOP = () => {};
|
|
73
66
|
|
|
74
|
-
// Module-level storage for the original body pointer-events value.
|
|
75
|
-
// Follows Radix's pattern: save once when the first blocking layer mounts,
|
|
76
|
-
// restore when the last blocking layer unmounts. This decouples save/restore
|
|
77
|
-
// from React's bottom-up cleanup order.
|
|
78
|
-
let originalBodyPointerEvents: string;
|
|
79
|
-
|
|
80
67
|
export function useDismissibleLayer(options: UseDismissibleLayerOptions) {
|
|
81
68
|
const {
|
|
82
69
|
enabled,
|
|
83
|
-
blockPointerEvents = false,
|
|
84
70
|
onEscapeKeyDown,
|
|
85
71
|
onPressOutside,
|
|
86
72
|
onFocusOutside,
|
|
@@ -114,7 +100,6 @@ export function useDismissibleLayer(options: UseDismissibleLayerOptions) {
|
|
|
114
100
|
const layer = {
|
|
115
101
|
node,
|
|
116
102
|
dismiss: (detail: CascadeDismissDetail) => onCascadeDismissRef.current?.(detail),
|
|
117
|
-
blockPointerEvents,
|
|
118
103
|
parentNode,
|
|
119
104
|
};
|
|
120
105
|
|
|
@@ -123,35 +108,9 @@ export function useDismissibleLayer(options: UseDismissibleLayerOptions) {
|
|
|
123
108
|
return () => {
|
|
124
109
|
removeLayer(ctx, node);
|
|
125
110
|
};
|
|
126
|
-
}, [node, enabled,
|
|
111
|
+
}, [node, enabled, ctx, parentNode]);
|
|
127
112
|
|
|
128
|
-
// --
|
|
129
|
-
// Follows Radix's pattern: save original value once when the first blocking
|
|
130
|
-
// layer mounts, restore when the last unmounts. The module-level variable
|
|
131
|
-
// decouples save/restore from React's bottom-up cleanup order.
|
|
132
|
-
useEffect(() => {
|
|
133
|
-
if (!node || !enabled || !blockPointerEvents) return;
|
|
134
|
-
|
|
135
|
-
const ownerDocument = node.ownerDocument ?? document;
|
|
136
|
-
const blockingLayers = ctx.layers.filter((l) => l.blockPointerEvents);
|
|
137
|
-
|
|
138
|
-
if (blockingLayers.length === 1) {
|
|
139
|
-
// First blocking layer — save the original value and block
|
|
140
|
-
originalBodyPointerEvents = ownerDocument.body.style.pointerEvents;
|
|
141
|
-
ownerDocument.body.style.pointerEvents = "none";
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return () => {
|
|
145
|
-
// Check count BEFORE this layer is removed from ctx.layers.
|
|
146
|
-
// At cleanup time, this layer is still in the array.
|
|
147
|
-
const remainingBlocking = ctx.layers.filter((l) => l.blockPointerEvents && l.node !== node);
|
|
148
|
-
if (remainingBlocking.length === 0) {
|
|
149
|
-
ownerDocument.body.style.pointerEvents = originalBodyPointerEvents;
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
}, [node, enabled, blockPointerEvents, ctx]);
|
|
153
|
-
|
|
154
|
-
// -- Subscribe to layer changes for style updates --
|
|
113
|
+
// -- Subscribe to layer changes to keep isTopLayer fresh --
|
|
155
114
|
useEffect(() => {
|
|
156
115
|
if (!enabled) return;
|
|
157
116
|
const handler = () => forceRender({});
|
|
@@ -195,14 +154,6 @@ export function useDismissibleLayer(options: UseDismissibleLayerOptions) {
|
|
|
195
154
|
onFocusOutside: handleFocusOutside,
|
|
196
155
|
});
|
|
197
156
|
|
|
198
|
-
// -- Compute pointer-events style --
|
|
199
|
-
const hasBlocking = ctx.layers.some((l) => l.blockPointerEvents);
|
|
200
|
-
const pointerEventsEnabled = node ? getPointerEventsEnabled(ctx, node) : true;
|
|
201
|
-
|
|
202
|
-
const style: CSSProperties = hasBlocking
|
|
203
|
-
? { pointerEvents: pointerEventsEnabled ? "auto" : "none" }
|
|
204
|
-
: {};
|
|
205
|
-
|
|
206
157
|
const topLayer = node ? isTopMost(ctx, node) : true;
|
|
207
158
|
|
|
208
159
|
if (!enabled) {
|
|
@@ -225,7 +176,6 @@ export function useDismissibleLayer(options: UseDismissibleLayerOptions) {
|
|
|
225
176
|
onPointerDownCapture: pressOutsideProps.onPointerDownCapture,
|
|
226
177
|
onFocusCapture: focusOutsideProps.onFocusCapture,
|
|
227
178
|
onBlurCapture: focusOutsideProps.onBlurCapture,
|
|
228
|
-
style,
|
|
229
179
|
},
|
|
230
180
|
isTopLayer: topLayer,
|
|
231
181
|
/** The current layer node, for providing DismissibleParentContext to children. */
|