@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
package/src/useDrawer.ts
ADDED
|
@@ -0,0 +1,810 @@
|
|
|
1
|
+
import { useControllableState } from "@seed-design/react-use-controllable-state";
|
|
2
|
+
import { buttonProps, dataAttr, elementProps } from "@seed-design/dom-utils";
|
|
3
|
+
import type React from "react";
|
|
4
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { isAndroid, isIOS, isMobileFirefox } from "./browser";
|
|
6
|
+
import {
|
|
7
|
+
CLOSE_THRESHOLD,
|
|
8
|
+
DRAG_CLASS,
|
|
9
|
+
SCROLL_LOCK_TIMEOUT,
|
|
10
|
+
TRANSITIONS,
|
|
11
|
+
VELOCITY_THRESHOLD,
|
|
12
|
+
WINDOW_TOP_OFFSET,
|
|
13
|
+
} from "./constants";
|
|
14
|
+
import { dampenValue, getTranslate, isInput, isVertical, reset, set } from "./helpers";
|
|
15
|
+
import { usePositionFixed } from "./use-position-fixed";
|
|
16
|
+
import { useSnapPoints } from "./use-snap-points";
|
|
17
|
+
|
|
18
|
+
interface DrawerReasonToDetailMap {
|
|
19
|
+
// we might add synthetic events later if needed; currently we aim consistency; DismissibleLayer gives us native events
|
|
20
|
+
closeButton: { event: MouseEvent };
|
|
21
|
+
escapeKeyDown: { event: KeyboardEvent };
|
|
22
|
+
interactOutside: { event: PointerEvent | TouchEvent | FocusEvent };
|
|
23
|
+
drag: { event: PointerEvent };
|
|
24
|
+
handleClickOnLastSnapPoint: { event: MouseEvent };
|
|
25
|
+
cascadeDismiss: { dismissedParent: HTMLElement };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type DrawerChangeDetails = {
|
|
29
|
+
[R in keyof DrawerReasonToDetailMap]: {
|
|
30
|
+
reason?: R;
|
|
31
|
+
} & DrawerReasonToDetailMap[R];
|
|
32
|
+
}[keyof DrawerReasonToDetailMap];
|
|
33
|
+
|
|
34
|
+
export interface UseDrawerProps {
|
|
35
|
+
activeSnapPoint?: number | string | null;
|
|
36
|
+
setActiveSnapPoint?: (snapPoint: number | string | null) => void;
|
|
37
|
+
children?: React.ReactNode;
|
|
38
|
+
open?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Number between 0 and 1 that determines when the drawer should be closed.
|
|
41
|
+
* Example: threshold of 0.5 would close the drawer if the user swiped for 50% of the height of the drawer or more.
|
|
42
|
+
* @default 0.25
|
|
43
|
+
*/
|
|
44
|
+
closeThreshold?: number;
|
|
45
|
+
/**
|
|
46
|
+
* When `true` the `body` doesn't get any styles assigned from Drawer
|
|
47
|
+
* @default true
|
|
48
|
+
*/
|
|
49
|
+
noBodyStyles?: boolean;
|
|
50
|
+
onOpenChange?: (open: boolean, details?: DrawerChangeDetails) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Duration for which the drawer is not draggable after scrolling content inside of the drawer.
|
|
53
|
+
* @default 500ms
|
|
54
|
+
*/
|
|
55
|
+
scrollLockTimeout?: number;
|
|
56
|
+
/**
|
|
57
|
+
* When `true`, don't move the drawer upwards if there's space, but rather only change it's height so it's fully scrollable when the keyboard is open
|
|
58
|
+
*/
|
|
59
|
+
fixed?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* When `true` only allows the drawer to be dragged by the `<Drawer.Handle />` component.
|
|
62
|
+
* @default false
|
|
63
|
+
*/
|
|
64
|
+
handleOnly?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* When `false` dragging, clicking outside, pressing esc, etc. will not close the drawer.
|
|
67
|
+
* Use this in combination with the `open` prop, otherwise you won't be able to open/close the drawer.
|
|
68
|
+
* @default true
|
|
69
|
+
*/
|
|
70
|
+
dismissible?: boolean;
|
|
71
|
+
onDrag?: (event: React.PointerEvent<HTMLDivElement>, percentageDragged: number) => void;
|
|
72
|
+
onRelease?: (event: React.PointerEvent<HTMLDivElement>, open: boolean) => void;
|
|
73
|
+
/**
|
|
74
|
+
* When `false` it allows to interact with elements outside of the drawer without closing it.
|
|
75
|
+
* @default true
|
|
76
|
+
*/
|
|
77
|
+
modal?: boolean;
|
|
78
|
+
nested?: boolean;
|
|
79
|
+
onClose?: () => void;
|
|
80
|
+
/**
|
|
81
|
+
* Direction of the drawer. Can be `top` or `bottom`, `left`, `right`.
|
|
82
|
+
* @default 'bottom'
|
|
83
|
+
*/
|
|
84
|
+
direction?: "top" | "bottom" | "left" | "right";
|
|
85
|
+
/**
|
|
86
|
+
* Opened by default. Still reacts to `open` state changes
|
|
87
|
+
* @default false
|
|
88
|
+
*/
|
|
89
|
+
defaultOpen?: boolean;
|
|
90
|
+
/**
|
|
91
|
+
* When `true` Vaul will reposition inputs rather than scroll then into view if the keyboard is in the way.
|
|
92
|
+
* Setting it to `false` will fall back to the default browser behavior.
|
|
93
|
+
* @default true when {@link snapPoints} is defined
|
|
94
|
+
*/
|
|
95
|
+
repositionInputs?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Disabled velocity based swiping for snap points.
|
|
98
|
+
* This means that a snap point won't be skipped even if the velocity is high enough.
|
|
99
|
+
* Useful if each snap point in a drawer is equally important.
|
|
100
|
+
* @default false
|
|
101
|
+
*/
|
|
102
|
+
snapToSequentialPoint?: boolean;
|
|
103
|
+
container?: HTMLElement | null;
|
|
104
|
+
/**
|
|
105
|
+
* Gets triggered after the open or close animation ends, it receives an `open` argument with the `open` state of the drawer by the time the function was triggered.
|
|
106
|
+
* Useful to revert any state changes for example.
|
|
107
|
+
*/
|
|
108
|
+
onAnimationEnd?: (open: boolean) => void;
|
|
109
|
+
preventScrollRestoration?: boolean;
|
|
110
|
+
autoFocus?: boolean;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Array of snap points to use.
|
|
114
|
+
* Example: snapPoints={["100px", "200px", 1]} will use the snap points 100px, 200px and fully open (1 = 100% of the container).
|
|
115
|
+
* @default undefined
|
|
116
|
+
*/
|
|
117
|
+
snapPoints?: (number | string)[];
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Index of the snap point to start fading from.
|
|
121
|
+
* Example: fadeFromIndex={0} will start fading from the first snap point.
|
|
122
|
+
* @default snapPoints.length - 1
|
|
123
|
+
*/
|
|
124
|
+
fadeFromIndex?: number;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Whether to close the drawer when interacting outside of the drawer.
|
|
128
|
+
* @default true
|
|
129
|
+
*/
|
|
130
|
+
closeOnInteractOutside?: boolean;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Whether to close the drawer when pressing the escape key.
|
|
134
|
+
* @default true
|
|
135
|
+
*/
|
|
136
|
+
closeOnEscape?: boolean;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Whether to lazy mount the drawer content on first open.
|
|
140
|
+
* @default false
|
|
141
|
+
*/
|
|
142
|
+
lazyMount?: boolean;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Whether to unmount the drawer content on exit.
|
|
146
|
+
* @default false
|
|
147
|
+
*/
|
|
148
|
+
unmountOnExit?: boolean;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function useDrawer(props: UseDrawerProps) {
|
|
152
|
+
const {
|
|
153
|
+
open: openProp,
|
|
154
|
+
onOpenChange,
|
|
155
|
+
onDrag: onDragProp,
|
|
156
|
+
onRelease: onReleaseProp,
|
|
157
|
+
snapPoints,
|
|
158
|
+
closeThreshold = CLOSE_THRESHOLD,
|
|
159
|
+
scrollLockTimeout = SCROLL_LOCK_TIMEOUT,
|
|
160
|
+
dismissible = true,
|
|
161
|
+
handleOnly = false,
|
|
162
|
+
fadeFromIndex = snapPoints && snapPoints.length - 1,
|
|
163
|
+
activeSnapPoint: activeSnapPointProp,
|
|
164
|
+
setActiveSnapPoint: setActiveSnapPointProp,
|
|
165
|
+
fixed,
|
|
166
|
+
modal = true,
|
|
167
|
+
onClose,
|
|
168
|
+
nested,
|
|
169
|
+
noBodyStyles = true,
|
|
170
|
+
direction = "bottom",
|
|
171
|
+
defaultOpen = false,
|
|
172
|
+
snapToSequentialPoint = false,
|
|
173
|
+
preventScrollRestoration = false,
|
|
174
|
+
repositionInputs = true,
|
|
175
|
+
onAnimationEnd,
|
|
176
|
+
container,
|
|
177
|
+
autoFocus = true,
|
|
178
|
+
closeOnInteractOutside = true,
|
|
179
|
+
closeOnEscape = true,
|
|
180
|
+
lazyMount: lazyMountProp = false,
|
|
181
|
+
unmountOnExit: unmountOnExitProp = false,
|
|
182
|
+
} = props;
|
|
183
|
+
|
|
184
|
+
const drawerId = useId();
|
|
185
|
+
const titleId = `${drawerId}-title`;
|
|
186
|
+
const descriptionId = `${drawerId}-description`;
|
|
187
|
+
|
|
188
|
+
const [isOpen = false, setIsOpen] = useControllableState<boolean, DrawerChangeDetails>({
|
|
189
|
+
defaultProp: defaultOpen,
|
|
190
|
+
prop: openProp,
|
|
191
|
+
onChange: (o: boolean, details?: DrawerChangeDetails) => {
|
|
192
|
+
onOpenChange?.(o, details);
|
|
193
|
+
|
|
194
|
+
if (!o && !nested) {
|
|
195
|
+
restorePositionSetting();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
setTimeout(() => {
|
|
199
|
+
onAnimationEnd?.(o);
|
|
200
|
+
}, TRANSITIONS.EXIT_DURATION * 1000);
|
|
201
|
+
|
|
202
|
+
if (o && !modal) {
|
|
203
|
+
if (typeof window !== "undefined") {
|
|
204
|
+
window.requestAnimationFrame(() => {
|
|
205
|
+
document.body.style.pointerEvents = "auto";
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!o) {
|
|
211
|
+
document.body.style.pointerEvents = "auto";
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const [hasBeenOpened, setHasBeenOpened] = useState<boolean>(false);
|
|
217
|
+
const [hasAnimationDone, setHasAnimationDone] = useState<boolean>(false);
|
|
218
|
+
const [isDragging, setIsDragging] = useState<boolean>(false);
|
|
219
|
+
const [shouldOverlayAnimate, setShouldOverlayAnimate] = useState<boolean>(false);
|
|
220
|
+
|
|
221
|
+
const [isCloseButtonRendered, setIsCloseButtonRendered] = useState<boolean>(false);
|
|
222
|
+
const closeButtonRef = useCallback((node: HTMLButtonElement | null) => {
|
|
223
|
+
setIsCloseButtonRendered(!!node);
|
|
224
|
+
}, []);
|
|
225
|
+
|
|
226
|
+
const overlayRef = useRef<HTMLDivElement>(null);
|
|
227
|
+
const openTime = useRef<Date | null>(null);
|
|
228
|
+
const dragStartTime = useRef<Date | null>(null);
|
|
229
|
+
const dragEndTime = useRef<Date | null>(null);
|
|
230
|
+
const lastTimeDragPrevented = useRef<Date | null>(null);
|
|
231
|
+
const isAllowedToDrag = useRef<boolean>(false);
|
|
232
|
+
const pointerStart = useRef(0);
|
|
233
|
+
const keyboardIsOpen = useRef(false);
|
|
234
|
+
const previousDiffFromInitial = useRef(0);
|
|
235
|
+
const drawerRef = useRef<HTMLDivElement>(null);
|
|
236
|
+
const drawerHeightRef = useRef(drawerRef.current?.getBoundingClientRect().height || 0);
|
|
237
|
+
const drawerWidthRef = useRef(drawerRef.current?.getBoundingClientRect().width || 0);
|
|
238
|
+
const initialDrawerHeight = useRef(0);
|
|
239
|
+
|
|
240
|
+
const onSnapPointChange = useCallback(
|
|
241
|
+
(activeSnapPointIndex: number) => {
|
|
242
|
+
if (snapPoints && activeSnapPointIndex === snapPointsOffset.length - 1) {
|
|
243
|
+
openTime.current = new Date();
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
[snapPoints],
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const {
|
|
250
|
+
activeSnapPoint,
|
|
251
|
+
activeSnapPointIndex,
|
|
252
|
+
setActiveSnapPoint,
|
|
253
|
+
onRelease: onReleaseSnapPoints,
|
|
254
|
+
snapPointsOffset,
|
|
255
|
+
onDrag: onDragSnapPoints,
|
|
256
|
+
shouldFade,
|
|
257
|
+
getPercentageDragged: getSnapPointsPercentageDragged,
|
|
258
|
+
} = useSnapPoints({
|
|
259
|
+
snapPoints,
|
|
260
|
+
activeSnapPointProp,
|
|
261
|
+
setActiveSnapPointProp,
|
|
262
|
+
drawerRef,
|
|
263
|
+
fadeFromIndex,
|
|
264
|
+
overlayRef,
|
|
265
|
+
onSnapPointChange,
|
|
266
|
+
direction,
|
|
267
|
+
snapToSequentialPoint,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const { restorePositionSetting } = usePositionFixed({
|
|
271
|
+
isOpen,
|
|
272
|
+
modal,
|
|
273
|
+
nested: nested ?? false,
|
|
274
|
+
hasBeenOpened,
|
|
275
|
+
preventScrollRestoration,
|
|
276
|
+
noBodyStyles,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
function onPress(event: React.PointerEvent<HTMLDivElement>) {
|
|
280
|
+
if (!dismissible && !snapPoints) return;
|
|
281
|
+
if (drawerRef.current && !drawerRef.current.contains(event.target as Node)) return;
|
|
282
|
+
|
|
283
|
+
drawerHeightRef.current = drawerRef.current?.getBoundingClientRect().height || 0;
|
|
284
|
+
drawerWidthRef.current = drawerRef.current?.getBoundingClientRect().width || 0;
|
|
285
|
+
setIsDragging(true);
|
|
286
|
+
dragStartTime.current = new Date();
|
|
287
|
+
|
|
288
|
+
if (isIOS()) {
|
|
289
|
+
window.addEventListener(
|
|
290
|
+
"touchend",
|
|
291
|
+
() => {
|
|
292
|
+
isAllowedToDrag.current = false;
|
|
293
|
+
},
|
|
294
|
+
{ once: true },
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
(event.target as HTMLElement).setPointerCapture(event.pointerId);
|
|
298
|
+
|
|
299
|
+
pointerStart.current = isVertical(direction) ? event.pageY : event.pageX;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function shouldDrag(el: EventTarget, isDraggingInDirection: boolean) {
|
|
303
|
+
let element = el as HTMLElement;
|
|
304
|
+
const highlightedText = window.getSelection()?.toString();
|
|
305
|
+
const swipeAmount = drawerRef.current ? getTranslate(drawerRef.current, direction) : null;
|
|
306
|
+
const date = new Date();
|
|
307
|
+
|
|
308
|
+
if (element.tagName === "SELECT") {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (element.hasAttribute("data-no-drag") || element.closest("[data-no-drag]")) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (direction === "right" || direction === "left") {
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (openTime.current && date.getTime() - openTime.current.getTime() < 500) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (swipeAmount !== null) {
|
|
325
|
+
if (direction === "bottom" ? swipeAmount > 0 : swipeAmount < 0) {
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (highlightedText && highlightedText.length > 0) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (
|
|
335
|
+
lastTimeDragPrevented.current &&
|
|
336
|
+
date.getTime() - lastTimeDragPrevented.current.getTime() < scrollLockTimeout &&
|
|
337
|
+
swipeAmount === 0
|
|
338
|
+
) {
|
|
339
|
+
lastTimeDragPrevented.current = date;
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (isDraggingInDirection) {
|
|
344
|
+
lastTimeDragPrevented.current = date;
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
while (element) {
|
|
349
|
+
if (element.scrollHeight > element.clientHeight) {
|
|
350
|
+
if (element.scrollTop !== 0) {
|
|
351
|
+
lastTimeDragPrevented.current = new Date();
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (element.getAttribute("role") === "dialog") {
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
element = element.parentNode as HTMLElement;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function onDrag(event: React.PointerEvent<HTMLDivElement>) {
|
|
367
|
+
if (!drawerRef.current) return;
|
|
368
|
+
|
|
369
|
+
if (isDragging) {
|
|
370
|
+
const directionMultiplier = direction === "bottom" || direction === "right" ? 1 : -1;
|
|
371
|
+
const draggedDistance =
|
|
372
|
+
(pointerStart.current - (isVertical(direction) ? event.pageY : event.pageX)) *
|
|
373
|
+
directionMultiplier;
|
|
374
|
+
const isDraggingInDirection = draggedDistance > 0;
|
|
375
|
+
|
|
376
|
+
const noCloseSnapPointsPreCondition = snapPoints && !dismissible && !isDraggingInDirection;
|
|
377
|
+
|
|
378
|
+
if (noCloseSnapPointsPreCondition && activeSnapPointIndex === 0) return;
|
|
379
|
+
|
|
380
|
+
const absDraggedDistance = Math.abs(draggedDistance);
|
|
381
|
+
const drawerDimension =
|
|
382
|
+
direction === "bottom" || direction === "top"
|
|
383
|
+
? drawerHeightRef.current
|
|
384
|
+
: drawerWidthRef.current;
|
|
385
|
+
|
|
386
|
+
let percentageDragged = absDraggedDistance / drawerDimension;
|
|
387
|
+
const snapPointPercentageDragged = getSnapPointsPercentageDragged(
|
|
388
|
+
absDraggedDistance,
|
|
389
|
+
isDraggingInDirection,
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
if (snapPointPercentageDragged !== null) {
|
|
393
|
+
percentageDragged = snapPointPercentageDragged;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (noCloseSnapPointsPreCondition && percentageDragged >= 1) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!isAllowedToDrag.current && !shouldDrag(event.target, isDraggingInDirection)) return;
|
|
401
|
+
drawerRef.current.classList.add(DRAG_CLASS);
|
|
402
|
+
|
|
403
|
+
isAllowedToDrag.current = true;
|
|
404
|
+
set(drawerRef.current, {
|
|
405
|
+
transition: "none",
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
set(overlayRef.current, {
|
|
409
|
+
transition: "none",
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
if (snapPoints) {
|
|
413
|
+
onDragSnapPoints({ draggedDistance });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (isDraggingInDirection && !snapPoints) {
|
|
417
|
+
const dampenedDraggedDistance = dampenValue(draggedDistance);
|
|
418
|
+
const translateValue = Math.min(dampenedDraggedDistance * -1, 0) * directionMultiplier;
|
|
419
|
+
set(drawerRef.current, {
|
|
420
|
+
transform: isVertical(direction)
|
|
421
|
+
? `translate3d(0, ${translateValue}px, 0)`
|
|
422
|
+
: `translate3d(${translateValue}px, 0, 0)`,
|
|
423
|
+
});
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const opacityValue = 1 - percentageDragged;
|
|
428
|
+
|
|
429
|
+
if (shouldFade || (fadeFromIndex && activeSnapPointIndex === fadeFromIndex - 1)) {
|
|
430
|
+
onDragProp?.(event, percentageDragged);
|
|
431
|
+
|
|
432
|
+
set(
|
|
433
|
+
overlayRef.current,
|
|
434
|
+
{
|
|
435
|
+
opacity: `${opacityValue}`,
|
|
436
|
+
transition: "none",
|
|
437
|
+
},
|
|
438
|
+
true,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (!snapPoints) {
|
|
443
|
+
const translateValue = absDraggedDistance * directionMultiplier;
|
|
444
|
+
|
|
445
|
+
set(drawerRef.current, {
|
|
446
|
+
transform: isVertical(direction)
|
|
447
|
+
? `translate3d(0, ${translateValue}px, 0)`
|
|
448
|
+
: `translate3d(${translateValue}px, 0, 0)`,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const cancelDrag = useCallback(() => {
|
|
455
|
+
if (!isDragging || !drawerRef.current) return;
|
|
456
|
+
|
|
457
|
+
drawerRef.current.classList.remove(DRAG_CLASS);
|
|
458
|
+
isAllowedToDrag.current = false;
|
|
459
|
+
setIsDragging(false);
|
|
460
|
+
dragEndTime.current = new Date();
|
|
461
|
+
}, [isDragging]);
|
|
462
|
+
|
|
463
|
+
const closeDrawer = useCallback(
|
|
464
|
+
(fromWithin?: boolean, details?: DrawerChangeDetails) => {
|
|
465
|
+
cancelDrag();
|
|
466
|
+
onClose?.();
|
|
467
|
+
|
|
468
|
+
if (!fromWithin) {
|
|
469
|
+
setIsOpen(false, details);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (fadeFromIndex !== undefined && fadeFromIndex > 0 && activeSnapPointIndex === 0) {
|
|
473
|
+
set(overlayRef.current, {
|
|
474
|
+
opacity: "0",
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
setTimeout(() => {
|
|
479
|
+
if (snapPoints) {
|
|
480
|
+
setActiveSnapPoint(snapPoints[0]);
|
|
481
|
+
}
|
|
482
|
+
}, TRANSITIONS.EXIT_DURATION * 1000);
|
|
483
|
+
},
|
|
484
|
+
[
|
|
485
|
+
cancelDrag,
|
|
486
|
+
onClose,
|
|
487
|
+
snapPoints,
|
|
488
|
+
setActiveSnapPoint,
|
|
489
|
+
setIsOpen,
|
|
490
|
+
fadeFromIndex,
|
|
491
|
+
activeSnapPointIndex,
|
|
492
|
+
],
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
function resetDrawer() {
|
|
496
|
+
if (!drawerRef.current) return;
|
|
497
|
+
|
|
498
|
+
set(drawerRef.current, {
|
|
499
|
+
transform: "translate3d(0, 0, 0)",
|
|
500
|
+
transition: `transform ${TRANSITIONS.EXIT_DURATION}s ${TRANSITIONS.CONTENT_EXIT_TIMING_FUNCTION}`,
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
set(overlayRef.current, {
|
|
504
|
+
transition: `opacity ${TRANSITIONS.EXIT_DURATION}s ${TRANSITIONS.OVERLAY_EXIT_TIMING_FUNCTION}`,
|
|
505
|
+
opacity: "1",
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function onRelease(event: React.PointerEvent<HTMLDivElement> | null) {
|
|
510
|
+
if (!isDragging || !drawerRef.current) return;
|
|
511
|
+
|
|
512
|
+
drawerRef.current.classList.remove(DRAG_CLASS);
|
|
513
|
+
isAllowedToDrag.current = false;
|
|
514
|
+
setIsDragging(false);
|
|
515
|
+
dragEndTime.current = new Date();
|
|
516
|
+
const swipeAmount = getTranslate(drawerRef.current, direction);
|
|
517
|
+
|
|
518
|
+
if (!event || !shouldDrag(event.target, false) || !swipeAmount || Number.isNaN(swipeAmount))
|
|
519
|
+
return;
|
|
520
|
+
|
|
521
|
+
if (dragStartTime.current === null) return;
|
|
522
|
+
|
|
523
|
+
const timeTaken = dragEndTime.current.getTime() - dragStartTime.current.getTime();
|
|
524
|
+
const distMoved = pointerStart.current - (isVertical(direction) ? event.pageY : event.pageX);
|
|
525
|
+
const velocity = Math.abs(distMoved) / timeTaken;
|
|
526
|
+
|
|
527
|
+
if (snapPoints) {
|
|
528
|
+
const directionMultiplier = direction === "bottom" || direction === "right" ? 1 : -1;
|
|
529
|
+
onReleaseSnapPoints({
|
|
530
|
+
draggedDistance: distMoved * directionMultiplier,
|
|
531
|
+
closeDrawer,
|
|
532
|
+
velocity,
|
|
533
|
+
dismissible,
|
|
534
|
+
event: event.nativeEvent,
|
|
535
|
+
});
|
|
536
|
+
onReleaseProp?.(event, true);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (direction === "bottom" || direction === "right" ? distMoved > 0 : distMoved < 0) {
|
|
541
|
+
resetDrawer();
|
|
542
|
+
onReleaseProp?.(event, true);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (velocity > VELOCITY_THRESHOLD) {
|
|
547
|
+
closeDrawer(false, { reason: "drag", event: event.nativeEvent });
|
|
548
|
+
onReleaseProp?.(event, false);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const visibleDrawerHeight = Math.min(
|
|
553
|
+
drawerRef.current.getBoundingClientRect().height ?? 0,
|
|
554
|
+
window.innerHeight,
|
|
555
|
+
);
|
|
556
|
+
const visibleDrawerWidth = Math.min(
|
|
557
|
+
drawerRef.current.getBoundingClientRect().width ?? 0,
|
|
558
|
+
window.innerWidth,
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
const isHorizontalSwipe = direction === "left" || direction === "right";
|
|
562
|
+
if (
|
|
563
|
+
Math.abs(swipeAmount) >=
|
|
564
|
+
(isHorizontalSwipe ? visibleDrawerWidth : visibleDrawerHeight) * closeThreshold
|
|
565
|
+
) {
|
|
566
|
+
closeDrawer(false, { reason: "drag", event: event.nativeEvent });
|
|
567
|
+
onReleaseProp?.(event, false);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
onReleaseProp?.(event, true);
|
|
572
|
+
resetDrawer();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
useEffect(() => {
|
|
576
|
+
if (isOpen) {
|
|
577
|
+
set(document.documentElement, {
|
|
578
|
+
scrollBehavior: "auto",
|
|
579
|
+
});
|
|
580
|
+
openTime.current = new Date();
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return () => {
|
|
584
|
+
reset(document.documentElement, "scrollBehavior");
|
|
585
|
+
};
|
|
586
|
+
}, [isOpen]);
|
|
587
|
+
|
|
588
|
+
useEffect(() => {
|
|
589
|
+
function onVisualViewportChange() {
|
|
590
|
+
if (!drawerRef.current || !repositionInputs) return;
|
|
591
|
+
|
|
592
|
+
const focusedElement = document.activeElement as HTMLElement;
|
|
593
|
+
if (isInput(focusedElement) || keyboardIsOpen.current) {
|
|
594
|
+
const visualViewportHeight = window.visualViewport?.height || 0;
|
|
595
|
+
const totalHeight = window.innerHeight;
|
|
596
|
+
let diffFromInitial = totalHeight - visualViewportHeight;
|
|
597
|
+
const drawerHeight = drawerRef.current.getBoundingClientRect().height || 0;
|
|
598
|
+
const isTallEnough = drawerHeight > totalHeight * 0.8;
|
|
599
|
+
|
|
600
|
+
if (!initialDrawerHeight.current) {
|
|
601
|
+
initialDrawerHeight.current = drawerHeight;
|
|
602
|
+
}
|
|
603
|
+
const offsetFromTop = drawerRef.current.getBoundingClientRect().top;
|
|
604
|
+
|
|
605
|
+
if (Math.abs(previousDiffFromInitial.current - diffFromInitial) > 60) {
|
|
606
|
+
keyboardIsOpen.current = !keyboardIsOpen.current;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (snapPoints && snapPoints.length > 0 && snapPointsOffset && activeSnapPointIndex) {
|
|
610
|
+
const activeSnapPointHeight = snapPointsOffset[activeSnapPointIndex] || 0;
|
|
611
|
+
diffFromInitial += activeSnapPointHeight;
|
|
612
|
+
}
|
|
613
|
+
previousDiffFromInitial.current = diffFromInitial;
|
|
614
|
+
|
|
615
|
+
if (drawerHeight > visualViewportHeight || keyboardIsOpen.current) {
|
|
616
|
+
const height = drawerRef.current.getBoundingClientRect().height;
|
|
617
|
+
let newDrawerHeight = height;
|
|
618
|
+
|
|
619
|
+
if (height > visualViewportHeight) {
|
|
620
|
+
newDrawerHeight =
|
|
621
|
+
visualViewportHeight - (isTallEnough ? offsetFromTop : WINDOW_TOP_OFFSET);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (fixed) {
|
|
625
|
+
drawerRef.current.style.height = `${height - Math.max(diffFromInitial, 0)}px`;
|
|
626
|
+
} else {
|
|
627
|
+
drawerRef.current.style.height = `${Math.max(newDrawerHeight, visualViewportHeight - offsetFromTop)}px`;
|
|
628
|
+
}
|
|
629
|
+
} else if (!isMobileFirefox() && !isAndroid()) {
|
|
630
|
+
drawerRef.current.style.height = `${initialDrawerHeight.current}px`;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (snapPoints && snapPoints.length > 0 && !keyboardIsOpen.current) {
|
|
634
|
+
drawerRef.current.style.bottom = "0px";
|
|
635
|
+
} else {
|
|
636
|
+
drawerRef.current.style.bottom = `${Math.max(diffFromInitial, 0)}px`;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
window.visualViewport?.addEventListener("resize", onVisualViewportChange);
|
|
642
|
+
return () => window.visualViewport?.removeEventListener("resize", onVisualViewportChange);
|
|
643
|
+
}, [activeSnapPointIndex, snapPoints, snapPointsOffset, repositionInputs, fixed]);
|
|
644
|
+
|
|
645
|
+
useEffect(() => {
|
|
646
|
+
if (!modal) {
|
|
647
|
+
window.requestAnimationFrame(() => {
|
|
648
|
+
document.body.style.pointerEvents = "auto";
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}, [modal]);
|
|
652
|
+
|
|
653
|
+
// Effect 1: Track drawer open state
|
|
654
|
+
useEffect(() => {
|
|
655
|
+
if (isOpen) {
|
|
656
|
+
setHasBeenOpened(true);
|
|
657
|
+
}
|
|
658
|
+
}, [isOpen]);
|
|
659
|
+
|
|
660
|
+
// Effect 2: Handle animation state and timer
|
|
661
|
+
useEffect(() => {
|
|
662
|
+
if (isOpen) {
|
|
663
|
+
// Only reset animation state if this is the first open
|
|
664
|
+
if (!hasBeenOpened) {
|
|
665
|
+
setHasAnimationDone(false);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const timeoutId = setTimeout(() => {
|
|
669
|
+
setHasAnimationDone(true);
|
|
670
|
+
}, TRANSITIONS.ENTER_DURATION * 1000);
|
|
671
|
+
|
|
672
|
+
return () => clearTimeout(timeoutId);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Reset animation state when drawer closes
|
|
676
|
+
setHasAnimationDone(false);
|
|
677
|
+
}, [isOpen, hasBeenOpened]);
|
|
678
|
+
|
|
679
|
+
useEffect(() => {
|
|
680
|
+
if (isOpen && snapPoints && fadeFromIndex === 0) {
|
|
681
|
+
setShouldOverlayAnimate(true);
|
|
682
|
+
|
|
683
|
+
const timeoutId = setTimeout(() => {
|
|
684
|
+
setShouldOverlayAnimate(false);
|
|
685
|
+
}, TRANSITIONS.ENTER_DURATION * 1000);
|
|
686
|
+
|
|
687
|
+
return () => clearTimeout(timeoutId);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
setShouldOverlayAnimate(false);
|
|
691
|
+
}, [isOpen, snapPoints, fadeFromIndex]);
|
|
692
|
+
|
|
693
|
+
const stateProps = useMemo(
|
|
694
|
+
() =>
|
|
695
|
+
elementProps({
|
|
696
|
+
"data-open": dataAttr(isOpen),
|
|
697
|
+
}),
|
|
698
|
+
[isOpen],
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
return useMemo(
|
|
702
|
+
() => ({
|
|
703
|
+
activeSnapPoint,
|
|
704
|
+
snapPoints,
|
|
705
|
+
setActiveSnapPoint,
|
|
706
|
+
drawerRef,
|
|
707
|
+
overlayRef,
|
|
708
|
+
shouldOverlayAnimate,
|
|
709
|
+
onOpenChange,
|
|
710
|
+
onPress,
|
|
711
|
+
onRelease,
|
|
712
|
+
onDrag,
|
|
713
|
+
dismissible,
|
|
714
|
+
handleOnly,
|
|
715
|
+
isOpen,
|
|
716
|
+
isDragging,
|
|
717
|
+
shouldFade,
|
|
718
|
+
closeDrawer,
|
|
719
|
+
keyboardIsOpen,
|
|
720
|
+
modal,
|
|
721
|
+
snapPointsOffset,
|
|
722
|
+
activeSnapPointIndex,
|
|
723
|
+
direction,
|
|
724
|
+
noBodyStyles,
|
|
725
|
+
container,
|
|
726
|
+
autoFocus,
|
|
727
|
+
setHasBeenOpened,
|
|
728
|
+
setIsOpen,
|
|
729
|
+
closeOnInteractOutside,
|
|
730
|
+
closeOnEscape,
|
|
731
|
+
titleId,
|
|
732
|
+
descriptionId,
|
|
733
|
+
lazyMount: lazyMountProp,
|
|
734
|
+
unmountOnExit: unmountOnExitProp,
|
|
735
|
+
hasAnimationDone,
|
|
736
|
+
closeButtonRef,
|
|
737
|
+
isCloseButtonRendered,
|
|
738
|
+
|
|
739
|
+
triggerProps: buttonProps({
|
|
740
|
+
...stateProps,
|
|
741
|
+
onClick: (e) => {
|
|
742
|
+
if (e.defaultPrevented) return;
|
|
743
|
+
setIsOpen(true);
|
|
744
|
+
},
|
|
745
|
+
}),
|
|
746
|
+
positionerProps: elementProps({
|
|
747
|
+
...stateProps,
|
|
748
|
+
style: {
|
|
749
|
+
pointerEvents: isOpen && modal ? undefined : "none",
|
|
750
|
+
},
|
|
751
|
+
}),
|
|
752
|
+
backdropProps: elementProps({
|
|
753
|
+
...stateProps,
|
|
754
|
+
}),
|
|
755
|
+
titleProps: elementProps({
|
|
756
|
+
id: titleId,
|
|
757
|
+
...stateProps,
|
|
758
|
+
}),
|
|
759
|
+
descriptionProps: elementProps({
|
|
760
|
+
id: descriptionId,
|
|
761
|
+
...stateProps,
|
|
762
|
+
}),
|
|
763
|
+
headerProps: elementProps({
|
|
764
|
+
"data-show-close-button": dataAttr(isCloseButtonRendered),
|
|
765
|
+
...stateProps,
|
|
766
|
+
}),
|
|
767
|
+
closeButtonProps: buttonProps({
|
|
768
|
+
...stateProps,
|
|
769
|
+
onClick: (e) => {
|
|
770
|
+
if (e.defaultPrevented) return;
|
|
771
|
+
setIsOpen(false, { reason: "closeButton", event: e.nativeEvent });
|
|
772
|
+
},
|
|
773
|
+
}),
|
|
774
|
+
}),
|
|
775
|
+
[
|
|
776
|
+
activeSnapPoint,
|
|
777
|
+
snapPoints,
|
|
778
|
+
setActiveSnapPoint,
|
|
779
|
+
onOpenChange,
|
|
780
|
+
dismissible,
|
|
781
|
+
handleOnly,
|
|
782
|
+
isOpen,
|
|
783
|
+
isDragging,
|
|
784
|
+
shouldFade,
|
|
785
|
+
shouldOverlayAnimate,
|
|
786
|
+
closeDrawer,
|
|
787
|
+
modal,
|
|
788
|
+
snapPointsOffset,
|
|
789
|
+
activeSnapPointIndex,
|
|
790
|
+
direction,
|
|
791
|
+
noBodyStyles,
|
|
792
|
+
container,
|
|
793
|
+
autoFocus,
|
|
794
|
+
setIsOpen,
|
|
795
|
+
closeOnInteractOutside,
|
|
796
|
+
closeOnEscape,
|
|
797
|
+
onRelease,
|
|
798
|
+
onDrag,
|
|
799
|
+
onPress,
|
|
800
|
+
titleId,
|
|
801
|
+
descriptionId,
|
|
802
|
+
lazyMountProp,
|
|
803
|
+
unmountOnExitProp,
|
|
804
|
+
hasAnimationDone,
|
|
805
|
+
closeButtonRef,
|
|
806
|
+
isCloseButtonRendered,
|
|
807
|
+
stateProps,
|
|
808
|
+
],
|
|
809
|
+
);
|
|
810
|
+
}
|