@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,202 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useRef, useEffect } from 'react';
|
|
3
|
+
import { i as isTopMost, c as isBelowPointerBlockingLayer, a as isInBranch, b as isInNestedLayer } from './layer-stack-12s-DjRVp1_u.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detects pointer-down events outside a React subtree.
|
|
7
|
+
* Uses `onPointerDownCapture` on the element to distinguish React-tree-inside
|
|
8
|
+
* from DOM-tree-inside (Portal support).
|
|
9
|
+
*
|
|
10
|
+
* Ported from Radix's usePointerDownOutside with layer stack integration.
|
|
11
|
+
*/ function usePointerDownOutside(node, ctx, options) {
|
|
12
|
+
const callbackRef = useRef(options.onPressOutside);
|
|
13
|
+
callbackRef.current = options.onPressOutside;
|
|
14
|
+
const excludeRef = useRef(options.exclude);
|
|
15
|
+
excludeRef.current = options.exclude;
|
|
16
|
+
const pressBehaviorRef = useRef(options.pressBehavior);
|
|
17
|
+
pressBehaviorRef.current = options.pressBehavior;
|
|
18
|
+
const isPointerInsideReactTreeRef = useRef(false);
|
|
19
|
+
const handleClickRef = useRef(()=>{});
|
|
20
|
+
// Touch drag state (only used in "drag" mode)
|
|
21
|
+
const touchStateRef = useRef(null);
|
|
22
|
+
const suppressPointerRef = useRef(false);
|
|
23
|
+
const suppressTimerRef = useRef();
|
|
24
|
+
useEffect(()=>{
|
|
25
|
+
if (!node || !options.enabled) return;
|
|
26
|
+
const ownerDocument = node.ownerDocument ?? document;
|
|
27
|
+
function isOutsideLayer(target) {
|
|
28
|
+
if (!isTopMost(ctx, node)) return false;
|
|
29
|
+
if (isBelowPointerBlockingLayer(ctx, node)) return false;
|
|
30
|
+
if (isInBranch(ctx, target)) return false;
|
|
31
|
+
if (isInNestedLayer(ctx, node, target)) return false;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
function isOutsideTarget(target) {
|
|
35
|
+
if (!isOutsideLayer(target)) return false;
|
|
36
|
+
if (excludeRef.current?.(target)) return false;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
function deferToClick(handleAndDispatch) {
|
|
40
|
+
ownerDocument.removeEventListener("click", handleClickRef.current);
|
|
41
|
+
handleClickRef.current = handleAndDispatch;
|
|
42
|
+
ownerDocument.addEventListener("click", handleClickRef.current, {
|
|
43
|
+
once: true
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const handlePointerDown = (event)=>{
|
|
47
|
+
const target = event.target;
|
|
48
|
+
if (target && !isPointerInsideReactTreeRef.current) {
|
|
49
|
+
if (!isOutsideTarget(target)) {
|
|
50
|
+
isPointerInsideReactTreeRef.current = false;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// In drag mode, suppress synthetic pointerdown that follows a touch dismiss.
|
|
54
|
+
if (pressBehaviorRef.current === "drag" && suppressPointerRef.current) {
|
|
55
|
+
isPointerInsideReactTreeRef.current = false;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
function handleAndDispatch() {
|
|
59
|
+
callbackRef.current(event);
|
|
60
|
+
}
|
|
61
|
+
const behavior = pressBehaviorRef.current ?? "confirm";
|
|
62
|
+
if (behavior === "confirm") {
|
|
63
|
+
// Both mouse and touch defer to click
|
|
64
|
+
deferToClick(handleAndDispatch);
|
|
65
|
+
} else if (behavior === "drag") {
|
|
66
|
+
// Touch is handled by touchstart/move/end listeners.
|
|
67
|
+
// Only process non-touch pointerdown (mouse, pen).
|
|
68
|
+
if (event.pointerType === "touch") {
|
|
69
|
+
isPointerInsideReactTreeRef.current = false;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
handleAndDispatch();
|
|
73
|
+
} else {
|
|
74
|
+
// "eager" (default): mouse immediate, touch defers to click
|
|
75
|
+
if (event.pointerType === "touch") {
|
|
76
|
+
deferToClick(handleAndDispatch);
|
|
77
|
+
} else {
|
|
78
|
+
handleAndDispatch();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
ownerDocument.removeEventListener("click", handleClickRef.current);
|
|
83
|
+
}
|
|
84
|
+
isPointerInsideReactTreeRef.current = false;
|
|
85
|
+
};
|
|
86
|
+
// -- Touch drag handlers (only active in "drag" mode) --
|
|
87
|
+
const isTouchInsideReactTreeRef = {
|
|
88
|
+
current: false
|
|
89
|
+
};
|
|
90
|
+
const handleTouchStartCapture = ()=>{
|
|
91
|
+
isTouchInsideReactTreeRef.current = true;
|
|
92
|
+
};
|
|
93
|
+
const handleTouchStart = (event)=>{
|
|
94
|
+
if (pressBehaviorRef.current !== "drag") return;
|
|
95
|
+
if (isTouchInsideReactTreeRef.current) {
|
|
96
|
+
isTouchInsideReactTreeRef.current = false;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const target = event.target;
|
|
100
|
+
if (!target || !isOutsideLayer(target)) return;
|
|
101
|
+
const touch = event.touches[0];
|
|
102
|
+
if (!touch) return;
|
|
103
|
+
const startedOnExcluded = !!excludeRef.current?.(target);
|
|
104
|
+
touchStateRef.current = {
|
|
105
|
+
startX: touch.clientX,
|
|
106
|
+
startY: touch.clientY,
|
|
107
|
+
dismissOnTouchEnd: false,
|
|
108
|
+
startedOnExcluded
|
|
109
|
+
};
|
|
110
|
+
// Activate click-through guard
|
|
111
|
+
suppressPointerRef.current = true;
|
|
112
|
+
clearTimeout(suppressTimerRef.current);
|
|
113
|
+
suppressTimerRef.current = setTimeout(()=>{
|
|
114
|
+
suppressPointerRef.current = false;
|
|
115
|
+
if (touchStateRef.current) {
|
|
116
|
+
touchStateRef.current.dismissOnTouchEnd = false;
|
|
117
|
+
}
|
|
118
|
+
}, 1000);
|
|
119
|
+
};
|
|
120
|
+
const handleTouchMove = (event)=>{
|
|
121
|
+
if (pressBehaviorRef.current !== "drag" || !touchStateRef.current) return;
|
|
122
|
+
const target = event.target;
|
|
123
|
+
if (target && node.contains(target)) return;
|
|
124
|
+
const touch = event.touches[0];
|
|
125
|
+
if (!touch) return;
|
|
126
|
+
const deltaX = Math.abs(touch.clientX - touchStateRef.current.startX);
|
|
127
|
+
const deltaY = Math.abs(touch.clientY - touchStateRef.current.startY);
|
|
128
|
+
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
129
|
+
if (distance > 5) {
|
|
130
|
+
touchStateRef.current.dismissOnTouchEnd = true;
|
|
131
|
+
}
|
|
132
|
+
if (distance > 10) {
|
|
133
|
+
callbackRef.current(event);
|
|
134
|
+
clearTimeout(suppressTimerRef.current);
|
|
135
|
+
suppressTimerRef.current = setTimeout(()=>{
|
|
136
|
+
suppressPointerRef.current = false;
|
|
137
|
+
}, 1000);
|
|
138
|
+
touchStateRef.current = null;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const handleTouchEnd = ()=>{
|
|
142
|
+
if (pressBehaviorRef.current !== "drag" || !touchStateRef.current) return;
|
|
143
|
+
if (touchStateRef.current.dismissOnTouchEnd) {
|
|
144
|
+
// Drag detected (>5px): dismiss immediately
|
|
145
|
+
callbackRef.current(new PointerEvent("pointerdown"));
|
|
146
|
+
clearTimeout(suppressTimerRef.current);
|
|
147
|
+
suppressTimerRef.current = setTimeout(()=>{
|
|
148
|
+
suppressPointerRef.current = false;
|
|
149
|
+
}, 1000);
|
|
150
|
+
} else if (!touchStateRef.current.startedOnExcluded) {
|
|
151
|
+
// Tap (no significant movement): defer to click, like "confirm" mode.
|
|
152
|
+
// Skip if tap started on an excluded target (e.g., trigger) — toggle handles it.
|
|
153
|
+
suppressPointerRef.current = false;
|
|
154
|
+
clearTimeout(suppressTimerRef.current);
|
|
155
|
+
deferToClick(()=>callbackRef.current(new PointerEvent("pointerdown")));
|
|
156
|
+
} else {
|
|
157
|
+
// Tap on excluded target — clean up guard without dismissing
|
|
158
|
+
suppressPointerRef.current = false;
|
|
159
|
+
clearTimeout(suppressTimerRef.current);
|
|
160
|
+
}
|
|
161
|
+
touchStateRef.current = null;
|
|
162
|
+
};
|
|
163
|
+
// Delay registration to prevent the mount-triggering pointerdown from
|
|
164
|
+
// being detected as "outside". This is a DOM behavior, not React-specific.
|
|
165
|
+
const timerId = window.setTimeout(()=>{
|
|
166
|
+
ownerDocument.addEventListener("pointerdown", handlePointerDown);
|
|
167
|
+
if (pressBehaviorRef.current === "drag") {
|
|
168
|
+
ownerDocument.addEventListener("touchstart", handleTouchStart);
|
|
169
|
+
ownerDocument.addEventListener("touchmove", handleTouchMove);
|
|
170
|
+
ownerDocument.addEventListener("touchend", handleTouchEnd);
|
|
171
|
+
}
|
|
172
|
+
}, 0);
|
|
173
|
+
// Register capture handler on node for React tree detection
|
|
174
|
+
if (pressBehaviorRef.current === "drag") {
|
|
175
|
+
node.addEventListener("touchstart", handleTouchStartCapture, true);
|
|
176
|
+
}
|
|
177
|
+
return ()=>{
|
|
178
|
+
window.clearTimeout(timerId);
|
|
179
|
+
clearTimeout(suppressTimerRef.current);
|
|
180
|
+
ownerDocument.removeEventListener("pointerdown", handlePointerDown);
|
|
181
|
+
ownerDocument.removeEventListener("click", handleClickRef.current);
|
|
182
|
+
ownerDocument.removeEventListener("touchstart", handleTouchStart);
|
|
183
|
+
ownerDocument.removeEventListener("touchmove", handleTouchMove);
|
|
184
|
+
ownerDocument.removeEventListener("touchend", handleTouchEnd);
|
|
185
|
+
node?.removeEventListener("touchstart", handleTouchStartCapture, true);
|
|
186
|
+
touchStateRef.current = null;
|
|
187
|
+
suppressPointerRef.current = false;
|
|
188
|
+
};
|
|
189
|
+
}, [
|
|
190
|
+
node,
|
|
191
|
+
ctx,
|
|
192
|
+
options.enabled
|
|
193
|
+
]);
|
|
194
|
+
return {
|
|
195
|
+
// React synthetic event — fires for React-tree descendants (including Portals).
|
|
196
|
+
onPointerDownCapture: ()=>{
|
|
197
|
+
isPointerInsideReactTreeRef.current = true;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export { usePointerDownOutside as u };
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
var react = require('react');
|
|
3
|
+
var layerStack12s = require('./layer-stack-12s-Ctg3ipjW.cjs');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detects pointer-down events outside a React subtree.
|
|
7
|
+
* Uses `onPointerDownCapture` on the element to distinguish React-tree-inside
|
|
8
|
+
* from DOM-tree-inside (Portal support).
|
|
9
|
+
*
|
|
10
|
+
* Ported from Radix's usePointerDownOutside with layer stack integration.
|
|
11
|
+
*/ function usePointerDownOutside(node, ctx, options) {
|
|
12
|
+
const callbackRef = react.useRef(options.onPressOutside);
|
|
13
|
+
callbackRef.current = options.onPressOutside;
|
|
14
|
+
const excludeRef = react.useRef(options.exclude);
|
|
15
|
+
excludeRef.current = options.exclude;
|
|
16
|
+
const pressBehaviorRef = react.useRef(options.pressBehavior);
|
|
17
|
+
pressBehaviorRef.current = options.pressBehavior;
|
|
18
|
+
const isPointerInsideReactTreeRef = react.useRef(false);
|
|
19
|
+
const handleClickRef = react.useRef(()=>{});
|
|
20
|
+
// Touch drag state (only used in "drag" mode)
|
|
21
|
+
const touchStateRef = react.useRef(null);
|
|
22
|
+
const suppressPointerRef = react.useRef(false);
|
|
23
|
+
const suppressTimerRef = react.useRef();
|
|
24
|
+
react.useEffect(()=>{
|
|
25
|
+
if (!node || !options.enabled) return;
|
|
26
|
+
const ownerDocument = node.ownerDocument ?? document;
|
|
27
|
+
function isOutsideLayer(target) {
|
|
28
|
+
if (!layerStack12s.isTopMost(ctx, node)) return false;
|
|
29
|
+
if (layerStack12s.isBelowPointerBlockingLayer(ctx, node)) return false;
|
|
30
|
+
if (layerStack12s.isInBranch(ctx, target)) return false;
|
|
31
|
+
if (layerStack12s.isInNestedLayer(ctx, node, target)) return false;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
function isOutsideTarget(target) {
|
|
35
|
+
if (!isOutsideLayer(target)) return false;
|
|
36
|
+
if (excludeRef.current?.(target)) return false;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
function deferToClick(handleAndDispatch) {
|
|
40
|
+
ownerDocument.removeEventListener("click", handleClickRef.current);
|
|
41
|
+
handleClickRef.current = handleAndDispatch;
|
|
42
|
+
ownerDocument.addEventListener("click", handleClickRef.current, {
|
|
43
|
+
once: true
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const handlePointerDown = (event)=>{
|
|
47
|
+
const target = event.target;
|
|
48
|
+
if (target && !isPointerInsideReactTreeRef.current) {
|
|
49
|
+
if (!isOutsideTarget(target)) {
|
|
50
|
+
isPointerInsideReactTreeRef.current = false;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// In drag mode, suppress synthetic pointerdown that follows a touch dismiss.
|
|
54
|
+
if (pressBehaviorRef.current === "drag" && suppressPointerRef.current) {
|
|
55
|
+
isPointerInsideReactTreeRef.current = false;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
function handleAndDispatch() {
|
|
59
|
+
callbackRef.current(event);
|
|
60
|
+
}
|
|
61
|
+
const behavior = pressBehaviorRef.current ?? "confirm";
|
|
62
|
+
if (behavior === "confirm") {
|
|
63
|
+
// Both mouse and touch defer to click
|
|
64
|
+
deferToClick(handleAndDispatch);
|
|
65
|
+
} else if (behavior === "drag") {
|
|
66
|
+
// Touch is handled by touchstart/move/end listeners.
|
|
67
|
+
// Only process non-touch pointerdown (mouse, pen).
|
|
68
|
+
if (event.pointerType === "touch") {
|
|
69
|
+
isPointerInsideReactTreeRef.current = false;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
handleAndDispatch();
|
|
73
|
+
} else {
|
|
74
|
+
// "eager" (default): mouse immediate, touch defers to click
|
|
75
|
+
if (event.pointerType === "touch") {
|
|
76
|
+
deferToClick(handleAndDispatch);
|
|
77
|
+
} else {
|
|
78
|
+
handleAndDispatch();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
ownerDocument.removeEventListener("click", handleClickRef.current);
|
|
83
|
+
}
|
|
84
|
+
isPointerInsideReactTreeRef.current = false;
|
|
85
|
+
};
|
|
86
|
+
// -- Touch drag handlers (only active in "drag" mode) --
|
|
87
|
+
const isTouchInsideReactTreeRef = {
|
|
88
|
+
current: false
|
|
89
|
+
};
|
|
90
|
+
const handleTouchStartCapture = ()=>{
|
|
91
|
+
isTouchInsideReactTreeRef.current = true;
|
|
92
|
+
};
|
|
93
|
+
const handleTouchStart = (event)=>{
|
|
94
|
+
if (pressBehaviorRef.current !== "drag") return;
|
|
95
|
+
if (isTouchInsideReactTreeRef.current) {
|
|
96
|
+
isTouchInsideReactTreeRef.current = false;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const target = event.target;
|
|
100
|
+
if (!target || !isOutsideLayer(target)) return;
|
|
101
|
+
const touch = event.touches[0];
|
|
102
|
+
if (!touch) return;
|
|
103
|
+
const startedOnExcluded = !!excludeRef.current?.(target);
|
|
104
|
+
touchStateRef.current = {
|
|
105
|
+
startX: touch.clientX,
|
|
106
|
+
startY: touch.clientY,
|
|
107
|
+
dismissOnTouchEnd: false,
|
|
108
|
+
startedOnExcluded
|
|
109
|
+
};
|
|
110
|
+
// Activate click-through guard
|
|
111
|
+
suppressPointerRef.current = true;
|
|
112
|
+
clearTimeout(suppressTimerRef.current);
|
|
113
|
+
suppressTimerRef.current = setTimeout(()=>{
|
|
114
|
+
suppressPointerRef.current = false;
|
|
115
|
+
if (touchStateRef.current) {
|
|
116
|
+
touchStateRef.current.dismissOnTouchEnd = false;
|
|
117
|
+
}
|
|
118
|
+
}, 1000);
|
|
119
|
+
};
|
|
120
|
+
const handleTouchMove = (event)=>{
|
|
121
|
+
if (pressBehaviorRef.current !== "drag" || !touchStateRef.current) return;
|
|
122
|
+
const target = event.target;
|
|
123
|
+
if (target && node.contains(target)) return;
|
|
124
|
+
const touch = event.touches[0];
|
|
125
|
+
if (!touch) return;
|
|
126
|
+
const deltaX = Math.abs(touch.clientX - touchStateRef.current.startX);
|
|
127
|
+
const deltaY = Math.abs(touch.clientY - touchStateRef.current.startY);
|
|
128
|
+
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
129
|
+
if (distance > 5) {
|
|
130
|
+
touchStateRef.current.dismissOnTouchEnd = true;
|
|
131
|
+
}
|
|
132
|
+
if (distance > 10) {
|
|
133
|
+
callbackRef.current(event);
|
|
134
|
+
clearTimeout(suppressTimerRef.current);
|
|
135
|
+
suppressTimerRef.current = setTimeout(()=>{
|
|
136
|
+
suppressPointerRef.current = false;
|
|
137
|
+
}, 1000);
|
|
138
|
+
touchStateRef.current = null;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const handleTouchEnd = ()=>{
|
|
142
|
+
if (pressBehaviorRef.current !== "drag" || !touchStateRef.current) return;
|
|
143
|
+
if (touchStateRef.current.dismissOnTouchEnd) {
|
|
144
|
+
// Drag detected (>5px): dismiss immediately
|
|
145
|
+
callbackRef.current(new PointerEvent("pointerdown"));
|
|
146
|
+
clearTimeout(suppressTimerRef.current);
|
|
147
|
+
suppressTimerRef.current = setTimeout(()=>{
|
|
148
|
+
suppressPointerRef.current = false;
|
|
149
|
+
}, 1000);
|
|
150
|
+
} else if (!touchStateRef.current.startedOnExcluded) {
|
|
151
|
+
// Tap (no significant movement): defer to click, like "confirm" mode.
|
|
152
|
+
// Skip if tap started on an excluded target (e.g., trigger) — toggle handles it.
|
|
153
|
+
suppressPointerRef.current = false;
|
|
154
|
+
clearTimeout(suppressTimerRef.current);
|
|
155
|
+
deferToClick(()=>callbackRef.current(new PointerEvent("pointerdown")));
|
|
156
|
+
} else {
|
|
157
|
+
// Tap on excluded target — clean up guard without dismissing
|
|
158
|
+
suppressPointerRef.current = false;
|
|
159
|
+
clearTimeout(suppressTimerRef.current);
|
|
160
|
+
}
|
|
161
|
+
touchStateRef.current = null;
|
|
162
|
+
};
|
|
163
|
+
// Delay registration to prevent the mount-triggering pointerdown from
|
|
164
|
+
// being detected as "outside". This is a DOM behavior, not React-specific.
|
|
165
|
+
const timerId = window.setTimeout(()=>{
|
|
166
|
+
ownerDocument.addEventListener("pointerdown", handlePointerDown);
|
|
167
|
+
if (pressBehaviorRef.current === "drag") {
|
|
168
|
+
ownerDocument.addEventListener("touchstart", handleTouchStart);
|
|
169
|
+
ownerDocument.addEventListener("touchmove", handleTouchMove);
|
|
170
|
+
ownerDocument.addEventListener("touchend", handleTouchEnd);
|
|
171
|
+
}
|
|
172
|
+
}, 0);
|
|
173
|
+
// Register capture handler on node for React tree detection
|
|
174
|
+
if (pressBehaviorRef.current === "drag") {
|
|
175
|
+
node.addEventListener("touchstart", handleTouchStartCapture, true);
|
|
176
|
+
}
|
|
177
|
+
return ()=>{
|
|
178
|
+
window.clearTimeout(timerId);
|
|
179
|
+
clearTimeout(suppressTimerRef.current);
|
|
180
|
+
ownerDocument.removeEventListener("pointerdown", handlePointerDown);
|
|
181
|
+
ownerDocument.removeEventListener("click", handleClickRef.current);
|
|
182
|
+
ownerDocument.removeEventListener("touchstart", handleTouchStart);
|
|
183
|
+
ownerDocument.removeEventListener("touchmove", handleTouchMove);
|
|
184
|
+
ownerDocument.removeEventListener("touchend", handleTouchEnd);
|
|
185
|
+
node?.removeEventListener("touchstart", handleTouchStartCapture, true);
|
|
186
|
+
touchStateRef.current = null;
|
|
187
|
+
suppressPointerRef.current = false;
|
|
188
|
+
};
|
|
189
|
+
}, [
|
|
190
|
+
node,
|
|
191
|
+
ctx,
|
|
192
|
+
options.enabled
|
|
193
|
+
]);
|
|
194
|
+
return {
|
|
195
|
+
// React synthetic event — fires for React-tree descendants (including Portals).
|
|
196
|
+
onPointerDownCapture: ()=>{
|
|
197
|
+
isPointerInsideReactTreeRef.current = true;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
exports.usePointerDownOutside = usePointerDownOutside;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
3
|
+
import { u as useLayerStackContext, d as useDismissibleParentNode, e as addLayer, r as removeLayer, L as LAYER_UPDATE_EVENT, g as getPointerEventsEnabled, i as isTopMost } from './layer-stack-12s-DjRVp1_u.js';
|
|
4
|
+
import { u as useEscapeKeydown } from './use-escape-keydown-12s-Bl_her-_.js';
|
|
5
|
+
import { u as usePointerDownOutside } from './use-pointer-down-outside-12s-BLctGw0m.js';
|
|
6
|
+
import { u as useFocusOutside } from './use-focus-outside-12s-LpFt7UjO.js';
|
|
7
|
+
|
|
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
|
+
function useDismissibleLayer(options) {
|
|
15
|
+
const { enabled, blockPointerEvents = false, onEscapeKeyDown, onPressOutside, onFocusOutside, onCascadeDismiss, exclude, pressBehavior } = options;
|
|
16
|
+
const ctx = useLayerStackContext();
|
|
17
|
+
const parentNode = useDismissibleParentNode();
|
|
18
|
+
const [node, setNode] = useState(null);
|
|
19
|
+
const [, forceRender] = useState({});
|
|
20
|
+
// Callback ref — fires when element mounts/unmounts.
|
|
21
|
+
// Drives `node` state which triggers registration and event listener setup.
|
|
22
|
+
const dismissibleRef = useCallback((el)=>{
|
|
23
|
+
setNode(el);
|
|
24
|
+
}, []);
|
|
25
|
+
// Stable callback refs
|
|
26
|
+
const onCascadeDismissRef = useRef(onCascadeDismiss);
|
|
27
|
+
onCascadeDismissRef.current = onCascadeDismiss;
|
|
28
|
+
const onEscapeKeyDownRef = useRef(onEscapeKeyDown);
|
|
29
|
+
onEscapeKeyDownRef.current = onEscapeKeyDown;
|
|
30
|
+
// -- Layer registration --
|
|
31
|
+
useEffect(()=>{
|
|
32
|
+
if (!node || !enabled) return;
|
|
33
|
+
const layer = {
|
|
34
|
+
node,
|
|
35
|
+
dismiss: (detail)=>onCascadeDismissRef.current?.(detail),
|
|
36
|
+
blockPointerEvents,
|
|
37
|
+
parentNode
|
|
38
|
+
};
|
|
39
|
+
addLayer(ctx, layer);
|
|
40
|
+
return ()=>{
|
|
41
|
+
removeLayer(ctx, node);
|
|
42
|
+
};
|
|
43
|
+
}, [
|
|
44
|
+
node,
|
|
45
|
+
enabled,
|
|
46
|
+
blockPointerEvents,
|
|
47
|
+
ctx,
|
|
48
|
+
parentNode
|
|
49
|
+
]);
|
|
50
|
+
// -- Pointer event blocking --
|
|
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 --
|
|
78
|
+
useEffect(()=>{
|
|
79
|
+
if (!enabled) return;
|
|
80
|
+
const handler = ()=>forceRender({});
|
|
81
|
+
document.addEventListener(LAYER_UPDATE_EVENT, handler);
|
|
82
|
+
return ()=>document.removeEventListener(LAYER_UPDATE_EVENT, handler);
|
|
83
|
+
}, [
|
|
84
|
+
enabled
|
|
85
|
+
]);
|
|
86
|
+
// -- Escape keydown --
|
|
87
|
+
const handleEscapeKeyDown = useCallback((event)=>{
|
|
88
|
+
onEscapeKeyDownRef.current?.(event);
|
|
89
|
+
}, []);
|
|
90
|
+
useEscapeKeydown(enabled ? node : null, ctx, handleEscapeKeyDown);
|
|
91
|
+
// -- Press outside --
|
|
92
|
+
const handlePressOutside = useCallback((event)=>{
|
|
93
|
+
onPressOutside(event);
|
|
94
|
+
}, [
|
|
95
|
+
onPressOutside
|
|
96
|
+
]);
|
|
97
|
+
const pressOutsideProps = usePointerDownOutside(enabled ? node : null, ctx, {
|
|
98
|
+
enabled,
|
|
99
|
+
exclude,
|
|
100
|
+
onPressOutside: handlePressOutside,
|
|
101
|
+
pressBehavior
|
|
102
|
+
});
|
|
103
|
+
// -- Focus outside --
|
|
104
|
+
const handleFocusOutside = useCallback((event)=>{
|
|
105
|
+
onFocusOutside(event);
|
|
106
|
+
}, [
|
|
107
|
+
onFocusOutside
|
|
108
|
+
]);
|
|
109
|
+
const focusOutsideProps = useFocusOutside(enabled ? node : null, ctx, {
|
|
110
|
+
enabled,
|
|
111
|
+
exclude,
|
|
112
|
+
onFocusOutside: handleFocusOutside
|
|
113
|
+
});
|
|
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
|
+
const topLayer = node ? isTopMost(ctx, node) : true;
|
|
121
|
+
if (!enabled) {
|
|
122
|
+
return {
|
|
123
|
+
dismissibleRef,
|
|
124
|
+
dismissibleProps: {
|
|
125
|
+
onPointerDownCapture: NOOP,
|
|
126
|
+
onFocusCapture: NOOP,
|
|
127
|
+
onBlurCapture: NOOP
|
|
128
|
+
},
|
|
129
|
+
isTopLayer: true,
|
|
130
|
+
/** The current layer node, for providing DismissibleParentContext to children. */ layerNode: node
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
dismissibleRef,
|
|
135
|
+
dismissibleProps: {
|
|
136
|
+
onPointerDownCapture: pressOutsideProps.onPointerDownCapture,
|
|
137
|
+
onFocusCapture: focusOutsideProps.onFocusCapture,
|
|
138
|
+
onBlurCapture: focusOutsideProps.onBlurCapture,
|
|
139
|
+
style
|
|
140
|
+
},
|
|
141
|
+
isTopLayer: topLayer,
|
|
142
|
+
/** The current layer node, for providing DismissibleParentContext to children. */ layerNode: node
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export { useDismissibleLayer as u };
|