@seed-design/react-drawer 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/Drawer-12s-D38FEQUp.cjs +1361 -0
- package/lib/Drawer-12s-DPrL_6Um.js +1346 -0
- package/lib/index.cjs +29 -0
- package/lib/index.d.ts +4317 -0
- package/lib/index.js +18 -0
- package/package.json +52 -0
- package/src/Drawer.namespace.ts +22 -0
- package/src/Drawer.tsx +451 -0
- package/src/browser.ts +44 -0
- package/src/constants.ts +21 -0
- package/src/helpers.ts +145 -0
- package/src/index.ts +27 -0
- package/src/types.ts +7 -0
- package/src/use-position-fixed.ts +149 -0
- package/src/use-snap-points.ts +353 -0
- package/src/useDrawer.test.tsx +341 -0
- package/src/useDrawer.ts +810 -0
- package/src/useDrawerContext.tsx +15 -0
|
@@ -0,0 +1,1361 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
3
|
+
var reactComposeRefs = require('@radix-ui/react-compose-refs');
|
|
4
|
+
var reactFocusScope = require('@radix-ui/react-focus-scope');
|
|
5
|
+
var reactUseCallbackRef = require('@radix-ui/react-use-callback-ref');
|
|
6
|
+
var ariaHidden = require('aria-hidden');
|
|
7
|
+
var reactDismissibleLayer = require('@seed-design/react-dismissible-layer');
|
|
8
|
+
var reactPresence = require('@seed-design/react-presence');
|
|
9
|
+
var domUtils = require('@seed-design/dom-utils');
|
|
10
|
+
var reactPrimitive = require('@seed-design/react-primitive');
|
|
11
|
+
var React = require('react');
|
|
12
|
+
var reactUseControllableState = require('@seed-design/react-use-controllable-state');
|
|
13
|
+
|
|
14
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
15
|
+
|
|
16
|
+
var React__default = /*#__PURE__*/_interopDefault(React);
|
|
17
|
+
|
|
18
|
+
function isMobileFirefox() {
|
|
19
|
+
if (typeof window === "undefined" || typeof navigator === "undefined") return false;
|
|
20
|
+
return /Firefox/.test(navigator.userAgent) && /Mobile/.test(navigator.userAgent) || /FxiOS/.test(navigator.userAgent);
|
|
21
|
+
}
|
|
22
|
+
function isMac() {
|
|
23
|
+
return testPlatform(/^Mac/);
|
|
24
|
+
}
|
|
25
|
+
function isIPhone() {
|
|
26
|
+
return testPlatform(/^iPhone/);
|
|
27
|
+
}
|
|
28
|
+
function isSafari() {
|
|
29
|
+
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
30
|
+
}
|
|
31
|
+
function isIPad() {
|
|
32
|
+
return testPlatform(/^iPad/) || // iPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support.
|
|
33
|
+
isMac() && navigator.maxTouchPoints > 1;
|
|
34
|
+
}
|
|
35
|
+
function isIOS() {
|
|
36
|
+
return isIPhone() || isIPad();
|
|
37
|
+
}
|
|
38
|
+
function isAndroid() {
|
|
39
|
+
if (typeof window === "undefined" || typeof navigator === "undefined") return false;
|
|
40
|
+
return /Android/.test(navigator.userAgent);
|
|
41
|
+
}
|
|
42
|
+
// TODO: use userAgent instead?
|
|
43
|
+
function testPlatform(re) {
|
|
44
|
+
return typeof window !== "undefined" && window.navigator != null ? re.test(window.navigator.platform) : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* TODO: move to recipe
|
|
49
|
+
*/ const TRANSITIONS = {
|
|
50
|
+
ENTER_DURATION: 0.3,
|
|
51
|
+
EXIT_DURATION: 0.2,
|
|
52
|
+
OVERLAY_ENTER_TIMING_FUNCTION: "cubic-bezier(0, 0, 0.15, 1)",
|
|
53
|
+
OVERLAY_EXIT_TIMING_FUNCTION: "cubic-bezier(0.35, 0, 1, 1)",
|
|
54
|
+
CONTENT_ENTER_TIMING_FUNCTION: "cubic-bezier(0.03, 0.4, 0.1, 1)",
|
|
55
|
+
CONTENT_EXIT_TIMING_FUNCTION: "cubic-bezier(0.35, 0, 1, 1)"
|
|
56
|
+
};
|
|
57
|
+
const VELOCITY_THRESHOLD = 0.4;
|
|
58
|
+
const CLOSE_THRESHOLD = 0.25;
|
|
59
|
+
const SCROLL_LOCK_TIMEOUT = 100;
|
|
60
|
+
const WINDOW_TOP_OFFSET = 26;
|
|
61
|
+
const DRAG_CLASS = "seed-dragging";
|
|
62
|
+
|
|
63
|
+
const cache = new WeakMap();
|
|
64
|
+
// HTML input types that do not cause the software keyboard to appear.
|
|
65
|
+
const nonTextInputTypes = new Set([
|
|
66
|
+
"checkbox",
|
|
67
|
+
"radio",
|
|
68
|
+
"range",
|
|
69
|
+
"color",
|
|
70
|
+
"file",
|
|
71
|
+
"image",
|
|
72
|
+
"button",
|
|
73
|
+
"submit",
|
|
74
|
+
"reset"
|
|
75
|
+
]);
|
|
76
|
+
function isInput(target) {
|
|
77
|
+
return target instanceof HTMLInputElement && !nonTextInputTypes.has(target.type) || target instanceof HTMLTextAreaElement || target instanceof HTMLElement && target.isContentEditable;
|
|
78
|
+
}
|
|
79
|
+
function set(el, styles, ignoreCache = false) {
|
|
80
|
+
if (!el || !(el instanceof HTMLElement)) return;
|
|
81
|
+
let originalStyles = {};
|
|
82
|
+
Object.entries(styles).forEach(([key, value])=>{
|
|
83
|
+
if (key.startsWith("--")) {
|
|
84
|
+
el.style.setProperty(key, value);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
originalStyles[key] = el.style[key];
|
|
88
|
+
el.style[key] = value;
|
|
89
|
+
});
|
|
90
|
+
if (ignoreCache) return;
|
|
91
|
+
cache.set(el, originalStyles);
|
|
92
|
+
}
|
|
93
|
+
function reset(el, prop) {
|
|
94
|
+
if (!el || !(el instanceof HTMLElement)) return;
|
|
95
|
+
let originalStyles = cache.get(el);
|
|
96
|
+
if (!originalStyles) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
{
|
|
100
|
+
el.style[prop] = originalStyles[prop];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const isVertical = (direction)=>{
|
|
104
|
+
switch(direction){
|
|
105
|
+
case "top":
|
|
106
|
+
case "bottom":
|
|
107
|
+
return true;
|
|
108
|
+
case "left":
|
|
109
|
+
case "right":
|
|
110
|
+
return false;
|
|
111
|
+
default:
|
|
112
|
+
return direction;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
function getTranslate(element, direction) {
|
|
116
|
+
if (!element) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const style = window.getComputedStyle(element);
|
|
120
|
+
const transform = // @ts-ignore
|
|
121
|
+
style.transform || style.webkitTransform || style.mozTransform;
|
|
122
|
+
let mat = transform.match(/^matrix3d\((.+)\)$/);
|
|
123
|
+
if (mat) {
|
|
124
|
+
// https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix3d
|
|
125
|
+
return Number.parseFloat(mat[1].split(", ")[isVertical(direction) ? 13 : 12]);
|
|
126
|
+
}
|
|
127
|
+
// https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix
|
|
128
|
+
mat = transform.match(/^matrix\((.+)\)$/);
|
|
129
|
+
return mat ? Number.parseFloat(mat[1].split(", ")[isVertical(direction) ? 5 : 4]) : null;
|
|
130
|
+
}
|
|
131
|
+
function dampenValue(v) {
|
|
132
|
+
return 8 * (Math.log(v + 1) - 2);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// This code comes from https://github.com/emilkowalski/vaul/blob/main/src/use-position-fixed.ts
|
|
136
|
+
let previousBodyPosition = null;
|
|
137
|
+
/**
|
|
138
|
+
* This hook is necessary to prevent buggy behavior on iOS devices (need to test on Android).
|
|
139
|
+
* I won't get into too much detail about what bugs it solves, but so far I've found that setting the body to `position: fixed` is the most reliable way to prevent those bugs.
|
|
140
|
+
* Issues that this hook solves:
|
|
141
|
+
* https://github.com/emilkowalski/vaul/issues/435
|
|
142
|
+
* https://github.com/emilkowalski/vaul/issues/433
|
|
143
|
+
* And more that I discovered, but were just not reported.
|
|
144
|
+
*/ function usePositionFixed({ isOpen, modal, nested, hasBeenOpened, preventScrollRestoration, noBodyStyles }) {
|
|
145
|
+
const [activeUrl, setActiveUrl] = React__default.default.useState(()=>typeof window !== "undefined" ? window.location.href : "");
|
|
146
|
+
const scrollPos = React__default.default.useRef(0);
|
|
147
|
+
const setPositionFixed = React__default.default.useCallback(()=>{
|
|
148
|
+
// All browsers on iOS will return true here.
|
|
149
|
+
if (!isSafari()) return;
|
|
150
|
+
// If previousBodyPosition is already set, don't set it again.
|
|
151
|
+
if (previousBodyPosition === null && isOpen && !noBodyStyles) {
|
|
152
|
+
previousBodyPosition = {
|
|
153
|
+
position: document.body.style.position,
|
|
154
|
+
top: document.body.style.top,
|
|
155
|
+
left: document.body.style.left,
|
|
156
|
+
height: document.body.style.height,
|
|
157
|
+
right: "unset"
|
|
158
|
+
};
|
|
159
|
+
// Update the dom inside an animation frame
|
|
160
|
+
const { scrollX, innerHeight } = window;
|
|
161
|
+
document.body.style.setProperty("position", "fixed", "important");
|
|
162
|
+
Object.assign(document.body.style, {
|
|
163
|
+
top: `${-scrollPos.current}px`,
|
|
164
|
+
left: `${-scrollX}px`,
|
|
165
|
+
right: "0px",
|
|
166
|
+
height: "auto"
|
|
167
|
+
});
|
|
168
|
+
window.setTimeout(()=>window.requestAnimationFrame(()=>{
|
|
169
|
+
// Attempt to check if the bottom bar appeared due to the position change
|
|
170
|
+
const bottomBarHeight = innerHeight - window.innerHeight;
|
|
171
|
+
if (bottomBarHeight && scrollPos.current >= innerHeight) {
|
|
172
|
+
// Move the content further up so that the bottom bar doesn't hide it
|
|
173
|
+
document.body.style.top = `${-(scrollPos.current + bottomBarHeight)}px`;
|
|
174
|
+
}
|
|
175
|
+
}), 300);
|
|
176
|
+
}
|
|
177
|
+
}, [
|
|
178
|
+
isOpen
|
|
179
|
+
]);
|
|
180
|
+
const restorePositionSetting = React__default.default.useCallback(()=>{
|
|
181
|
+
// All browsers on iOS will return true here.
|
|
182
|
+
if (!isSafari()) return;
|
|
183
|
+
if (previousBodyPosition !== null && !noBodyStyles) {
|
|
184
|
+
// Convert the position from "px" to Int
|
|
185
|
+
const y = -parseInt(document.body.style.top, 10);
|
|
186
|
+
const x = -parseInt(document.body.style.left, 10);
|
|
187
|
+
// Restore styles
|
|
188
|
+
Object.assign(document.body.style, previousBodyPosition);
|
|
189
|
+
window.requestAnimationFrame(()=>{
|
|
190
|
+
if (preventScrollRestoration && activeUrl !== window.location.href) {
|
|
191
|
+
setActiveUrl(window.location.href);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
window.scrollTo(x, y);
|
|
195
|
+
});
|
|
196
|
+
previousBodyPosition = null;
|
|
197
|
+
}
|
|
198
|
+
}, [
|
|
199
|
+
activeUrl
|
|
200
|
+
]);
|
|
201
|
+
React__default.default.useEffect(()=>{
|
|
202
|
+
function onScroll() {
|
|
203
|
+
scrollPos.current = window.scrollY;
|
|
204
|
+
}
|
|
205
|
+
onScroll();
|
|
206
|
+
window.addEventListener("scroll", onScroll);
|
|
207
|
+
return ()=>{
|
|
208
|
+
window.removeEventListener("scroll", onScroll);
|
|
209
|
+
};
|
|
210
|
+
}, []);
|
|
211
|
+
React__default.default.useEffect(()=>{
|
|
212
|
+
if (!modal) return;
|
|
213
|
+
return ()=>{
|
|
214
|
+
if (typeof document === "undefined") return;
|
|
215
|
+
// Another drawer is opened, safe to ignore the execution
|
|
216
|
+
const hasDrawerOpened = !!document.querySelector("[data-drawer]");
|
|
217
|
+
if (hasDrawerOpened) return;
|
|
218
|
+
restorePositionSetting();
|
|
219
|
+
};
|
|
220
|
+
}, [
|
|
221
|
+
modal,
|
|
222
|
+
restorePositionSetting
|
|
223
|
+
]);
|
|
224
|
+
React__default.default.useEffect(()=>{
|
|
225
|
+
if (nested || !hasBeenOpened) return;
|
|
226
|
+
// This is needed to force Safari toolbar to show **before** the drawer starts animating to prevent a gnarly shift from happening
|
|
227
|
+
if (isOpen) {
|
|
228
|
+
// avoid for standalone mode (PWA)
|
|
229
|
+
const isStandalone = window.matchMedia("(display-mode: standalone)").matches;
|
|
230
|
+
!isStandalone && setPositionFixed();
|
|
231
|
+
if (!modal) {
|
|
232
|
+
window.setTimeout(()=>{
|
|
233
|
+
restorePositionSetting();
|
|
234
|
+
}, 500);
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
restorePositionSetting();
|
|
238
|
+
}
|
|
239
|
+
}, [
|
|
240
|
+
isOpen,
|
|
241
|
+
hasBeenOpened,
|
|
242
|
+
activeUrl,
|
|
243
|
+
modal,
|
|
244
|
+
nested,
|
|
245
|
+
setPositionFixed,
|
|
246
|
+
restorePositionSetting
|
|
247
|
+
]);
|
|
248
|
+
return {
|
|
249
|
+
restorePositionSetting
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function useSnapPoints({ activeSnapPointProp, setActiveSnapPointProp, snapPoints, drawerRef, overlayRef, fadeFromIndex, onSnapPointChange, direction = "bottom", container, snapToSequentialPoint }) {
|
|
254
|
+
const [activeSnapPoint, setActiveSnapPoint] = reactUseControllableState.useControllableState({
|
|
255
|
+
prop: activeSnapPointProp,
|
|
256
|
+
defaultProp: snapPoints?.[0] ?? null,
|
|
257
|
+
onChange: setActiveSnapPointProp
|
|
258
|
+
});
|
|
259
|
+
const [windowDimensions, setWindowDimensions] = React__default.default.useState(typeof window !== "undefined" ? {
|
|
260
|
+
innerWidth: window.innerWidth,
|
|
261
|
+
innerHeight: window.innerHeight
|
|
262
|
+
} : undefined);
|
|
263
|
+
React__default.default.useEffect(()=>{
|
|
264
|
+
function onResize() {
|
|
265
|
+
setWindowDimensions({
|
|
266
|
+
innerWidth: window.innerWidth,
|
|
267
|
+
innerHeight: window.innerHeight
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
window.addEventListener("resize", onResize);
|
|
271
|
+
return ()=>window.removeEventListener("resize", onResize);
|
|
272
|
+
}, []);
|
|
273
|
+
const isLastSnapPoint = React__default.default.useMemo(()=>activeSnapPoint === snapPoints?.[snapPoints.length - 1] || null, [
|
|
274
|
+
snapPoints,
|
|
275
|
+
activeSnapPoint
|
|
276
|
+
]);
|
|
277
|
+
const activeSnapPointIndex = React__default.default.useMemo(()=>snapPoints?.findIndex((snapPoint)=>snapPoint === activeSnapPoint) ?? null, [
|
|
278
|
+
snapPoints,
|
|
279
|
+
activeSnapPoint
|
|
280
|
+
]);
|
|
281
|
+
const shouldFade = snapPoints && snapPoints.length > 0 && (fadeFromIndex || fadeFromIndex === 0) && !Number.isNaN(fadeFromIndex) && snapPoints[fadeFromIndex] === activeSnapPoint || !snapPoints;
|
|
282
|
+
const snapPointsOffset = React__default.default.useMemo(()=>{
|
|
283
|
+
const containerSize = container ? {
|
|
284
|
+
width: container.getBoundingClientRect().width,
|
|
285
|
+
height: container.getBoundingClientRect().height
|
|
286
|
+
} : typeof window !== "undefined" ? {
|
|
287
|
+
width: window.innerWidth,
|
|
288
|
+
height: window.innerHeight
|
|
289
|
+
} : {
|
|
290
|
+
width: 0,
|
|
291
|
+
height: 0
|
|
292
|
+
};
|
|
293
|
+
return snapPoints?.map((snapPoint)=>{
|
|
294
|
+
// FIXME
|
|
295
|
+
// 1 -> container 100% << expected
|
|
296
|
+
// 0.5 -> container 50% << expected
|
|
297
|
+
// 300px -> 300 -> 300px << expected
|
|
298
|
+
// 15rem -> 15 -> 15px << this makes no sense, should fix or disallow
|
|
299
|
+
const isPx = typeof snapPoint === "string";
|
|
300
|
+
let snapPointAsNumber = 0;
|
|
301
|
+
if (isPx) {
|
|
302
|
+
snapPointAsNumber = Number.parseInt(snapPoint, 10);
|
|
303
|
+
}
|
|
304
|
+
if (isVertical(direction)) {
|
|
305
|
+
const height = isPx ? snapPointAsNumber : windowDimensions ? snapPoint * containerSize.height : 0;
|
|
306
|
+
if (windowDimensions) {
|
|
307
|
+
return direction === "bottom" ? containerSize.height - height : -containerSize.height + height;
|
|
308
|
+
}
|
|
309
|
+
return height;
|
|
310
|
+
}
|
|
311
|
+
const width = isPx ? snapPointAsNumber : windowDimensions ? snapPoint * containerSize.width : 0;
|
|
312
|
+
if (windowDimensions) {
|
|
313
|
+
return direction === "right" ? containerSize.width - width : -containerSize.width + width;
|
|
314
|
+
}
|
|
315
|
+
return width;
|
|
316
|
+
}) ?? [];
|
|
317
|
+
}, [
|
|
318
|
+
snapPoints,
|
|
319
|
+
windowDimensions,
|
|
320
|
+
container,
|
|
321
|
+
direction
|
|
322
|
+
]);
|
|
323
|
+
const activeSnapPointOffset = React__default.default.useMemo(()=>activeSnapPointIndex !== null ? snapPointsOffset?.[activeSnapPointIndex] : null, [
|
|
324
|
+
snapPointsOffset,
|
|
325
|
+
activeSnapPointIndex
|
|
326
|
+
]);
|
|
327
|
+
const snapToPoint = React__default.default.useCallback((dimension)=>{
|
|
328
|
+
const newSnapPointIndex = snapPointsOffset?.findIndex((snapPointDim)=>snapPointDim === dimension) ?? null;
|
|
329
|
+
onSnapPointChange(newSnapPointIndex);
|
|
330
|
+
set(drawerRef.current, {
|
|
331
|
+
transition: `transform ${TRANSITIONS.ENTER_DURATION}s ${TRANSITIONS.CONTENT_ENTER_TIMING_FUNCTION}`,
|
|
332
|
+
transform: isVertical(direction) ? `translate3d(0, ${dimension}px, 0)` : `translate3d(${dimension}px, 0, 0)`
|
|
333
|
+
});
|
|
334
|
+
if (snapPointsOffset && newSnapPointIndex !== snapPointsOffset.length - 1 && fadeFromIndex !== undefined && newSnapPointIndex !== fadeFromIndex && newSnapPointIndex < fadeFromIndex) {
|
|
335
|
+
if (fadeFromIndex !== 0) {
|
|
336
|
+
set(overlayRef.current, {
|
|
337
|
+
transition: `opacity ${TRANSITIONS.EXIT_DURATION}s ${TRANSITIONS.OVERLAY_EXIT_TIMING_FUNCTION}`,
|
|
338
|
+
opacity: "0"
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
set(overlayRef.current, {
|
|
343
|
+
transition: `opacity ${TRANSITIONS.ENTER_DURATION}s ${TRANSITIONS.OVERLAY_ENTER_TIMING_FUNCTION}`,
|
|
344
|
+
opacity: "1"
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
setActiveSnapPoint(snapPoints?.[Math.max(newSnapPointIndex, 0)]);
|
|
348
|
+
}, [
|
|
349
|
+
drawerRef,
|
|
350
|
+
overlayRef,
|
|
351
|
+
snapPoints,
|
|
352
|
+
snapPointsOffset,
|
|
353
|
+
fadeFromIndex,
|
|
354
|
+
direction,
|
|
355
|
+
onSnapPointChange,
|
|
356
|
+
setActiveSnapPoint
|
|
357
|
+
]);
|
|
358
|
+
React__default.default.useEffect(()=>{
|
|
359
|
+
if (activeSnapPoint || activeSnapPointProp) {
|
|
360
|
+
const newIndex = snapPoints?.findIndex((snapPoint)=>snapPoint === activeSnapPointProp || snapPoint === activeSnapPoint) ?? -1;
|
|
361
|
+
if (snapPointsOffset && newIndex !== -1 && typeof snapPointsOffset[newIndex] === "number") {
|
|
362
|
+
snapToPoint(snapPointsOffset[newIndex]);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}, [
|
|
366
|
+
activeSnapPoint,
|
|
367
|
+
activeSnapPointProp,
|
|
368
|
+
snapPoints,
|
|
369
|
+
snapPointsOffset,
|
|
370
|
+
snapToPoint
|
|
371
|
+
]);
|
|
372
|
+
function onRelease({ draggedDistance, closeDrawer, velocity, dismissible, event }) {
|
|
373
|
+
if (fadeFromIndex === undefined) return;
|
|
374
|
+
const currentPosition = direction === "bottom" || direction === "right" ? (activeSnapPointOffset ?? 0) - draggedDistance : (activeSnapPointOffset ?? 0) + draggedDistance;
|
|
375
|
+
const isOverlaySnapPoint = activeSnapPointIndex === fadeFromIndex - 1;
|
|
376
|
+
const isFirst = activeSnapPointIndex === 0;
|
|
377
|
+
const hasDraggedUp = draggedDistance > 0;
|
|
378
|
+
if (isOverlaySnapPoint) {
|
|
379
|
+
set(overlayRef.current, {
|
|
380
|
+
transition: `opacity ${TRANSITIONS.EXIT_DURATION}s ${TRANSITIONS.OVERLAY_EXIT_TIMING_FUNCTION}`
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
if (!snapToSequentialPoint && velocity > 2 && !hasDraggedUp) {
|
|
384
|
+
if (dismissible) closeDrawer(false, {
|
|
385
|
+
reason: "drag",
|
|
386
|
+
event
|
|
387
|
+
});
|
|
388
|
+
else snapToPoint(snapPointsOffset[0]); // snap to initial point
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (!snapToSequentialPoint && velocity > 2 && hasDraggedUp && snapPointsOffset && snapPoints) {
|
|
392
|
+
snapToPoint(snapPointsOffset[snapPoints.length - 1]);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
// Find the closest snap point to the current position
|
|
396
|
+
const closestSnapPoint = snapPointsOffset?.reduce((prev, curr)=>{
|
|
397
|
+
if (typeof prev !== "number" || typeof curr !== "number") return prev;
|
|
398
|
+
return Math.abs(curr - currentPosition) < Math.abs(prev - currentPosition) ? curr : prev;
|
|
399
|
+
});
|
|
400
|
+
const dim = isVertical(direction) ? window.innerHeight : window.innerWidth;
|
|
401
|
+
if (velocity > VELOCITY_THRESHOLD && Math.abs(draggedDistance) < dim * 0.4) {
|
|
402
|
+
const dragDirection = hasDraggedUp ? 1 : -1; // 1 = up, -1 = down
|
|
403
|
+
// Don't do anything if we swipe upwards while being on the last snap point
|
|
404
|
+
if (dragDirection > 0 && isLastSnapPoint && snapPoints) {
|
|
405
|
+
snapToPoint(snapPointsOffset[snapPoints.length - 1]);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (isFirst && dragDirection < 0 && dismissible) {
|
|
409
|
+
closeDrawer(false, {
|
|
410
|
+
reason: "drag",
|
|
411
|
+
event
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
if (activeSnapPointIndex === null) return;
|
|
415
|
+
snapToPoint(snapPointsOffset[activeSnapPointIndex + dragDirection]);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
snapToPoint(closestSnapPoint);
|
|
419
|
+
}
|
|
420
|
+
function onDrag({ draggedDistance }) {
|
|
421
|
+
if (activeSnapPointOffset === null) return;
|
|
422
|
+
const newValue = direction === "bottom" || direction === "right" ? activeSnapPointOffset - draggedDistance : activeSnapPointOffset + draggedDistance;
|
|
423
|
+
// Don't do anything if we exceed the last(biggest) snap point
|
|
424
|
+
if ((direction === "bottom" || direction === "right") && newValue < snapPointsOffset[snapPointsOffset.length - 1]) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if ((direction === "top" || direction === "left") && newValue > snapPointsOffset[snapPointsOffset.length - 1]) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
set(drawerRef.current, {
|
|
431
|
+
transform: isVertical(direction) ? `translate3d(0, ${newValue}px, 0)` : `translate3d(${newValue}px, 0, 0)`
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
function getPercentageDragged(absDraggedDistance, isDraggingDown) {
|
|
435
|
+
if (!snapPoints || typeof activeSnapPointIndex !== "number" || !snapPointsOffset || fadeFromIndex === undefined) return null;
|
|
436
|
+
// If this is true we are dragging to a snap point that is supposed to have an overlay
|
|
437
|
+
const isOverlaySnapPoint = activeSnapPointIndex === fadeFromIndex - 1;
|
|
438
|
+
const isOverlaySnapPointOrHigher = activeSnapPointIndex >= fadeFromIndex;
|
|
439
|
+
if (isOverlaySnapPointOrHigher && isDraggingDown) {
|
|
440
|
+
return 0;
|
|
441
|
+
}
|
|
442
|
+
// Don't animate, but still use this one if we are dragging away from the overlaySnapPoint
|
|
443
|
+
if (isOverlaySnapPoint && !isDraggingDown) return 1;
|
|
444
|
+
if (!shouldFade && !isOverlaySnapPoint) return null;
|
|
445
|
+
// Either fadeFrom index or the one before
|
|
446
|
+
const targetSnapPointIndex = isOverlaySnapPoint ? activeSnapPointIndex + 1 : activeSnapPointIndex - 1;
|
|
447
|
+
if (fadeFromIndex === 0 && activeSnapPointIndex === 0) {
|
|
448
|
+
let firstSnapPoint = snapPoints[0];
|
|
449
|
+
if (typeof firstSnapPoint === "string") {
|
|
450
|
+
firstSnapPoint = Number.parseInt(firstSnapPoint, 10);
|
|
451
|
+
}
|
|
452
|
+
const snapPointDistance = firstSnapPoint;
|
|
453
|
+
const percentageDragged = absDraggedDistance / Math.abs(snapPointDistance);
|
|
454
|
+
return percentageDragged;
|
|
455
|
+
}
|
|
456
|
+
// Get the distance from overlaySnapPoint to the one before or vice-versa to calculate the opacity percentage accordingly
|
|
457
|
+
const snapPointDistance = isOverlaySnapPoint ? snapPointsOffset[targetSnapPointIndex] - snapPointsOffset[targetSnapPointIndex - 1] : snapPointsOffset[targetSnapPointIndex + 1] - snapPointsOffset[targetSnapPointIndex];
|
|
458
|
+
const percentageDragged = absDraggedDistance / Math.abs(snapPointDistance);
|
|
459
|
+
if (isOverlaySnapPoint) {
|
|
460
|
+
return 1 - percentageDragged;
|
|
461
|
+
}
|
|
462
|
+
return percentageDragged;
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
isLastSnapPoint,
|
|
466
|
+
activeSnapPoint,
|
|
467
|
+
shouldFade,
|
|
468
|
+
getPercentageDragged,
|
|
469
|
+
setActiveSnapPoint,
|
|
470
|
+
activeSnapPointIndex,
|
|
471
|
+
onRelease,
|
|
472
|
+
onDrag,
|
|
473
|
+
snapPointsOffset
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function useDrawer(props) {
|
|
478
|
+
const { open: openProp, onOpenChange, onDrag: onDragProp, onRelease: onReleaseProp, snapPoints, closeThreshold = CLOSE_THRESHOLD, scrollLockTimeout = SCROLL_LOCK_TIMEOUT, dismissible = true, handleOnly = false, fadeFromIndex = snapPoints && snapPoints.length - 1, activeSnapPoint: activeSnapPointProp, setActiveSnapPoint: setActiveSnapPointProp, fixed, modal = true, onClose, nested, noBodyStyles = true, direction = "bottom", defaultOpen = false, snapToSequentialPoint = false, preventScrollRestoration = false, repositionInputs = true, onAnimationEnd, container, autoFocus = true, closeOnInteractOutside = true, closeOnEscape = true, lazyMount: lazyMountProp = false, unmountOnExit: unmountOnExitProp = false } = props;
|
|
479
|
+
const drawerId = React.useId();
|
|
480
|
+
const titleId = `${drawerId}-title`;
|
|
481
|
+
const descriptionId = `${drawerId}-description`;
|
|
482
|
+
const [isOpen = false, setIsOpen] = reactUseControllableState.useControllableState({
|
|
483
|
+
defaultProp: defaultOpen,
|
|
484
|
+
prop: openProp,
|
|
485
|
+
onChange: (o, details)=>{
|
|
486
|
+
onOpenChange?.(o, details);
|
|
487
|
+
if (!o && !nested) {
|
|
488
|
+
restorePositionSetting();
|
|
489
|
+
}
|
|
490
|
+
setTimeout(()=>{
|
|
491
|
+
onAnimationEnd?.(o);
|
|
492
|
+
}, TRANSITIONS.EXIT_DURATION * 1000);
|
|
493
|
+
if (o && !modal) {
|
|
494
|
+
if (typeof window !== "undefined") {
|
|
495
|
+
window.requestAnimationFrame(()=>{
|
|
496
|
+
document.body.style.pointerEvents = "auto";
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (!o) {
|
|
501
|
+
document.body.style.pointerEvents = "auto";
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
const [hasBeenOpened, setHasBeenOpened] = React.useState(false);
|
|
506
|
+
const [hasAnimationDone, setHasAnimationDone] = React.useState(false);
|
|
507
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
508
|
+
const [shouldOverlayAnimate, setShouldOverlayAnimate] = React.useState(false);
|
|
509
|
+
const [isCloseButtonRendered, setIsCloseButtonRendered] = React.useState(false);
|
|
510
|
+
const closeButtonRef = React.useCallback((node)=>{
|
|
511
|
+
setIsCloseButtonRendered(!!node);
|
|
512
|
+
}, []);
|
|
513
|
+
const overlayRef = React.useRef(null);
|
|
514
|
+
const openTime = React.useRef(null);
|
|
515
|
+
const dragStartTime = React.useRef(null);
|
|
516
|
+
const dragEndTime = React.useRef(null);
|
|
517
|
+
const lastTimeDragPrevented = React.useRef(null);
|
|
518
|
+
const isAllowedToDrag = React.useRef(false);
|
|
519
|
+
const pointerStart = React.useRef(0);
|
|
520
|
+
const keyboardIsOpen = React.useRef(false);
|
|
521
|
+
const previousDiffFromInitial = React.useRef(0);
|
|
522
|
+
const drawerRef = React.useRef(null);
|
|
523
|
+
const drawerHeightRef = React.useRef(drawerRef.current?.getBoundingClientRect().height || 0);
|
|
524
|
+
const drawerWidthRef = React.useRef(drawerRef.current?.getBoundingClientRect().width || 0);
|
|
525
|
+
const initialDrawerHeight = React.useRef(0);
|
|
526
|
+
const onSnapPointChange = React.useCallback((activeSnapPointIndex)=>{
|
|
527
|
+
if (snapPoints && activeSnapPointIndex === snapPointsOffset.length - 1) {
|
|
528
|
+
openTime.current = new Date();
|
|
529
|
+
}
|
|
530
|
+
}, [
|
|
531
|
+
snapPoints
|
|
532
|
+
]);
|
|
533
|
+
const { activeSnapPoint, activeSnapPointIndex, setActiveSnapPoint, onRelease: onReleaseSnapPoints, snapPointsOffset, onDrag: onDragSnapPoints, shouldFade, getPercentageDragged: getSnapPointsPercentageDragged } = useSnapPoints({
|
|
534
|
+
snapPoints,
|
|
535
|
+
activeSnapPointProp,
|
|
536
|
+
setActiveSnapPointProp,
|
|
537
|
+
drawerRef,
|
|
538
|
+
fadeFromIndex,
|
|
539
|
+
overlayRef,
|
|
540
|
+
onSnapPointChange,
|
|
541
|
+
direction,
|
|
542
|
+
snapToSequentialPoint
|
|
543
|
+
});
|
|
544
|
+
const { restorePositionSetting } = usePositionFixed({
|
|
545
|
+
isOpen,
|
|
546
|
+
modal,
|
|
547
|
+
nested: nested ?? false,
|
|
548
|
+
hasBeenOpened,
|
|
549
|
+
preventScrollRestoration,
|
|
550
|
+
noBodyStyles
|
|
551
|
+
});
|
|
552
|
+
function onPress(event) {
|
|
553
|
+
if (!dismissible && !snapPoints) return;
|
|
554
|
+
if (drawerRef.current && !drawerRef.current.contains(event.target)) return;
|
|
555
|
+
drawerHeightRef.current = drawerRef.current?.getBoundingClientRect().height || 0;
|
|
556
|
+
drawerWidthRef.current = drawerRef.current?.getBoundingClientRect().width || 0;
|
|
557
|
+
setIsDragging(true);
|
|
558
|
+
dragStartTime.current = new Date();
|
|
559
|
+
if (isIOS()) {
|
|
560
|
+
window.addEventListener("touchend", ()=>{
|
|
561
|
+
isAllowedToDrag.current = false;
|
|
562
|
+
}, {
|
|
563
|
+
once: true
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
event.target.setPointerCapture(event.pointerId);
|
|
567
|
+
pointerStart.current = isVertical(direction) ? event.pageY : event.pageX;
|
|
568
|
+
}
|
|
569
|
+
function shouldDrag(el, isDraggingInDirection) {
|
|
570
|
+
let element = el;
|
|
571
|
+
const highlightedText = window.getSelection()?.toString();
|
|
572
|
+
const swipeAmount = drawerRef.current ? getTranslate(drawerRef.current, direction) : null;
|
|
573
|
+
const date = new Date();
|
|
574
|
+
if (element.tagName === "SELECT") {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
if (element.hasAttribute("data-no-drag") || element.closest("[data-no-drag]")) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
if (direction === "right" || direction === "left") {
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
if (openTime.current && date.getTime() - openTime.current.getTime() < 500) {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
if (swipeAmount !== null) {
|
|
587
|
+
if (direction === "bottom" ? swipeAmount > 0 : swipeAmount < 0) {
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (highlightedText && highlightedText.length > 0) {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
if (lastTimeDragPrevented.current && date.getTime() - lastTimeDragPrevented.current.getTime() < scrollLockTimeout && swipeAmount === 0) {
|
|
595
|
+
lastTimeDragPrevented.current = date;
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
if (isDraggingInDirection) {
|
|
599
|
+
lastTimeDragPrevented.current = date;
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
while(element){
|
|
603
|
+
if (element.scrollHeight > element.clientHeight) {
|
|
604
|
+
if (element.scrollTop !== 0) {
|
|
605
|
+
lastTimeDragPrevented.current = new Date();
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
if (element.getAttribute("role") === "dialog") {
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
element = element.parentNode;
|
|
613
|
+
}
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
function onDrag(event) {
|
|
617
|
+
if (!drawerRef.current) return;
|
|
618
|
+
if (isDragging) {
|
|
619
|
+
const directionMultiplier = direction === "bottom" || direction === "right" ? 1 : -1;
|
|
620
|
+
const draggedDistance = (pointerStart.current - (isVertical(direction) ? event.pageY : event.pageX)) * directionMultiplier;
|
|
621
|
+
const isDraggingInDirection = draggedDistance > 0;
|
|
622
|
+
const noCloseSnapPointsPreCondition = snapPoints && !dismissible && !isDraggingInDirection;
|
|
623
|
+
if (noCloseSnapPointsPreCondition && activeSnapPointIndex === 0) return;
|
|
624
|
+
const absDraggedDistance = Math.abs(draggedDistance);
|
|
625
|
+
const drawerDimension = direction === "bottom" || direction === "top" ? drawerHeightRef.current : drawerWidthRef.current;
|
|
626
|
+
let percentageDragged = absDraggedDistance / drawerDimension;
|
|
627
|
+
const snapPointPercentageDragged = getSnapPointsPercentageDragged(absDraggedDistance, isDraggingInDirection);
|
|
628
|
+
if (snapPointPercentageDragged !== null) {
|
|
629
|
+
percentageDragged = snapPointPercentageDragged;
|
|
630
|
+
}
|
|
631
|
+
if (noCloseSnapPointsPreCondition && percentageDragged >= 1) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (!isAllowedToDrag.current && !shouldDrag(event.target, isDraggingInDirection)) return;
|
|
635
|
+
drawerRef.current.classList.add(DRAG_CLASS);
|
|
636
|
+
isAllowedToDrag.current = true;
|
|
637
|
+
set(drawerRef.current, {
|
|
638
|
+
transition: "none"
|
|
639
|
+
});
|
|
640
|
+
set(overlayRef.current, {
|
|
641
|
+
transition: "none"
|
|
642
|
+
});
|
|
643
|
+
if (snapPoints) {
|
|
644
|
+
onDragSnapPoints({
|
|
645
|
+
draggedDistance
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
if (isDraggingInDirection && !snapPoints) {
|
|
649
|
+
const dampenedDraggedDistance = dampenValue(draggedDistance);
|
|
650
|
+
const translateValue = Math.min(dampenedDraggedDistance * -1, 0) * directionMultiplier;
|
|
651
|
+
set(drawerRef.current, {
|
|
652
|
+
transform: isVertical(direction) ? `translate3d(0, ${translateValue}px, 0)` : `translate3d(${translateValue}px, 0, 0)`
|
|
653
|
+
});
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const opacityValue = 1 - percentageDragged;
|
|
657
|
+
if (shouldFade || fadeFromIndex && activeSnapPointIndex === fadeFromIndex - 1) {
|
|
658
|
+
onDragProp?.(event, percentageDragged);
|
|
659
|
+
set(overlayRef.current, {
|
|
660
|
+
opacity: `${opacityValue}`,
|
|
661
|
+
transition: "none"
|
|
662
|
+
}, true);
|
|
663
|
+
}
|
|
664
|
+
if (!snapPoints) {
|
|
665
|
+
const translateValue = absDraggedDistance * directionMultiplier;
|
|
666
|
+
set(drawerRef.current, {
|
|
667
|
+
transform: isVertical(direction) ? `translate3d(0, ${translateValue}px, 0)` : `translate3d(${translateValue}px, 0, 0)`
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const cancelDrag = React.useCallback(()=>{
|
|
673
|
+
if (!isDragging || !drawerRef.current) return;
|
|
674
|
+
drawerRef.current.classList.remove(DRAG_CLASS);
|
|
675
|
+
isAllowedToDrag.current = false;
|
|
676
|
+
setIsDragging(false);
|
|
677
|
+
dragEndTime.current = new Date();
|
|
678
|
+
}, [
|
|
679
|
+
isDragging
|
|
680
|
+
]);
|
|
681
|
+
const closeDrawer = React.useCallback((fromWithin, details)=>{
|
|
682
|
+
cancelDrag();
|
|
683
|
+
onClose?.();
|
|
684
|
+
if (!fromWithin) {
|
|
685
|
+
setIsOpen(false, details);
|
|
686
|
+
}
|
|
687
|
+
if (fadeFromIndex !== undefined && fadeFromIndex > 0 && activeSnapPointIndex === 0) {
|
|
688
|
+
set(overlayRef.current, {
|
|
689
|
+
opacity: "0"
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
setTimeout(()=>{
|
|
693
|
+
if (snapPoints) {
|
|
694
|
+
setActiveSnapPoint(snapPoints[0]);
|
|
695
|
+
}
|
|
696
|
+
}, TRANSITIONS.EXIT_DURATION * 1000);
|
|
697
|
+
}, [
|
|
698
|
+
cancelDrag,
|
|
699
|
+
onClose,
|
|
700
|
+
snapPoints,
|
|
701
|
+
setActiveSnapPoint,
|
|
702
|
+
setIsOpen,
|
|
703
|
+
fadeFromIndex,
|
|
704
|
+
activeSnapPointIndex
|
|
705
|
+
]);
|
|
706
|
+
function resetDrawer() {
|
|
707
|
+
if (!drawerRef.current) return;
|
|
708
|
+
set(drawerRef.current, {
|
|
709
|
+
transform: "translate3d(0, 0, 0)",
|
|
710
|
+
transition: `transform ${TRANSITIONS.EXIT_DURATION}s ${TRANSITIONS.CONTENT_EXIT_TIMING_FUNCTION}`
|
|
711
|
+
});
|
|
712
|
+
set(overlayRef.current, {
|
|
713
|
+
transition: `opacity ${TRANSITIONS.EXIT_DURATION}s ${TRANSITIONS.OVERLAY_EXIT_TIMING_FUNCTION}`,
|
|
714
|
+
opacity: "1"
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
function onRelease(event) {
|
|
718
|
+
if (!isDragging || !drawerRef.current) return;
|
|
719
|
+
drawerRef.current.classList.remove(DRAG_CLASS);
|
|
720
|
+
isAllowedToDrag.current = false;
|
|
721
|
+
setIsDragging(false);
|
|
722
|
+
dragEndTime.current = new Date();
|
|
723
|
+
const swipeAmount = getTranslate(drawerRef.current, direction);
|
|
724
|
+
if (!event || !shouldDrag(event.target, false) || !swipeAmount || Number.isNaN(swipeAmount)) return;
|
|
725
|
+
if (dragStartTime.current === null) return;
|
|
726
|
+
const timeTaken = dragEndTime.current.getTime() - dragStartTime.current.getTime();
|
|
727
|
+
const distMoved = pointerStart.current - (isVertical(direction) ? event.pageY : event.pageX);
|
|
728
|
+
const velocity = Math.abs(distMoved) / timeTaken;
|
|
729
|
+
if (snapPoints) {
|
|
730
|
+
const directionMultiplier = direction === "bottom" || direction === "right" ? 1 : -1;
|
|
731
|
+
onReleaseSnapPoints({
|
|
732
|
+
draggedDistance: distMoved * directionMultiplier,
|
|
733
|
+
closeDrawer,
|
|
734
|
+
velocity,
|
|
735
|
+
dismissible,
|
|
736
|
+
event: event.nativeEvent
|
|
737
|
+
});
|
|
738
|
+
onReleaseProp?.(event, true);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (direction === "bottom" || direction === "right" ? distMoved > 0 : distMoved < 0) {
|
|
742
|
+
resetDrawer();
|
|
743
|
+
onReleaseProp?.(event, true);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (velocity > VELOCITY_THRESHOLD) {
|
|
747
|
+
closeDrawer(false, {
|
|
748
|
+
reason: "drag",
|
|
749
|
+
event: event.nativeEvent
|
|
750
|
+
});
|
|
751
|
+
onReleaseProp?.(event, false);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const visibleDrawerHeight = Math.min(drawerRef.current.getBoundingClientRect().height ?? 0, window.innerHeight);
|
|
755
|
+
const visibleDrawerWidth = Math.min(drawerRef.current.getBoundingClientRect().width ?? 0, window.innerWidth);
|
|
756
|
+
const isHorizontalSwipe = direction === "left" || direction === "right";
|
|
757
|
+
if (Math.abs(swipeAmount) >= (isHorizontalSwipe ? visibleDrawerWidth : visibleDrawerHeight) * closeThreshold) {
|
|
758
|
+
closeDrawer(false, {
|
|
759
|
+
reason: "drag",
|
|
760
|
+
event: event.nativeEvent
|
|
761
|
+
});
|
|
762
|
+
onReleaseProp?.(event, false);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
onReleaseProp?.(event, true);
|
|
766
|
+
resetDrawer();
|
|
767
|
+
}
|
|
768
|
+
React.useEffect(()=>{
|
|
769
|
+
if (isOpen) {
|
|
770
|
+
set(document.documentElement, {
|
|
771
|
+
scrollBehavior: "auto"
|
|
772
|
+
});
|
|
773
|
+
openTime.current = new Date();
|
|
774
|
+
}
|
|
775
|
+
return ()=>{
|
|
776
|
+
reset(document.documentElement, "scrollBehavior");
|
|
777
|
+
};
|
|
778
|
+
}, [
|
|
779
|
+
isOpen
|
|
780
|
+
]);
|
|
781
|
+
React.useEffect(()=>{
|
|
782
|
+
function onVisualViewportChange() {
|
|
783
|
+
if (!drawerRef.current || !repositionInputs) return;
|
|
784
|
+
const focusedElement = document.activeElement;
|
|
785
|
+
if (isInput(focusedElement) || keyboardIsOpen.current) {
|
|
786
|
+
const visualViewportHeight = window.visualViewport?.height || 0;
|
|
787
|
+
const totalHeight = window.innerHeight;
|
|
788
|
+
let diffFromInitial = totalHeight - visualViewportHeight;
|
|
789
|
+
const drawerHeight = drawerRef.current.getBoundingClientRect().height || 0;
|
|
790
|
+
const isTallEnough = drawerHeight > totalHeight * 0.8;
|
|
791
|
+
if (!initialDrawerHeight.current) {
|
|
792
|
+
initialDrawerHeight.current = drawerHeight;
|
|
793
|
+
}
|
|
794
|
+
const offsetFromTop = drawerRef.current.getBoundingClientRect().top;
|
|
795
|
+
if (Math.abs(previousDiffFromInitial.current - diffFromInitial) > 60) {
|
|
796
|
+
keyboardIsOpen.current = !keyboardIsOpen.current;
|
|
797
|
+
}
|
|
798
|
+
if (snapPoints && snapPoints.length > 0 && snapPointsOffset && activeSnapPointIndex) {
|
|
799
|
+
const activeSnapPointHeight = snapPointsOffset[activeSnapPointIndex] || 0;
|
|
800
|
+
diffFromInitial += activeSnapPointHeight;
|
|
801
|
+
}
|
|
802
|
+
previousDiffFromInitial.current = diffFromInitial;
|
|
803
|
+
if (drawerHeight > visualViewportHeight || keyboardIsOpen.current) {
|
|
804
|
+
const height = drawerRef.current.getBoundingClientRect().height;
|
|
805
|
+
let newDrawerHeight = height;
|
|
806
|
+
if (height > visualViewportHeight) {
|
|
807
|
+
newDrawerHeight = visualViewportHeight - (isTallEnough ? offsetFromTop : WINDOW_TOP_OFFSET);
|
|
808
|
+
}
|
|
809
|
+
if (fixed) {
|
|
810
|
+
drawerRef.current.style.height = `${height - Math.max(diffFromInitial, 0)}px`;
|
|
811
|
+
} else {
|
|
812
|
+
drawerRef.current.style.height = `${Math.max(newDrawerHeight, visualViewportHeight - offsetFromTop)}px`;
|
|
813
|
+
}
|
|
814
|
+
} else if (!isMobileFirefox() && !isAndroid()) {
|
|
815
|
+
drawerRef.current.style.height = `${initialDrawerHeight.current}px`;
|
|
816
|
+
}
|
|
817
|
+
if (snapPoints && snapPoints.length > 0 && !keyboardIsOpen.current) {
|
|
818
|
+
drawerRef.current.style.bottom = "0px";
|
|
819
|
+
} else {
|
|
820
|
+
drawerRef.current.style.bottom = `${Math.max(diffFromInitial, 0)}px`;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
window.visualViewport?.addEventListener("resize", onVisualViewportChange);
|
|
825
|
+
return ()=>window.visualViewport?.removeEventListener("resize", onVisualViewportChange);
|
|
826
|
+
}, [
|
|
827
|
+
activeSnapPointIndex,
|
|
828
|
+
snapPoints,
|
|
829
|
+
snapPointsOffset,
|
|
830
|
+
repositionInputs,
|
|
831
|
+
fixed
|
|
832
|
+
]);
|
|
833
|
+
React.useEffect(()=>{
|
|
834
|
+
if (!modal) {
|
|
835
|
+
window.requestAnimationFrame(()=>{
|
|
836
|
+
document.body.style.pointerEvents = "auto";
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
}, [
|
|
840
|
+
modal
|
|
841
|
+
]);
|
|
842
|
+
// Effect 1: Track drawer open state
|
|
843
|
+
React.useEffect(()=>{
|
|
844
|
+
if (isOpen) {
|
|
845
|
+
setHasBeenOpened(true);
|
|
846
|
+
}
|
|
847
|
+
}, [
|
|
848
|
+
isOpen
|
|
849
|
+
]);
|
|
850
|
+
// Effect 2: Handle animation state and timer
|
|
851
|
+
React.useEffect(()=>{
|
|
852
|
+
if (isOpen) {
|
|
853
|
+
// Only reset animation state if this is the first open
|
|
854
|
+
if (!hasBeenOpened) {
|
|
855
|
+
setHasAnimationDone(false);
|
|
856
|
+
}
|
|
857
|
+
const timeoutId = setTimeout(()=>{
|
|
858
|
+
setHasAnimationDone(true);
|
|
859
|
+
}, TRANSITIONS.ENTER_DURATION * 1000);
|
|
860
|
+
return ()=>clearTimeout(timeoutId);
|
|
861
|
+
}
|
|
862
|
+
// Reset animation state when drawer closes
|
|
863
|
+
setHasAnimationDone(false);
|
|
864
|
+
}, [
|
|
865
|
+
isOpen,
|
|
866
|
+
hasBeenOpened
|
|
867
|
+
]);
|
|
868
|
+
React.useEffect(()=>{
|
|
869
|
+
if (isOpen && snapPoints && fadeFromIndex === 0) {
|
|
870
|
+
setShouldOverlayAnimate(true);
|
|
871
|
+
const timeoutId = setTimeout(()=>{
|
|
872
|
+
setShouldOverlayAnimate(false);
|
|
873
|
+
}, TRANSITIONS.ENTER_DURATION * 1000);
|
|
874
|
+
return ()=>clearTimeout(timeoutId);
|
|
875
|
+
}
|
|
876
|
+
setShouldOverlayAnimate(false);
|
|
877
|
+
}, [
|
|
878
|
+
isOpen,
|
|
879
|
+
snapPoints,
|
|
880
|
+
fadeFromIndex
|
|
881
|
+
]);
|
|
882
|
+
const stateProps = React.useMemo(()=>domUtils.elementProps({
|
|
883
|
+
"data-open": domUtils.dataAttr(isOpen)
|
|
884
|
+
}), [
|
|
885
|
+
isOpen
|
|
886
|
+
]);
|
|
887
|
+
return React.useMemo(()=>({
|
|
888
|
+
activeSnapPoint,
|
|
889
|
+
snapPoints,
|
|
890
|
+
setActiveSnapPoint,
|
|
891
|
+
drawerRef,
|
|
892
|
+
overlayRef,
|
|
893
|
+
shouldOverlayAnimate,
|
|
894
|
+
onOpenChange,
|
|
895
|
+
onPress,
|
|
896
|
+
onRelease,
|
|
897
|
+
onDrag,
|
|
898
|
+
dismissible,
|
|
899
|
+
handleOnly,
|
|
900
|
+
isOpen,
|
|
901
|
+
isDragging,
|
|
902
|
+
shouldFade,
|
|
903
|
+
closeDrawer,
|
|
904
|
+
keyboardIsOpen,
|
|
905
|
+
modal,
|
|
906
|
+
snapPointsOffset,
|
|
907
|
+
activeSnapPointIndex,
|
|
908
|
+
direction,
|
|
909
|
+
noBodyStyles,
|
|
910
|
+
container,
|
|
911
|
+
autoFocus,
|
|
912
|
+
setHasBeenOpened,
|
|
913
|
+
setIsOpen,
|
|
914
|
+
closeOnInteractOutside,
|
|
915
|
+
closeOnEscape,
|
|
916
|
+
titleId,
|
|
917
|
+
descriptionId,
|
|
918
|
+
lazyMount: lazyMountProp,
|
|
919
|
+
unmountOnExit: unmountOnExitProp,
|
|
920
|
+
hasAnimationDone,
|
|
921
|
+
closeButtonRef,
|
|
922
|
+
isCloseButtonRendered,
|
|
923
|
+
triggerProps: domUtils.buttonProps({
|
|
924
|
+
...stateProps,
|
|
925
|
+
onClick: (e)=>{
|
|
926
|
+
if (e.defaultPrevented) return;
|
|
927
|
+
setIsOpen(true);
|
|
928
|
+
}
|
|
929
|
+
}),
|
|
930
|
+
positionerProps: domUtils.elementProps({
|
|
931
|
+
...stateProps,
|
|
932
|
+
style: {
|
|
933
|
+
pointerEvents: isOpen && modal ? undefined : "none"
|
|
934
|
+
}
|
|
935
|
+
}),
|
|
936
|
+
backdropProps: domUtils.elementProps({
|
|
937
|
+
...stateProps
|
|
938
|
+
}),
|
|
939
|
+
titleProps: domUtils.elementProps({
|
|
940
|
+
id: titleId,
|
|
941
|
+
...stateProps
|
|
942
|
+
}),
|
|
943
|
+
descriptionProps: domUtils.elementProps({
|
|
944
|
+
id: descriptionId,
|
|
945
|
+
...stateProps
|
|
946
|
+
}),
|
|
947
|
+
headerProps: domUtils.elementProps({
|
|
948
|
+
"data-show-close-button": domUtils.dataAttr(isCloseButtonRendered),
|
|
949
|
+
...stateProps
|
|
950
|
+
}),
|
|
951
|
+
closeButtonProps: domUtils.buttonProps({
|
|
952
|
+
...stateProps,
|
|
953
|
+
onClick: (e)=>{
|
|
954
|
+
if (e.defaultPrevented) return;
|
|
955
|
+
setIsOpen(false, {
|
|
956
|
+
reason: "closeButton",
|
|
957
|
+
event: e.nativeEvent
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
})
|
|
961
|
+
}), [
|
|
962
|
+
activeSnapPoint,
|
|
963
|
+
snapPoints,
|
|
964
|
+
setActiveSnapPoint,
|
|
965
|
+
onOpenChange,
|
|
966
|
+
dismissible,
|
|
967
|
+
handleOnly,
|
|
968
|
+
isOpen,
|
|
969
|
+
isDragging,
|
|
970
|
+
shouldFade,
|
|
971
|
+
shouldOverlayAnimate,
|
|
972
|
+
closeDrawer,
|
|
973
|
+
modal,
|
|
974
|
+
snapPointsOffset,
|
|
975
|
+
activeSnapPointIndex,
|
|
976
|
+
direction,
|
|
977
|
+
noBodyStyles,
|
|
978
|
+
container,
|
|
979
|
+
autoFocus,
|
|
980
|
+
setIsOpen,
|
|
981
|
+
closeOnInteractOutside,
|
|
982
|
+
closeOnEscape,
|
|
983
|
+
onRelease,
|
|
984
|
+
onDrag,
|
|
985
|
+
onPress,
|
|
986
|
+
titleId,
|
|
987
|
+
descriptionId,
|
|
988
|
+
lazyMountProp,
|
|
989
|
+
unmountOnExitProp,
|
|
990
|
+
hasAnimationDone,
|
|
991
|
+
closeButtonRef,
|
|
992
|
+
isCloseButtonRendered,
|
|
993
|
+
stateProps
|
|
994
|
+
]);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const DrawerContext = /*#__PURE__*/ React.createContext(null);
|
|
998
|
+
const DrawerProvider = DrawerContext.Provider;
|
|
999
|
+
function useDrawerContext() {
|
|
1000
|
+
const context = React.useContext(DrawerContext);
|
|
1001
|
+
if (context === null) {
|
|
1002
|
+
throw new Error("useDrawerContext must be used within DrawerProvider");
|
|
1003
|
+
}
|
|
1004
|
+
return context;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const DrawerRoot = (props)=>{
|
|
1008
|
+
const api = useDrawer(props);
|
|
1009
|
+
return /*#__PURE__*/ jsxRuntime.jsx(DrawerProvider, {
|
|
1010
|
+
value: api,
|
|
1011
|
+
children: props.children
|
|
1012
|
+
});
|
|
1013
|
+
};
|
|
1014
|
+
const DrawerTrigger = /*#__PURE__*/ React.forwardRef((props, ref)=>{
|
|
1015
|
+
const api = useDrawerContext();
|
|
1016
|
+
return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.button, {
|
|
1017
|
+
ref: ref,
|
|
1018
|
+
...domUtils.mergeProps(api.triggerProps, props)
|
|
1019
|
+
});
|
|
1020
|
+
});
|
|
1021
|
+
DrawerTrigger.displayName = "DrawerTrigger";
|
|
1022
|
+
const DrawerPositioner = /*#__PURE__*/ React.forwardRef((props, ref)=>{
|
|
1023
|
+
const api = useDrawerContext();
|
|
1024
|
+
return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.div, {
|
|
1025
|
+
ref: ref,
|
|
1026
|
+
...domUtils.mergeProps(api.positionerProps, props)
|
|
1027
|
+
});
|
|
1028
|
+
});
|
|
1029
|
+
DrawerPositioner.displayName = "DrawerPositioner";
|
|
1030
|
+
const DrawerBackdrop = /*#__PURE__*/ React.forwardRef((props, ref)=>{
|
|
1031
|
+
const { overlayRef, onRelease, modal, snapPoints, isOpen, shouldFade, shouldOverlayAnimate, hasAnimationDone, lazyMount, unmountOnExit } = useDrawerContext();
|
|
1032
|
+
const composedRef = reactComposeRefs.composeRefs(ref, overlayRef);
|
|
1033
|
+
const hasSnapPoints = snapPoints && snapPoints.length > 0;
|
|
1034
|
+
const onMouseUp = reactUseCallbackRef.useCallbackRef((event)=>onRelease(event));
|
|
1035
|
+
if (!modal) {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
return /*#__PURE__*/ jsxRuntime.jsx(reactPresence.Presence, {
|
|
1039
|
+
present: isOpen,
|
|
1040
|
+
unmountOnExit: unmountOnExit,
|
|
1041
|
+
lazyMount: lazyMount,
|
|
1042
|
+
children: /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.div, {
|
|
1043
|
+
ref: composedRef,
|
|
1044
|
+
onMouseUp: onMouseUp,
|
|
1045
|
+
"data-snap-points": isOpen && hasSnapPoints ? "true" : "false",
|
|
1046
|
+
"data-snap-points-overlay": isOpen && shouldFade ? "true" : "false",
|
|
1047
|
+
"data-should-overlay-animate": shouldOverlayAnimate ? "true" : "false",
|
|
1048
|
+
"data-open": domUtils.dataAttr(isOpen),
|
|
1049
|
+
"data-animation-done": hasAnimationDone ? "true" : "false",
|
|
1050
|
+
...props
|
|
1051
|
+
})
|
|
1052
|
+
});
|
|
1053
|
+
});
|
|
1054
|
+
DrawerBackdrop.displayName = "DrawerBackdrop";
|
|
1055
|
+
const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
|
|
1056
|
+
const { style, ...restProps } = props;
|
|
1057
|
+
const { drawerRef, onPress, onRelease, onDrag, keyboardIsOpen, snapPointsOffset, activeSnapPointIndex, modal, isOpen, direction, snapPoints, container, handleOnly, autoFocus, closeDrawer, closeOnInteractOutside, closeOnEscape, dismissible, hasAnimationDone, titleId, descriptionId, lazyMount, unmountOnExit } = useDrawerContext();
|
|
1058
|
+
const [contentNode, setContentNode] = React.useState(null);
|
|
1059
|
+
const contentRef = React.useCallback((el)=>setContentNode(el), []);
|
|
1060
|
+
// aria-hide everything except the content when modal
|
|
1061
|
+
React.useEffect(()=>{
|
|
1062
|
+
if (!isOpen || !modal || !contentNode) return;
|
|
1063
|
+
return ariaHidden.hideOthers(contentNode);
|
|
1064
|
+
}, [
|
|
1065
|
+
isOpen,
|
|
1066
|
+
modal,
|
|
1067
|
+
contentNode
|
|
1068
|
+
]);
|
|
1069
|
+
// Needed to use transition instead of animations
|
|
1070
|
+
const [delayedSnapPoints, setDelayedSnapPoints] = React.useState(false);
|
|
1071
|
+
const pointerStartRef = React.useRef(null);
|
|
1072
|
+
const lastKnownPointerEventRef = React.useRef(null);
|
|
1073
|
+
const wasBeyondThePointRef = React.useRef(false);
|
|
1074
|
+
const hasSnapPoints = snapPoints && snapPoints.length > 0;
|
|
1075
|
+
const isDeltaInDirection = (delta, dir, threshold = 0)=>{
|
|
1076
|
+
if (wasBeyondThePointRef.current) return true;
|
|
1077
|
+
const deltaY = Math.abs(delta.y);
|
|
1078
|
+
const deltaX = Math.abs(delta.x);
|
|
1079
|
+
const isDeltaX = deltaX > deltaY;
|
|
1080
|
+
const dFactor = [
|
|
1081
|
+
"bottom",
|
|
1082
|
+
"right"
|
|
1083
|
+
].includes(dir) ? 1 : -1;
|
|
1084
|
+
if (dir === "left" || dir === "right") {
|
|
1085
|
+
const isReverseDirection = delta.x * dFactor < 0;
|
|
1086
|
+
if (!isReverseDirection && deltaX >= 0 && deltaX <= threshold) {
|
|
1087
|
+
return isDeltaX;
|
|
1088
|
+
}
|
|
1089
|
+
} else {
|
|
1090
|
+
const isReverseDirection = delta.y * dFactor < 0;
|
|
1091
|
+
if (!isReverseDirection && deltaY >= 0 && deltaY <= threshold) {
|
|
1092
|
+
return !isDeltaX;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
wasBeyondThePointRef.current = true;
|
|
1096
|
+
return true;
|
|
1097
|
+
};
|
|
1098
|
+
React.useEffect(()=>{
|
|
1099
|
+
if (hasSnapPoints) {
|
|
1100
|
+
window.requestAnimationFrame(()=>{
|
|
1101
|
+
setDelayedSnapPoints(true);
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
}, []);
|
|
1105
|
+
function handleOnPointerUp(event) {
|
|
1106
|
+
pointerStartRef.current = null;
|
|
1107
|
+
wasBeyondThePointRef.current = false;
|
|
1108
|
+
onRelease(event);
|
|
1109
|
+
}
|
|
1110
|
+
return /*#__PURE__*/ jsxRuntime.jsx(reactPresence.Presence, {
|
|
1111
|
+
present: isOpen,
|
|
1112
|
+
unmountOnExit: unmountOnExit,
|
|
1113
|
+
lazyMount: lazyMount,
|
|
1114
|
+
children: /*#__PURE__*/ jsxRuntime.jsx(reactDismissibleLayer.DismissibleLayer, {
|
|
1115
|
+
enabled: isOpen,
|
|
1116
|
+
blockPointerEvents: modal,
|
|
1117
|
+
onEscapeKeyDown: (e)=>{
|
|
1118
|
+
if (e.defaultPrevented) return;
|
|
1119
|
+
if (!dismissible || !closeOnEscape) return;
|
|
1120
|
+
closeDrawer(false, {
|
|
1121
|
+
reason: "escapeKeyDown",
|
|
1122
|
+
event: e
|
|
1123
|
+
});
|
|
1124
|
+
},
|
|
1125
|
+
onPressOutside: (e)=>{
|
|
1126
|
+
if (e.defaultPrevented) return;
|
|
1127
|
+
if (!modal) return;
|
|
1128
|
+
if (keyboardIsOpen.current) keyboardIsOpen.current = false;
|
|
1129
|
+
if (!dismissible || !closeOnInteractOutside) return;
|
|
1130
|
+
closeDrawer(false, {
|
|
1131
|
+
reason: "interactOutside",
|
|
1132
|
+
event: e
|
|
1133
|
+
});
|
|
1134
|
+
},
|
|
1135
|
+
onFocusOutside: ()=>{
|
|
1136
|
+
// Focus trapping is handled by FocusScope — nothing to do here.
|
|
1137
|
+
// The old Radix architecture needed e.preventDefault() here (PR #1187)
|
|
1138
|
+
// to prevent cascade closes, but the new DismissibleLayer handles
|
|
1139
|
+
// that via onCascadeDismiss instead.
|
|
1140
|
+
},
|
|
1141
|
+
onCascadeDismiss: ({ dismissedParent })=>{
|
|
1142
|
+
closeDrawer(false, {
|
|
1143
|
+
reason: "cascadeDismiss",
|
|
1144
|
+
dismissedParent
|
|
1145
|
+
});
|
|
1146
|
+
},
|
|
1147
|
+
children: /*#__PURE__*/ jsxRuntime.jsx(reactFocusScope.FocusScope, {
|
|
1148
|
+
asChild: true,
|
|
1149
|
+
loop: modal,
|
|
1150
|
+
trapped: modal && isOpen,
|
|
1151
|
+
onMountAutoFocus: (e)=>{
|
|
1152
|
+
// prevent FocusScope's default autoFocus behavior
|
|
1153
|
+
e.preventDefault();
|
|
1154
|
+
// when autoFocus is true, FocusScope sets the focus to the first tabbable element; otherwise content;
|
|
1155
|
+
// the desired behavior is to set the focus to the content regardless of whether there are tabbable elements or not when true]
|
|
1156
|
+
if (autoFocus) {
|
|
1157
|
+
drawerRef.current?.focus();
|
|
1158
|
+
}
|
|
1159
|
+
// later, we can do something like:
|
|
1160
|
+
// when trigger has clicked using keyboard, focus the first tabbable element;
|
|
1161
|
+
// when trigger has clicked using mouse, focus the content
|
|
1162
|
+
// -> matches Menu behavior
|
|
1163
|
+
},
|
|
1164
|
+
children: /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.div, {
|
|
1165
|
+
role: "dialog",
|
|
1166
|
+
"aria-modal": modal,
|
|
1167
|
+
"aria-labelledby": titleId,
|
|
1168
|
+
"aria-describedby": descriptionId,
|
|
1169
|
+
"data-delayed-snap-points": delayedSnapPoints ? "true" : "false",
|
|
1170
|
+
"data-drawer-direction": direction,
|
|
1171
|
+
"data-open": domUtils.dataAttr(isOpen),
|
|
1172
|
+
"data-animation-done": hasAnimationDone ? "true" : "false",
|
|
1173
|
+
"data-drawer": "",
|
|
1174
|
+
"data-snap-points": isOpen && hasSnapPoints ? "true" : "false",
|
|
1175
|
+
"data-custom-container": container ? "true" : "false",
|
|
1176
|
+
...restProps,
|
|
1177
|
+
ref: reactComposeRefs.composeRefs(ref, drawerRef, contentRef),
|
|
1178
|
+
style: {
|
|
1179
|
+
...snapPointsOffset && snapPointsOffset.length > 0 ? {
|
|
1180
|
+
"--snap-point-height": `${snapPointsOffset[activeSnapPointIndex ?? 0]}px`,
|
|
1181
|
+
...style
|
|
1182
|
+
} : style,
|
|
1183
|
+
...!modal && {
|
|
1184
|
+
pointerEvents: "auto"
|
|
1185
|
+
}
|
|
1186
|
+
},
|
|
1187
|
+
onPointerDown: (event)=>{
|
|
1188
|
+
if (handleOnly) return;
|
|
1189
|
+
restProps.onPointerDown?.(event);
|
|
1190
|
+
pointerStartRef.current = {
|
|
1191
|
+
x: event.pageX,
|
|
1192
|
+
y: event.pageY
|
|
1193
|
+
};
|
|
1194
|
+
onPress(event);
|
|
1195
|
+
},
|
|
1196
|
+
onPointerMove: (event)=>{
|
|
1197
|
+
lastKnownPointerEventRef.current = event;
|
|
1198
|
+
if (handleOnly) return;
|
|
1199
|
+
restProps.onPointerMove?.(event);
|
|
1200
|
+
if (!pointerStartRef.current) return;
|
|
1201
|
+
const yPosition = event.pageY - pointerStartRef.current.y;
|
|
1202
|
+
const xPosition = event.pageX - pointerStartRef.current.x;
|
|
1203
|
+
const swipeStartThreshold = event.pointerType === "touch" ? 10 : 2;
|
|
1204
|
+
const delta = {
|
|
1205
|
+
x: xPosition,
|
|
1206
|
+
y: yPosition
|
|
1207
|
+
};
|
|
1208
|
+
const isAllowedToSwipe = isDeltaInDirection(delta, direction, swipeStartThreshold);
|
|
1209
|
+
if (isAllowedToSwipe) onDrag(event);
|
|
1210
|
+
else if (Math.abs(xPosition) > swipeStartThreshold || Math.abs(yPosition) > swipeStartThreshold) {
|
|
1211
|
+
pointerStartRef.current = null;
|
|
1212
|
+
}
|
|
1213
|
+
},
|
|
1214
|
+
onPointerUp: (event)=>{
|
|
1215
|
+
restProps.onPointerUp?.(event);
|
|
1216
|
+
pointerStartRef.current = null;
|
|
1217
|
+
wasBeyondThePointRef.current = false;
|
|
1218
|
+
onRelease(event);
|
|
1219
|
+
},
|
|
1220
|
+
onPointerOut: (event)=>{
|
|
1221
|
+
restProps.onPointerOut?.(event);
|
|
1222
|
+
handleOnPointerUp(lastKnownPointerEventRef.current);
|
|
1223
|
+
},
|
|
1224
|
+
onContextMenu: (event)=>{
|
|
1225
|
+
restProps.onContextMenu?.(event);
|
|
1226
|
+
if (lastKnownPointerEventRef.current) {
|
|
1227
|
+
handleOnPointerUp(lastKnownPointerEventRef.current);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
})
|
|
1231
|
+
})
|
|
1232
|
+
})
|
|
1233
|
+
});
|
|
1234
|
+
});
|
|
1235
|
+
DrawerContent.displayName = "DrawerContent";
|
|
1236
|
+
const DrawerTitle = /*#__PURE__*/ React.forwardRef((props, ref)=>{
|
|
1237
|
+
const api = useDrawerContext();
|
|
1238
|
+
return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.h2, {
|
|
1239
|
+
ref: ref,
|
|
1240
|
+
...domUtils.mergeProps(api.titleProps, props)
|
|
1241
|
+
});
|
|
1242
|
+
});
|
|
1243
|
+
DrawerTitle.displayName = "DrawerTitle";
|
|
1244
|
+
const DrawerDescription = /*#__PURE__*/ React.forwardRef((props, ref)=>{
|
|
1245
|
+
const api = useDrawerContext();
|
|
1246
|
+
return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.p, {
|
|
1247
|
+
ref: ref,
|
|
1248
|
+
...domUtils.mergeProps(api.descriptionProps, props)
|
|
1249
|
+
});
|
|
1250
|
+
});
|
|
1251
|
+
DrawerDescription.displayName = "DrawerDescription";
|
|
1252
|
+
const DrawerHeader = /*#__PURE__*/ React.forwardRef((props, ref)=>{
|
|
1253
|
+
const api = useDrawerContext();
|
|
1254
|
+
return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.div, {
|
|
1255
|
+
ref: ref,
|
|
1256
|
+
...domUtils.mergeProps(api.headerProps, props)
|
|
1257
|
+
});
|
|
1258
|
+
});
|
|
1259
|
+
DrawerHeader.displayName = "DrawerHeader";
|
|
1260
|
+
const DrawerCloseButton = /*#__PURE__*/ React.forwardRef((props, ref)=>{
|
|
1261
|
+
const api = useDrawerContext();
|
|
1262
|
+
return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.button, {
|
|
1263
|
+
ref: reactComposeRefs.composeRefs(ref, api.closeButtonRef),
|
|
1264
|
+
...domUtils.mergeProps(api.closeButtonProps, props)
|
|
1265
|
+
});
|
|
1266
|
+
});
|
|
1267
|
+
const LONG_HANDLE_PRESS_TIMEOUT = 250;
|
|
1268
|
+
const DOUBLE_TAP_TIMEOUT = 120;
|
|
1269
|
+
const DrawerHandle = /*#__PURE__*/ React.forwardRef((props, ref)=>{
|
|
1270
|
+
const { preventCycle = false, children, ...rest } = props;
|
|
1271
|
+
const { closeDrawer, isDragging, snapPoints, activeSnapPoint, setActiveSnapPoint, dismissible, handleOnly, isOpen, onPress, onDrag, onRelease } = useDrawerContext();
|
|
1272
|
+
const closeTimeoutIdRef = React.useRef(null);
|
|
1273
|
+
const shouldCancelInteractionRef = React.useRef(false);
|
|
1274
|
+
function handleStartCycle(event) {
|
|
1275
|
+
// Stop if this is the second click of a double click
|
|
1276
|
+
if (shouldCancelInteractionRef.current) {
|
|
1277
|
+
handleCancelInteraction();
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
window.setTimeout(()=>{
|
|
1281
|
+
handleCycleSnapPoints(event);
|
|
1282
|
+
}, DOUBLE_TAP_TIMEOUT);
|
|
1283
|
+
}
|
|
1284
|
+
function handleCycleSnapPoints(event) {
|
|
1285
|
+
// Prevent accidental taps while resizing drawer
|
|
1286
|
+
if (isDragging || preventCycle || shouldCancelInteractionRef.current) {
|
|
1287
|
+
handleCancelInteraction();
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
// Make sure to clear the timeout id if the user releases the handle before the cancel timeout
|
|
1291
|
+
handleCancelInteraction();
|
|
1292
|
+
if (!snapPoints || snapPoints.length === 0) {
|
|
1293
|
+
if (!dismissible) {
|
|
1294
|
+
closeDrawer(false, {
|
|
1295
|
+
reason: "handleClickOnLastSnapPoint",
|
|
1296
|
+
event: event.nativeEvent
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
const isLastSnapPoint = activeSnapPoint === snapPoints[snapPoints.length - 1];
|
|
1302
|
+
if (isLastSnapPoint && dismissible) {
|
|
1303
|
+
closeDrawer(false, {
|
|
1304
|
+
reason: "handleClickOnLastSnapPoint",
|
|
1305
|
+
event: event.nativeEvent
|
|
1306
|
+
});
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
const currentSnapIndex = snapPoints.findIndex((point)=>point === activeSnapPoint);
|
|
1310
|
+
if (currentSnapIndex === -1) return; // activeSnapPoint not found in snapPoints
|
|
1311
|
+
const nextSnapPoint = snapPoints[currentSnapIndex + 1];
|
|
1312
|
+
setActiveSnapPoint(nextSnapPoint);
|
|
1313
|
+
}
|
|
1314
|
+
function handleStartInteraction() {
|
|
1315
|
+
closeTimeoutIdRef.current = window.setTimeout(()=>{
|
|
1316
|
+
// Cancel click interaction on a long press
|
|
1317
|
+
shouldCancelInteractionRef.current = true;
|
|
1318
|
+
}, LONG_HANDLE_PRESS_TIMEOUT);
|
|
1319
|
+
}
|
|
1320
|
+
function handleCancelInteraction() {
|
|
1321
|
+
if (closeTimeoutIdRef.current) {
|
|
1322
|
+
window.clearTimeout(closeTimeoutIdRef.current);
|
|
1323
|
+
}
|
|
1324
|
+
shouldCancelInteractionRef.current = false;
|
|
1325
|
+
}
|
|
1326
|
+
return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.div, {
|
|
1327
|
+
ref: ref,
|
|
1328
|
+
onClick: handleStartCycle,
|
|
1329
|
+
onPointerCancel: handleCancelInteraction,
|
|
1330
|
+
onPointerDown: (e)=>{
|
|
1331
|
+
if (handleOnly) onPress(e);
|
|
1332
|
+
handleStartInteraction();
|
|
1333
|
+
},
|
|
1334
|
+
onPointerMove: (e)=>{
|
|
1335
|
+
if (handleOnly) onDrag(e);
|
|
1336
|
+
},
|
|
1337
|
+
onPointerUp: (e)=>{
|
|
1338
|
+
if (handleOnly) onRelease(e);
|
|
1339
|
+
handleCancelInteraction();
|
|
1340
|
+
},
|
|
1341
|
+
"data-drawer-visible": isOpen ? "true" : "false",
|
|
1342
|
+
"data-handle": "",
|
|
1343
|
+
"aria-hidden": "true",
|
|
1344
|
+
...rest,
|
|
1345
|
+
children: children
|
|
1346
|
+
});
|
|
1347
|
+
});
|
|
1348
|
+
DrawerHandle.displayName = "DrawerHandle";
|
|
1349
|
+
|
|
1350
|
+
exports.DrawerBackdrop = DrawerBackdrop;
|
|
1351
|
+
exports.DrawerCloseButton = DrawerCloseButton;
|
|
1352
|
+
exports.DrawerContent = DrawerContent;
|
|
1353
|
+
exports.DrawerDescription = DrawerDescription;
|
|
1354
|
+
exports.DrawerHandle = DrawerHandle;
|
|
1355
|
+
exports.DrawerHeader = DrawerHeader;
|
|
1356
|
+
exports.DrawerPositioner = DrawerPositioner;
|
|
1357
|
+
exports.DrawerRoot = DrawerRoot;
|
|
1358
|
+
exports.DrawerTitle = DrawerTitle;
|
|
1359
|
+
exports.DrawerTrigger = DrawerTrigger;
|
|
1360
|
+
exports.useDrawer = useDrawer;
|
|
1361
|
+
exports.useDrawerContext = useDrawerContext;
|