@seed-design/react-drawer 1.0.10 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/{Drawer-12s-B-JAOvwq.js → Drawer-12s-5syEwTtq.js} +274 -317
- package/lib/{Drawer-12s-B35ZJcKm.cjs → Drawer-12s-gd4zJm0S.cjs} +271 -333
- package/lib/index.cjs +1 -1
- package/lib/index.d.ts +4075 -56
- package/lib/index.js +2 -2
- package/package.json +9 -5
- package/src/Drawer.test.tsx +111 -0
- package/src/Drawer.tsx +222 -175
- package/src/browser.ts +0 -4
- package/src/useDrawer.test.tsx +54 -40
- package/src/useDrawer.ts +77 -62
- package/src/use-position-fixed.ts +0 -149
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx } from 'react/jsx-runtime';
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import { composeRefs } from '@radix-ui/react-compose-refs';
|
|
4
|
+
import { FocusScope } from '@radix-ui/react-focus-scope';
|
|
5
5
|
import { useCallbackRef } from '@radix-ui/react-use-callback-ref';
|
|
6
|
-
import {
|
|
6
|
+
import { hideOthers } from 'aria-hidden';
|
|
7
|
+
import { DismissibleLayer } from '@seed-design/react-dismissible-layer';
|
|
8
|
+
import { Presence } from '@seed-design/react-presence';
|
|
9
|
+
import { elementProps, dataAttr, buttonProps, mergeProps } from '@seed-design/dom-utils';
|
|
10
|
+
import { usePreventScroll } from '@seed-design/react-prevent-scroll';
|
|
7
11
|
import { Primitive } from '@seed-design/react-primitive';
|
|
8
|
-
import React, {
|
|
12
|
+
import React, { useId, useState, useRef, useCallback, useEffect, useMemo, createContext, useContext, forwardRef } from 'react';
|
|
9
13
|
import { useControllableState } from '@seed-design/react-use-controllable-state';
|
|
10
14
|
|
|
11
15
|
function isMobileFirefox() {
|
|
@@ -18,9 +22,6 @@ function isMac() {
|
|
|
18
22
|
function isIPhone() {
|
|
19
23
|
return testPlatform(/^iPhone/);
|
|
20
24
|
}
|
|
21
|
-
function isSafari() {
|
|
22
|
-
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
23
|
-
}
|
|
24
25
|
function isIPad() {
|
|
25
26
|
return testPlatform(/^iPad/) || // iPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support.
|
|
26
27
|
isMac() && navigator.maxTouchPoints > 1;
|
|
@@ -125,124 +126,6 @@ function dampenValue(v) {
|
|
|
125
126
|
return 8 * (Math.log(v + 1) - 2);
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
// This code comes from https://github.com/emilkowalski/vaul/blob/main/src/use-position-fixed.ts
|
|
129
|
-
let previousBodyPosition = null;
|
|
130
|
-
/**
|
|
131
|
-
* This hook is necessary to prevent buggy behavior on iOS devices (need to test on Android).
|
|
132
|
-
* 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.
|
|
133
|
-
* Issues that this hook solves:
|
|
134
|
-
* https://github.com/emilkowalski/vaul/issues/435
|
|
135
|
-
* https://github.com/emilkowalski/vaul/issues/433
|
|
136
|
-
* And more that I discovered, but were just not reported.
|
|
137
|
-
*/ function usePositionFixed({ isOpen, modal, nested, hasBeenOpened, preventScrollRestoration, noBodyStyles }) {
|
|
138
|
-
const [activeUrl, setActiveUrl] = React.useState(()=>typeof window !== "undefined" ? window.location.href : "");
|
|
139
|
-
const scrollPos = React.useRef(0);
|
|
140
|
-
const setPositionFixed = React.useCallback(()=>{
|
|
141
|
-
// All browsers on iOS will return true here.
|
|
142
|
-
if (!isSafari()) return;
|
|
143
|
-
// If previousBodyPosition is already set, don't set it again.
|
|
144
|
-
if (previousBodyPosition === null && isOpen && !noBodyStyles) {
|
|
145
|
-
previousBodyPosition = {
|
|
146
|
-
position: document.body.style.position,
|
|
147
|
-
top: document.body.style.top,
|
|
148
|
-
left: document.body.style.left,
|
|
149
|
-
height: document.body.style.height,
|
|
150
|
-
right: "unset"
|
|
151
|
-
};
|
|
152
|
-
// Update the dom inside an animation frame
|
|
153
|
-
const { scrollX, innerHeight } = window;
|
|
154
|
-
document.body.style.setProperty("position", "fixed", "important");
|
|
155
|
-
Object.assign(document.body.style, {
|
|
156
|
-
top: `${-scrollPos.current}px`,
|
|
157
|
-
left: `${-scrollX}px`,
|
|
158
|
-
right: "0px",
|
|
159
|
-
height: "auto"
|
|
160
|
-
});
|
|
161
|
-
window.setTimeout(()=>window.requestAnimationFrame(()=>{
|
|
162
|
-
// Attempt to check if the bottom bar appeared due to the position change
|
|
163
|
-
const bottomBarHeight = innerHeight - window.innerHeight;
|
|
164
|
-
if (bottomBarHeight && scrollPos.current >= innerHeight) {
|
|
165
|
-
// Move the content further up so that the bottom bar doesn't hide it
|
|
166
|
-
document.body.style.top = `${-(scrollPos.current + bottomBarHeight)}px`;
|
|
167
|
-
}
|
|
168
|
-
}), 300);
|
|
169
|
-
}
|
|
170
|
-
}, [
|
|
171
|
-
isOpen
|
|
172
|
-
]);
|
|
173
|
-
const restorePositionSetting = React.useCallback(()=>{
|
|
174
|
-
// All browsers on iOS will return true here.
|
|
175
|
-
if (!isSafari()) return;
|
|
176
|
-
if (previousBodyPosition !== null && !noBodyStyles) {
|
|
177
|
-
// Convert the position from "px" to Int
|
|
178
|
-
const y = -parseInt(document.body.style.top, 10);
|
|
179
|
-
const x = -parseInt(document.body.style.left, 10);
|
|
180
|
-
// Restore styles
|
|
181
|
-
Object.assign(document.body.style, previousBodyPosition);
|
|
182
|
-
window.requestAnimationFrame(()=>{
|
|
183
|
-
if (preventScrollRestoration && activeUrl !== window.location.href) {
|
|
184
|
-
setActiveUrl(window.location.href);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
window.scrollTo(x, y);
|
|
188
|
-
});
|
|
189
|
-
previousBodyPosition = null;
|
|
190
|
-
}
|
|
191
|
-
}, [
|
|
192
|
-
activeUrl
|
|
193
|
-
]);
|
|
194
|
-
React.useEffect(()=>{
|
|
195
|
-
function onScroll() {
|
|
196
|
-
scrollPos.current = window.scrollY;
|
|
197
|
-
}
|
|
198
|
-
onScroll();
|
|
199
|
-
window.addEventListener("scroll", onScroll);
|
|
200
|
-
return ()=>{
|
|
201
|
-
window.removeEventListener("scroll", onScroll);
|
|
202
|
-
};
|
|
203
|
-
}, []);
|
|
204
|
-
React.useEffect(()=>{
|
|
205
|
-
if (!modal) return;
|
|
206
|
-
return ()=>{
|
|
207
|
-
if (typeof document === "undefined") return;
|
|
208
|
-
// Another drawer is opened, safe to ignore the execution
|
|
209
|
-
const hasDrawerOpened = !!document.querySelector("[data-drawer]");
|
|
210
|
-
if (hasDrawerOpened) return;
|
|
211
|
-
restorePositionSetting();
|
|
212
|
-
};
|
|
213
|
-
}, [
|
|
214
|
-
modal,
|
|
215
|
-
restorePositionSetting
|
|
216
|
-
]);
|
|
217
|
-
React.useEffect(()=>{
|
|
218
|
-
if (nested || !hasBeenOpened) return;
|
|
219
|
-
// This is needed to force Safari toolbar to show **before** the drawer starts animating to prevent a gnarly shift from happening
|
|
220
|
-
if (isOpen) {
|
|
221
|
-
// avoid for standalone mode (PWA)
|
|
222
|
-
const isStandalone = window.matchMedia("(display-mode: standalone)").matches;
|
|
223
|
-
!isStandalone && setPositionFixed();
|
|
224
|
-
if (!modal) {
|
|
225
|
-
window.setTimeout(()=>{
|
|
226
|
-
restorePositionSetting();
|
|
227
|
-
}, 500);
|
|
228
|
-
}
|
|
229
|
-
} else {
|
|
230
|
-
restorePositionSetting();
|
|
231
|
-
}
|
|
232
|
-
}, [
|
|
233
|
-
isOpen,
|
|
234
|
-
hasBeenOpened,
|
|
235
|
-
activeUrl,
|
|
236
|
-
modal,
|
|
237
|
-
nested,
|
|
238
|
-
setPositionFixed,
|
|
239
|
-
restorePositionSetting
|
|
240
|
-
]);
|
|
241
|
-
return {
|
|
242
|
-
restorePositionSetting
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
129
|
function useSnapPoints({ activeSnapPointProp, setActiveSnapPointProp, snapPoints, drawerRef, overlayRef, fadeFromIndex, onSnapPointChange, direction = "bottom", container, snapToSequentialPoint }) {
|
|
247
130
|
const [activeSnapPoint, setActiveSnapPoint] = useControllableState({
|
|
248
131
|
prop: activeSnapPointProp,
|
|
@@ -468,38 +351,24 @@ function useSnapPoints({ activeSnapPointProp, setActiveSnapPointProp, snapPoints
|
|
|
468
351
|
}
|
|
469
352
|
|
|
470
353
|
function useDrawer(props) {
|
|
471
|
-
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,
|
|
354
|
+
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, direction = "bottom", defaultOpen = false, snapToSequentialPoint = false, repositionInputs = true, onAnimationEnd, container, autoFocus = true, closeOnInteractOutside = true, closeOnEscape = true, lazyMount: lazyMountProp = false, unmountOnExit: unmountOnExitProp = false } = props;
|
|
355
|
+
const drawerId = useId();
|
|
356
|
+
const titleId = `${drawerId}-title`;
|
|
357
|
+
const descriptionId = `${drawerId}-description`;
|
|
472
358
|
const [isOpen = false, setIsOpen] = useControllableState({
|
|
473
359
|
defaultProp: defaultOpen,
|
|
474
360
|
prop: openProp,
|
|
475
361
|
onChange: (o, details)=>{
|
|
476
362
|
onOpenChange?.(o, details);
|
|
477
|
-
if (!o && !nested) {
|
|
478
|
-
restorePositionSetting();
|
|
479
|
-
}
|
|
480
363
|
setTimeout(()=>{
|
|
481
364
|
onAnimationEnd?.(o);
|
|
482
365
|
}, TRANSITIONS.EXIT_DURATION * 1000);
|
|
483
|
-
if (o && !modal) {
|
|
484
|
-
if (typeof window !== "undefined") {
|
|
485
|
-
window.requestAnimationFrame(()=>{
|
|
486
|
-
document.body.style.pointerEvents = "auto";
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
if (!o) {
|
|
491
|
-
document.body.style.pointerEvents = "auto";
|
|
492
|
-
}
|
|
493
366
|
}
|
|
494
367
|
});
|
|
495
368
|
const [hasBeenOpened, setHasBeenOpened] = useState(false);
|
|
496
369
|
const [hasAnimationDone, setHasAnimationDone] = useState(false);
|
|
497
370
|
const [isDragging, setIsDragging] = useState(false);
|
|
498
371
|
const [shouldOverlayAnimate, setShouldOverlayAnimate] = useState(false);
|
|
499
|
-
const [isCloseButtonRendered, setIsCloseButtonRendered] = useState(false);
|
|
500
|
-
const closeButtonRef = useCallback((node)=>{
|
|
501
|
-
setIsCloseButtonRendered(!!node);
|
|
502
|
-
}, []);
|
|
503
372
|
const overlayRef = useRef(null);
|
|
504
373
|
const openTime = useRef(null);
|
|
505
374
|
const dragStartTime = useRef(null);
|
|
@@ -531,14 +400,6 @@ function useDrawer(props) {
|
|
|
531
400
|
direction,
|
|
532
401
|
snapToSequentialPoint
|
|
533
402
|
});
|
|
534
|
-
const { restorePositionSetting } = usePositionFixed({
|
|
535
|
-
isOpen,
|
|
536
|
-
modal,
|
|
537
|
-
nested: nested ?? false,
|
|
538
|
-
hasBeenOpened,
|
|
539
|
-
preventScrollRestoration,
|
|
540
|
-
noBodyStyles
|
|
541
|
-
});
|
|
542
403
|
function onPress(event) {
|
|
543
404
|
if (!dismissible && !snapPoints) return;
|
|
544
405
|
if (drawerRef.current && !drawerRef.current.contains(event.target)) return;
|
|
@@ -820,15 +681,6 @@ function useDrawer(props) {
|
|
|
820
681
|
repositionInputs,
|
|
821
682
|
fixed
|
|
822
683
|
]);
|
|
823
|
-
useEffect(()=>{
|
|
824
|
-
if (!modal) {
|
|
825
|
-
window.requestAnimationFrame(()=>{
|
|
826
|
-
document.body.style.pointerEvents = "auto";
|
|
827
|
-
});
|
|
828
|
-
}
|
|
829
|
-
}, [
|
|
830
|
-
modal
|
|
831
|
-
]);
|
|
832
684
|
// Effect 1: Track drawer open state
|
|
833
685
|
useEffect(()=>{
|
|
834
686
|
if (isOpen) {
|
|
@@ -869,6 +721,11 @@ function useDrawer(props) {
|
|
|
869
721
|
snapPoints,
|
|
870
722
|
fadeFromIndex
|
|
871
723
|
]);
|
|
724
|
+
const stateProps = useMemo(()=>elementProps({
|
|
725
|
+
"data-open": dataAttr(isOpen)
|
|
726
|
+
}), [
|
|
727
|
+
isOpen
|
|
728
|
+
]);
|
|
872
729
|
return useMemo(()=>({
|
|
873
730
|
activeSnapPoint,
|
|
874
731
|
snapPoints,
|
|
@@ -891,16 +748,57 @@ function useDrawer(props) {
|
|
|
891
748
|
snapPointsOffset,
|
|
892
749
|
activeSnapPointIndex,
|
|
893
750
|
direction,
|
|
894
|
-
noBodyStyles,
|
|
895
751
|
container,
|
|
896
752
|
autoFocus,
|
|
897
753
|
setHasBeenOpened,
|
|
898
754
|
setIsOpen,
|
|
899
755
|
closeOnInteractOutside,
|
|
900
756
|
closeOnEscape,
|
|
757
|
+
titleId,
|
|
758
|
+
descriptionId,
|
|
759
|
+
lazyMount: lazyMountProp,
|
|
760
|
+
unmountOnExit: unmountOnExitProp,
|
|
901
761
|
hasAnimationDone,
|
|
902
|
-
|
|
903
|
-
|
|
762
|
+
triggerProps: buttonProps({
|
|
763
|
+
...stateProps,
|
|
764
|
+
onClick: (e)=>{
|
|
765
|
+
if (e.defaultPrevented) return;
|
|
766
|
+
setIsOpen(true, {
|
|
767
|
+
reason: "trigger",
|
|
768
|
+
event: e.nativeEvent
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}),
|
|
772
|
+
positionerProps: elementProps({
|
|
773
|
+
...stateProps,
|
|
774
|
+
style: {
|
|
775
|
+
pointerEvents: isOpen && modal ? undefined : "none"
|
|
776
|
+
}
|
|
777
|
+
}),
|
|
778
|
+
backdropProps: elementProps({
|
|
779
|
+
...stateProps
|
|
780
|
+
}),
|
|
781
|
+
titleProps: elementProps({
|
|
782
|
+
id: titleId,
|
|
783
|
+
...stateProps
|
|
784
|
+
}),
|
|
785
|
+
descriptionProps: elementProps({
|
|
786
|
+
id: descriptionId,
|
|
787
|
+
...stateProps
|
|
788
|
+
}),
|
|
789
|
+
headerProps: elementProps({
|
|
790
|
+
...stateProps
|
|
791
|
+
}),
|
|
792
|
+
closeButtonProps: buttonProps({
|
|
793
|
+
...stateProps,
|
|
794
|
+
onClick: (e)=>{
|
|
795
|
+
if (e.defaultPrevented) return;
|
|
796
|
+
setIsOpen(false, {
|
|
797
|
+
reason: "closeButton",
|
|
798
|
+
event: e.nativeEvent
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
})
|
|
904
802
|
}), [
|
|
905
803
|
activeSnapPoint,
|
|
906
804
|
snapPoints,
|
|
@@ -917,7 +815,6 @@ function useDrawer(props) {
|
|
|
917
815
|
snapPointsOffset,
|
|
918
816
|
activeSnapPointIndex,
|
|
919
817
|
direction,
|
|
920
|
-
noBodyStyles,
|
|
921
818
|
container,
|
|
922
819
|
autoFocus,
|
|
923
820
|
setIsOpen,
|
|
@@ -926,9 +823,12 @@ function useDrawer(props) {
|
|
|
926
823
|
onRelease,
|
|
927
824
|
onDrag,
|
|
928
825
|
onPress,
|
|
826
|
+
titleId,
|
|
827
|
+
descriptionId,
|
|
828
|
+
lazyMountProp,
|
|
829
|
+
unmountOnExitProp,
|
|
929
830
|
hasAnimationDone,
|
|
930
|
-
|
|
931
|
-
isCloseButtonRendered
|
|
831
|
+
stateProps
|
|
932
832
|
]);
|
|
933
833
|
}
|
|
934
834
|
|
|
@@ -943,71 +843,83 @@ function useDrawerContext() {
|
|
|
943
843
|
}
|
|
944
844
|
|
|
945
845
|
const DrawerRoot = (props)=>{
|
|
946
|
-
const { children, defaultOpen, dismissible, modal } = props;
|
|
947
846
|
const api = useDrawer(props);
|
|
948
|
-
return /*#__PURE__*/ jsx(
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
onOpenChange: (open)=>{
|
|
952
|
-
if (!dismissible && !open) return;
|
|
953
|
-
if (open) {
|
|
954
|
-
api.setHasBeenOpened(true);
|
|
955
|
-
} else {
|
|
956
|
-
api.closeDrawer(true);
|
|
957
|
-
}
|
|
958
|
-
api.setIsOpen(open);
|
|
959
|
-
},
|
|
960
|
-
modal: modal,
|
|
961
|
-
children: /*#__PURE__*/ jsx(DrawerProvider, {
|
|
962
|
-
value: api,
|
|
963
|
-
children: children
|
|
964
|
-
})
|
|
847
|
+
return /*#__PURE__*/ jsx(DrawerProvider, {
|
|
848
|
+
value: api,
|
|
849
|
+
children: props.children
|
|
965
850
|
});
|
|
966
851
|
};
|
|
967
|
-
const DrawerTrigger =
|
|
852
|
+
const DrawerTrigger = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
853
|
+
const api = useDrawerContext();
|
|
854
|
+
return /*#__PURE__*/ jsx(Primitive.button, {
|
|
855
|
+
ref: ref,
|
|
856
|
+
...mergeProps(api.triggerProps, props)
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
DrawerTrigger.displayName = "DrawerTrigger";
|
|
968
860
|
const DrawerPositioner = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
969
861
|
const api = useDrawerContext();
|
|
970
862
|
return /*#__PURE__*/ jsx(Primitive.div, {
|
|
971
863
|
ref: ref,
|
|
972
|
-
...props
|
|
973
|
-
style: {
|
|
974
|
-
pointerEvents: api.isOpen ? undefined : "none",
|
|
975
|
-
...props.style
|
|
976
|
-
}
|
|
864
|
+
...mergeProps(api.positionerProps, props)
|
|
977
865
|
});
|
|
978
866
|
});
|
|
979
867
|
DrawerPositioner.displayName = "DrawerPositioner";
|
|
980
868
|
const DrawerBackdrop = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
981
|
-
const { overlayRef, onRelease, modal, snapPoints, isOpen, shouldFade, shouldOverlayAnimate, hasAnimationDone } = useDrawerContext();
|
|
982
|
-
const composedRef =
|
|
869
|
+
const { overlayRef, onRelease, modal, snapPoints, isOpen, shouldFade, shouldOverlayAnimate, hasAnimationDone, lazyMount, unmountOnExit } = useDrawerContext();
|
|
870
|
+
const composedRef = composeRefs(ref, overlayRef);
|
|
983
871
|
const hasSnapPoints = snapPoints && snapPoints.length > 0;
|
|
984
872
|
const onMouseUp = useCallbackRef((event)=>onRelease(event));
|
|
985
873
|
if (!modal) {
|
|
986
874
|
return null;
|
|
987
875
|
}
|
|
988
|
-
return /*#__PURE__*/ jsx(
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
876
|
+
return /*#__PURE__*/ jsx(Presence, {
|
|
877
|
+
present: isOpen,
|
|
878
|
+
unmountOnExit: unmountOnExit,
|
|
879
|
+
lazyMount: lazyMount,
|
|
880
|
+
children: /*#__PURE__*/ jsx(Primitive.div, {
|
|
881
|
+
ref: composedRef,
|
|
882
|
+
onMouseUp: onMouseUp,
|
|
883
|
+
"data-snap-points": isOpen && hasSnapPoints ? "true" : "false",
|
|
884
|
+
"data-snap-points-overlay": isOpen && shouldFade ? "true" : "false",
|
|
885
|
+
"data-should-overlay-animate": shouldOverlayAnimate ? "true" : "false",
|
|
886
|
+
"data-open": dataAttr(isOpen),
|
|
887
|
+
"data-animation-done": hasAnimationDone ? "true" : "false",
|
|
888
|
+
...props
|
|
889
|
+
})
|
|
997
890
|
});
|
|
998
891
|
});
|
|
999
892
|
DrawerBackdrop.displayName = "DrawerBackdrop";
|
|
1000
893
|
const DrawerContent = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
1001
|
-
const {
|
|
1002
|
-
const { drawerRef, onPress, onRelease, onDrag, keyboardIsOpen, snapPointsOffset, activeSnapPointIndex, modal, isOpen, direction, snapPoints, container, handleOnly, autoFocus, closeDrawer, closeOnInteractOutside, closeOnEscape, dismissible, hasAnimationDone } = useDrawerContext();
|
|
894
|
+
const { style, ...restProps } = props;
|
|
895
|
+
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();
|
|
896
|
+
// Lock body scroll while the modal drawer is open. Mirrors the `modal` contract: the lock
|
|
897
|
+
// releases the moment the drawer is non-modal or closed.
|
|
898
|
+
usePreventScroll({
|
|
899
|
+
isDisabled: !(modal && isOpen)
|
|
900
|
+
});
|
|
901
|
+
const [contentNode, setContentNode] = useState(null);
|
|
902
|
+
const contentRef = useCallback((el)=>setContentNode(el), []);
|
|
903
|
+
// aria-hide everything except the content when modal
|
|
904
|
+
useEffect(()=>{
|
|
905
|
+
if (!isOpen || !modal || !contentNode) return;
|
|
906
|
+
return hideOthers(contentNode);
|
|
907
|
+
}, [
|
|
908
|
+
isOpen,
|
|
909
|
+
modal,
|
|
910
|
+
contentNode
|
|
911
|
+
]);
|
|
1003
912
|
// Needed to use transition instead of animations
|
|
1004
913
|
const [delayedSnapPoints, setDelayedSnapPoints] = useState(false);
|
|
1005
|
-
const composedRef = useComposedRefs(ref, drawerRef);
|
|
1006
914
|
const pointerStartRef = useRef(null);
|
|
1007
915
|
const lastKnownPointerEventRef = useRef(null);
|
|
1008
916
|
const wasBeyondThePointRef = useRef(false);
|
|
917
|
+
// Whether the current gesture moved past the swipe-start threshold. Used to
|
|
918
|
+
// suppress the trailing click so dragging the sheet doesn't also activate the
|
|
919
|
+
// element the gesture started on (iOS still synthesizes a click on release).
|
|
920
|
+
const draggedRef = useRef(false);
|
|
1009
921
|
const hasSnapPoints = snapPoints && snapPoints.length > 0;
|
|
1010
|
-
const isDeltaInDirection = (delta,
|
|
922
|
+
const isDeltaInDirection = (delta, dir, threshold = 0)=>{
|
|
1011
923
|
if (wasBeyondThePointRef.current) return true;
|
|
1012
924
|
const deltaY = Math.abs(delta.y);
|
|
1013
925
|
const deltaX = Math.abs(delta.x);
|
|
@@ -1015,8 +927,8 @@ const DrawerContent = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
|
1015
927
|
const dFactor = [
|
|
1016
928
|
"bottom",
|
|
1017
929
|
"right"
|
|
1018
|
-
].includes(
|
|
1019
|
-
if (
|
|
930
|
+
].includes(dir) ? 1 : -1;
|
|
931
|
+
if (dir === "left" || dir === "right") {
|
|
1020
932
|
const isReverseDirection = delta.x * dFactor < 0;
|
|
1021
933
|
if (!isReverseDirection && deltaX >= 0 && deltaX <= threshold) {
|
|
1022
934
|
return isDeltaX;
|
|
@@ -1042,132 +954,177 @@ const DrawerContent = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
|
1042
954
|
wasBeyondThePointRef.current = false;
|
|
1043
955
|
onRelease(event);
|
|
1044
956
|
}
|
|
1045
|
-
return /*#__PURE__*/ jsx(
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
ref: composedRef,
|
|
1055
|
-
style: snapPointsOffset && snapPointsOffset.length > 0 ? {
|
|
1056
|
-
"--snap-point-height": `${snapPointsOffset[activeSnapPointIndex ?? 0]}px`,
|
|
1057
|
-
...style
|
|
1058
|
-
} : style ?? {},
|
|
1059
|
-
onPointerDown: (event)=>{
|
|
1060
|
-
if (handleOnly) return;
|
|
1061
|
-
restProps.onPointerDown?.(event);
|
|
1062
|
-
pointerStartRef.current = {
|
|
1063
|
-
x: event.pageX,
|
|
1064
|
-
y: event.pageY
|
|
1065
|
-
};
|
|
1066
|
-
onPress(event);
|
|
1067
|
-
},
|
|
1068
|
-
onOpenAutoFocus: (e)=>{
|
|
1069
|
-
onOpenAutoFocus?.(e);
|
|
1070
|
-
if (!autoFocus) {
|
|
1071
|
-
e.preventDefault();
|
|
1072
|
-
}
|
|
1073
|
-
},
|
|
1074
|
-
onPointerDownOutside: (e)=>{
|
|
1075
|
-
onPointerDownOutside?.(e);
|
|
1076
|
-
if (!modal || e.defaultPrevented) {
|
|
1077
|
-
e.preventDefault();
|
|
1078
|
-
return;
|
|
1079
|
-
}
|
|
1080
|
-
if (keyboardIsOpen.current) {
|
|
1081
|
-
keyboardIsOpen.current = false;
|
|
1082
|
-
}
|
|
1083
|
-
},
|
|
1084
|
-
onFocusOutside: (e)=>{
|
|
1085
|
-
props.onFocusOutside?.(e);
|
|
1086
|
-
// Always prevent focusOutside to avoid conflicts when focus moves between modals
|
|
1087
|
-
// (e.g., when Dialog closes and restores focus while BottomSheet is opening)
|
|
1088
|
-
e.preventDefault();
|
|
1089
|
-
},
|
|
1090
|
-
onPointerMove: (event)=>{
|
|
1091
|
-
lastKnownPointerEventRef.current = event;
|
|
1092
|
-
if (handleOnly) return;
|
|
1093
|
-
restProps.onPointerMove?.(event);
|
|
1094
|
-
if (!pointerStartRef.current) return;
|
|
1095
|
-
const yPosition = event.pageY - pointerStartRef.current.y;
|
|
1096
|
-
const xPosition = event.pageX - pointerStartRef.current.x;
|
|
1097
|
-
const swipeStartThreshold = event.pointerType === "touch" ? 10 : 2;
|
|
1098
|
-
const delta = {
|
|
1099
|
-
x: xPosition,
|
|
1100
|
-
y: yPosition
|
|
1101
|
-
};
|
|
1102
|
-
const isAllowedToSwipe = isDeltaInDirection(delta, direction, swipeStartThreshold);
|
|
1103
|
-
if (isAllowedToSwipe) onDrag(event);
|
|
1104
|
-
else if (Math.abs(xPosition) > swipeStartThreshold || Math.abs(yPosition) > swipeStartThreshold) {
|
|
1105
|
-
pointerStartRef.current = null;
|
|
1106
|
-
}
|
|
1107
|
-
},
|
|
1108
|
-
onPointerUp: (event)=>{
|
|
1109
|
-
restProps.onPointerUp?.(event);
|
|
1110
|
-
pointerStartRef.current = null;
|
|
1111
|
-
wasBeyondThePointRef.current = false;
|
|
1112
|
-
onRelease(event);
|
|
1113
|
-
},
|
|
1114
|
-
onPointerOut: (event)=>{
|
|
1115
|
-
restProps.onPointerOut?.(event);
|
|
1116
|
-
handleOnPointerUp(lastKnownPointerEventRef.current);
|
|
1117
|
-
},
|
|
1118
|
-
onContextMenu: (event)=>{
|
|
1119
|
-
restProps.onContextMenu?.(event);
|
|
1120
|
-
if (lastKnownPointerEventRef.current) {
|
|
1121
|
-
handleOnPointerUp(lastKnownPointerEventRef.current);
|
|
1122
|
-
}
|
|
1123
|
-
},
|
|
1124
|
-
onInteractOutside: (e)=>{
|
|
1125
|
-
// Only close if event is not prevented (e.g., by onFocusOutside or onPointerDownOutside)
|
|
1126
|
-
if (dismissible && closeOnInteractOutside && !e.defaultPrevented) {
|
|
957
|
+
return /*#__PURE__*/ jsx(Presence, {
|
|
958
|
+
present: isOpen,
|
|
959
|
+
unmountOnExit: unmountOnExit,
|
|
960
|
+
lazyMount: lazyMount,
|
|
961
|
+
children: /*#__PURE__*/ jsx(DismissibleLayer, {
|
|
962
|
+
enabled: isOpen,
|
|
963
|
+
onEscapeKeyDown: (e)=>{
|
|
964
|
+
if (e.defaultPrevented) return;
|
|
965
|
+
if (!dismissible || !closeOnEscape) return;
|
|
1127
966
|
closeDrawer(false, {
|
|
1128
|
-
reason: "
|
|
1129
|
-
event: e
|
|
967
|
+
reason: "escapeKeyDown",
|
|
968
|
+
event: e
|
|
1130
969
|
});
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
970
|
+
},
|
|
971
|
+
onPressOutside: (e)=>{
|
|
972
|
+
if (e.defaultPrevented) return;
|
|
973
|
+
if (!modal) return;
|
|
974
|
+
if (keyboardIsOpen.current) keyboardIsOpen.current = false;
|
|
975
|
+
if (!dismissible || !closeOnInteractOutside) return;
|
|
1136
976
|
closeDrawer(false, {
|
|
1137
|
-
reason: "
|
|
977
|
+
reason: "interactOutside",
|
|
1138
978
|
event: e
|
|
1139
979
|
});
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
|
|
980
|
+
},
|
|
981
|
+
onFocusOutside: ()=>{
|
|
982
|
+
// Focus trapping is handled by FocusScope — nothing to do here.
|
|
983
|
+
// The old Radix architecture needed e.preventDefault() here (PR #1187)
|
|
984
|
+
// to prevent cascade closes, but the new DismissibleLayer handles
|
|
985
|
+
// that via onCascadeDismiss instead.
|
|
986
|
+
},
|
|
987
|
+
onCascadeDismiss: ({ dismissedParent })=>{
|
|
988
|
+
closeDrawer(false, {
|
|
989
|
+
reason: "cascadeDismiss",
|
|
990
|
+
dismissedParent
|
|
991
|
+
});
|
|
992
|
+
},
|
|
993
|
+
children: /*#__PURE__*/ jsx(FocusScope, {
|
|
994
|
+
asChild: true,
|
|
995
|
+
loop: modal,
|
|
996
|
+
trapped: modal && isOpen,
|
|
997
|
+
onMountAutoFocus: (e)=>{
|
|
998
|
+
// prevent FocusScope's default autoFocus behavior
|
|
999
|
+
e.preventDefault();
|
|
1000
|
+
// when autoFocus is true, FocusScope sets the focus to the first tabbable element; otherwise content;
|
|
1001
|
+
// the desired behavior is to set the focus to the content regardless of whether there are tabbable elements or not when true]
|
|
1002
|
+
if (autoFocus) {
|
|
1003
|
+
drawerRef.current?.focus();
|
|
1004
|
+
}
|
|
1005
|
+
// later, we can do something like:
|
|
1006
|
+
// when trigger has clicked using keyboard, focus the first tabbable element;
|
|
1007
|
+
// when trigger has clicked using mouse, focus the content
|
|
1008
|
+
// -> matches Menu behavior
|
|
1009
|
+
},
|
|
1010
|
+
children: /*#__PURE__*/ jsx(Primitive.div, {
|
|
1011
|
+
role: "dialog",
|
|
1012
|
+
"aria-modal": modal,
|
|
1013
|
+
"aria-labelledby": titleId,
|
|
1014
|
+
"aria-describedby": descriptionId,
|
|
1015
|
+
"data-delayed-snap-points": delayedSnapPoints ? "true" : "false",
|
|
1016
|
+
"data-drawer-direction": direction,
|
|
1017
|
+
"data-open": dataAttr(isOpen),
|
|
1018
|
+
"data-animation-done": hasAnimationDone ? "true" : "false",
|
|
1019
|
+
"data-drawer": "",
|
|
1020
|
+
"data-snap-points": isOpen && hasSnapPoints ? "true" : "false",
|
|
1021
|
+
"data-custom-container": container ? "true" : "false",
|
|
1022
|
+
...restProps,
|
|
1023
|
+
ref: composeRefs(ref, drawerRef, contentRef),
|
|
1024
|
+
style: {
|
|
1025
|
+
...snapPointsOffset && snapPointsOffset.length > 0 ? {
|
|
1026
|
+
"--snap-point-height": `${snapPointsOffset[activeSnapPointIndex ?? 0]}px`,
|
|
1027
|
+
...style
|
|
1028
|
+
} : style,
|
|
1029
|
+
...!modal && {
|
|
1030
|
+
pointerEvents: "auto"
|
|
1031
|
+
}
|
|
1032
|
+
},
|
|
1033
|
+
onPointerDown: (event)=>{
|
|
1034
|
+
if (handleOnly) return;
|
|
1035
|
+
restProps.onPointerDown?.(event);
|
|
1036
|
+
pointerStartRef.current = {
|
|
1037
|
+
x: event.pageX,
|
|
1038
|
+
y: event.pageY
|
|
1039
|
+
};
|
|
1040
|
+
draggedRef.current = false;
|
|
1041
|
+
onPress(event);
|
|
1042
|
+
},
|
|
1043
|
+
onPointerMove: (event)=>{
|
|
1044
|
+
lastKnownPointerEventRef.current = event;
|
|
1045
|
+
if (handleOnly) return;
|
|
1046
|
+
restProps.onPointerMove?.(event);
|
|
1047
|
+
if (!pointerStartRef.current) return;
|
|
1048
|
+
const yPosition = event.pageY - pointerStartRef.current.y;
|
|
1049
|
+
const xPosition = event.pageX - pointerStartRef.current.x;
|
|
1050
|
+
const swipeStartThreshold = event.pointerType === "touch" ? 10 : 2;
|
|
1051
|
+
const delta = {
|
|
1052
|
+
x: xPosition,
|
|
1053
|
+
y: yPosition
|
|
1054
|
+
};
|
|
1055
|
+
// Once the pointer travels past the swipe-start threshold the gesture
|
|
1056
|
+
// is a drag, not a tap — remember it so the trailing click can be
|
|
1057
|
+
// suppressed on release (iOS still synthesizes one on the origin).
|
|
1058
|
+
const isBeyondThreshold = Math.abs(xPosition) > swipeStartThreshold || Math.abs(yPosition) > swipeStartThreshold;
|
|
1059
|
+
if (isBeyondThreshold) draggedRef.current = true;
|
|
1060
|
+
const isAllowedToSwipe = isDeltaInDirection(delta, direction, swipeStartThreshold);
|
|
1061
|
+
if (isAllowedToSwipe) onDrag(event);
|
|
1062
|
+
else if (isBeyondThreshold) {
|
|
1063
|
+
pointerStartRef.current = null;
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
onPointerUp: (event)=>{
|
|
1067
|
+
restProps.onPointerUp?.(event);
|
|
1068
|
+
pointerStartRef.current = null;
|
|
1069
|
+
wasBeyondThePointRef.current = false;
|
|
1070
|
+
onRelease(event);
|
|
1071
|
+
},
|
|
1072
|
+
onPointerOut: (event)=>{
|
|
1073
|
+
restProps.onPointerOut?.(event);
|
|
1074
|
+
handleOnPointerUp(lastKnownPointerEventRef.current);
|
|
1075
|
+
},
|
|
1076
|
+
onContextMenu: (event)=>{
|
|
1077
|
+
restProps.onContextMenu?.(event);
|
|
1078
|
+
if (lastKnownPointerEventRef.current) {
|
|
1079
|
+
handleOnPointerUp(lastKnownPointerEventRef.current);
|
|
1080
|
+
}
|
|
1081
|
+
},
|
|
1082
|
+
onClickCapture: (event)=>{
|
|
1083
|
+
restProps.onClickCapture?.(event);
|
|
1084
|
+
// Swallow the click that follows a drag so dragging the sheet (e.g. a
|
|
1085
|
+
// swipe-to-dismiss started on an item) doesn't also activate that item.
|
|
1086
|
+
// A plain tap leaves draggedRef false and clicks normally.
|
|
1087
|
+
if (draggedRef.current) {
|
|
1088
|
+
event.preventDefault();
|
|
1089
|
+
event.stopPropagation();
|
|
1090
|
+
draggedRef.current = false;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
})
|
|
1094
|
+
})
|
|
1095
|
+
})
|
|
1143
1096
|
});
|
|
1144
1097
|
});
|
|
1145
1098
|
DrawerContent.displayName = "DrawerContent";
|
|
1146
|
-
const DrawerTitle =
|
|
1147
|
-
const
|
|
1099
|
+
const DrawerTitle = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
1100
|
+
const api = useDrawerContext();
|
|
1101
|
+
return /*#__PURE__*/ jsx(Primitive.h2, {
|
|
1102
|
+
ref: ref,
|
|
1103
|
+
...mergeProps(api.titleProps, props)
|
|
1104
|
+
});
|
|
1105
|
+
});
|
|
1106
|
+
DrawerTitle.displayName = "DrawerTitle";
|
|
1107
|
+
const DrawerDescription = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
1108
|
+
const api = useDrawerContext();
|
|
1109
|
+
return /*#__PURE__*/ jsx(Primitive.p, {
|
|
1110
|
+
ref: ref,
|
|
1111
|
+
...mergeProps(api.descriptionProps, props)
|
|
1112
|
+
});
|
|
1113
|
+
});
|
|
1114
|
+
DrawerDescription.displayName = "DrawerDescription";
|
|
1148
1115
|
const DrawerHeader = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
1149
|
-
const
|
|
1116
|
+
const api = useDrawerContext();
|
|
1150
1117
|
return /*#__PURE__*/ jsx(Primitive.div, {
|
|
1151
1118
|
ref: ref,
|
|
1152
|
-
|
|
1153
|
-
...props
|
|
1119
|
+
...mergeProps(api.headerProps, props)
|
|
1154
1120
|
});
|
|
1155
1121
|
});
|
|
1156
1122
|
DrawerHeader.displayName = "DrawerHeader";
|
|
1157
1123
|
const DrawerCloseButton = /*#__PURE__*/ forwardRef((props, ref)=>{
|
|
1158
|
-
const
|
|
1159
|
-
const composedRef = useComposedRefs(ref, closeButtonRef);
|
|
1124
|
+
const api = useDrawerContext();
|
|
1160
1125
|
return /*#__PURE__*/ jsx(Primitive.button, {
|
|
1161
|
-
ref:
|
|
1162
|
-
...props
|
|
1163
|
-
onClick: (e)=>{
|
|
1164
|
-
props.onClick?.(e);
|
|
1165
|
-
if (e.defaultPrevented) return;
|
|
1166
|
-
setIsOpen(false, {
|
|
1167
|
-
reason: "closeButton",
|
|
1168
|
-
event: e.nativeEvent
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1126
|
+
ref: ref,
|
|
1127
|
+
...mergeProps(api.closeButtonProps, props)
|
|
1171
1128
|
});
|
|
1172
1129
|
});
|
|
1173
1130
|
const LONG_HANDLE_PRESS_TIMEOUT = 250;
|