@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.
@@ -1,34 +1,19 @@
1
1
  'use client';
2
2
  var jsxRuntime = require('react/jsx-runtime');
3
3
  var reactComposeRefs = require('@radix-ui/react-compose-refs');
4
- var DialogPrimitive = require('@radix-ui/react-dialog');
4
+ var reactFocusScope = require('@radix-ui/react-focus-scope');
5
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');
6
9
  var domUtils = require('@seed-design/dom-utils');
10
+ var reactPreventScroll = require('@seed-design/react-prevent-scroll');
7
11
  var reactPrimitive = require('@seed-design/react-primitive');
8
12
  var React = require('react');
9
13
  var reactUseControllableState = require('@seed-design/react-use-controllable-state');
10
14
 
11
15
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
16
 
13
- function _interopNamespace(e) {
14
- if (e && e.__esModule) return e;
15
- var n = Object.create(null);
16
- if (e) {
17
- Object.keys(e).forEach(function (k) {
18
- if (k !== 'default') {
19
- var d = Object.getOwnPropertyDescriptor(e, k);
20
- Object.defineProperty(n, k, d.get ? d : {
21
- enumerable: true,
22
- get: function () { return e[k]; }
23
- });
24
- }
25
- });
26
- }
27
- n.default = e;
28
- return n;
29
- }
30
-
31
- var DialogPrimitive__namespace = /*#__PURE__*/_interopNamespace(DialogPrimitive);
32
17
  var React__default = /*#__PURE__*/_interopDefault(React);
33
18
 
34
19
  function isMobileFirefox() {
@@ -41,9 +26,6 @@ function isMac() {
41
26
  function isIPhone() {
42
27
  return testPlatform(/^iPhone/);
43
28
  }
44
- function isSafari() {
45
- return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
46
- }
47
29
  function isIPad() {
48
30
  return testPlatform(/^iPad/) || // iPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support.
49
31
  isMac() && navigator.maxTouchPoints > 1;
@@ -148,124 +130,6 @@ function dampenValue(v) {
148
130
  return 8 * (Math.log(v + 1) - 2);
149
131
  }
150
132
 
151
- // This code comes from https://github.com/emilkowalski/vaul/blob/main/src/use-position-fixed.ts
152
- let previousBodyPosition = null;
153
- /**
154
- * This hook is necessary to prevent buggy behavior on iOS devices (need to test on Android).
155
- * 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.
156
- * Issues that this hook solves:
157
- * https://github.com/emilkowalski/vaul/issues/435
158
- * https://github.com/emilkowalski/vaul/issues/433
159
- * And more that I discovered, but were just not reported.
160
- */ function usePositionFixed({ isOpen, modal, nested, hasBeenOpened, preventScrollRestoration, noBodyStyles }) {
161
- const [activeUrl, setActiveUrl] = React__default.default.useState(()=>typeof window !== "undefined" ? window.location.href : "");
162
- const scrollPos = React__default.default.useRef(0);
163
- const setPositionFixed = React__default.default.useCallback(()=>{
164
- // All browsers on iOS will return true here.
165
- if (!isSafari()) return;
166
- // If previousBodyPosition is already set, don't set it again.
167
- if (previousBodyPosition === null && isOpen && !noBodyStyles) {
168
- previousBodyPosition = {
169
- position: document.body.style.position,
170
- top: document.body.style.top,
171
- left: document.body.style.left,
172
- height: document.body.style.height,
173
- right: "unset"
174
- };
175
- // Update the dom inside an animation frame
176
- const { scrollX, innerHeight } = window;
177
- document.body.style.setProperty("position", "fixed", "important");
178
- Object.assign(document.body.style, {
179
- top: `${-scrollPos.current}px`,
180
- left: `${-scrollX}px`,
181
- right: "0px",
182
- height: "auto"
183
- });
184
- window.setTimeout(()=>window.requestAnimationFrame(()=>{
185
- // Attempt to check if the bottom bar appeared due to the position change
186
- const bottomBarHeight = innerHeight - window.innerHeight;
187
- if (bottomBarHeight && scrollPos.current >= innerHeight) {
188
- // Move the content further up so that the bottom bar doesn't hide it
189
- document.body.style.top = `${-(scrollPos.current + bottomBarHeight)}px`;
190
- }
191
- }), 300);
192
- }
193
- }, [
194
- isOpen
195
- ]);
196
- const restorePositionSetting = React__default.default.useCallback(()=>{
197
- // All browsers on iOS will return true here.
198
- if (!isSafari()) return;
199
- if (previousBodyPosition !== null && !noBodyStyles) {
200
- // Convert the position from "px" to Int
201
- const y = -parseInt(document.body.style.top, 10);
202
- const x = -parseInt(document.body.style.left, 10);
203
- // Restore styles
204
- Object.assign(document.body.style, previousBodyPosition);
205
- window.requestAnimationFrame(()=>{
206
- if (preventScrollRestoration && activeUrl !== window.location.href) {
207
- setActiveUrl(window.location.href);
208
- return;
209
- }
210
- window.scrollTo(x, y);
211
- });
212
- previousBodyPosition = null;
213
- }
214
- }, [
215
- activeUrl
216
- ]);
217
- React__default.default.useEffect(()=>{
218
- function onScroll() {
219
- scrollPos.current = window.scrollY;
220
- }
221
- onScroll();
222
- window.addEventListener("scroll", onScroll);
223
- return ()=>{
224
- window.removeEventListener("scroll", onScroll);
225
- };
226
- }, []);
227
- React__default.default.useEffect(()=>{
228
- if (!modal) return;
229
- return ()=>{
230
- if (typeof document === "undefined") return;
231
- // Another drawer is opened, safe to ignore the execution
232
- const hasDrawerOpened = !!document.querySelector("[data-drawer]");
233
- if (hasDrawerOpened) return;
234
- restorePositionSetting();
235
- };
236
- }, [
237
- modal,
238
- restorePositionSetting
239
- ]);
240
- React__default.default.useEffect(()=>{
241
- if (nested || !hasBeenOpened) return;
242
- // This is needed to force Safari toolbar to show **before** the drawer starts animating to prevent a gnarly shift from happening
243
- if (isOpen) {
244
- // avoid for standalone mode (PWA)
245
- const isStandalone = window.matchMedia("(display-mode: standalone)").matches;
246
- !isStandalone && setPositionFixed();
247
- if (!modal) {
248
- window.setTimeout(()=>{
249
- restorePositionSetting();
250
- }, 500);
251
- }
252
- } else {
253
- restorePositionSetting();
254
- }
255
- }, [
256
- isOpen,
257
- hasBeenOpened,
258
- activeUrl,
259
- modal,
260
- nested,
261
- setPositionFixed,
262
- restorePositionSetting
263
- ]);
264
- return {
265
- restorePositionSetting
266
- };
267
- }
268
-
269
133
  function useSnapPoints({ activeSnapPointProp, setActiveSnapPointProp, snapPoints, drawerRef, overlayRef, fadeFromIndex, onSnapPointChange, direction = "bottom", container, snapToSequentialPoint }) {
270
134
  const [activeSnapPoint, setActiveSnapPoint] = reactUseControllableState.useControllableState({
271
135
  prop: activeSnapPointProp,
@@ -491,38 +355,24 @@ function useSnapPoints({ activeSnapPointProp, setActiveSnapPointProp, snapPoints
491
355
  }
492
356
 
493
357
  function useDrawer(props) {
494
- 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 = false, closeOnInteractOutside = true, closeOnEscape = true } = props;
358
+ 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;
359
+ const drawerId = React.useId();
360
+ const titleId = `${drawerId}-title`;
361
+ const descriptionId = `${drawerId}-description`;
495
362
  const [isOpen = false, setIsOpen] = reactUseControllableState.useControllableState({
496
363
  defaultProp: defaultOpen,
497
364
  prop: openProp,
498
365
  onChange: (o, details)=>{
499
366
  onOpenChange?.(o, details);
500
- if (!o && !nested) {
501
- restorePositionSetting();
502
- }
503
367
  setTimeout(()=>{
504
368
  onAnimationEnd?.(o);
505
369
  }, TRANSITIONS.EXIT_DURATION * 1000);
506
- if (o && !modal) {
507
- if (typeof window !== "undefined") {
508
- window.requestAnimationFrame(()=>{
509
- document.body.style.pointerEvents = "auto";
510
- });
511
- }
512
- }
513
- if (!o) {
514
- document.body.style.pointerEvents = "auto";
515
- }
516
370
  }
517
371
  });
518
372
  const [hasBeenOpened, setHasBeenOpened] = React.useState(false);
519
373
  const [hasAnimationDone, setHasAnimationDone] = React.useState(false);
520
374
  const [isDragging, setIsDragging] = React.useState(false);
521
375
  const [shouldOverlayAnimate, setShouldOverlayAnimate] = React.useState(false);
522
- const [isCloseButtonRendered, setIsCloseButtonRendered] = React.useState(false);
523
- const closeButtonRef = React.useCallback((node)=>{
524
- setIsCloseButtonRendered(!!node);
525
- }, []);
526
376
  const overlayRef = React.useRef(null);
527
377
  const openTime = React.useRef(null);
528
378
  const dragStartTime = React.useRef(null);
@@ -554,14 +404,6 @@ function useDrawer(props) {
554
404
  direction,
555
405
  snapToSequentialPoint
556
406
  });
557
- const { restorePositionSetting } = usePositionFixed({
558
- isOpen,
559
- modal,
560
- nested: nested ?? false,
561
- hasBeenOpened,
562
- preventScrollRestoration,
563
- noBodyStyles
564
- });
565
407
  function onPress(event) {
566
408
  if (!dismissible && !snapPoints) return;
567
409
  if (drawerRef.current && !drawerRef.current.contains(event.target)) return;
@@ -843,15 +685,6 @@ function useDrawer(props) {
843
685
  repositionInputs,
844
686
  fixed
845
687
  ]);
846
- React.useEffect(()=>{
847
- if (!modal) {
848
- window.requestAnimationFrame(()=>{
849
- document.body.style.pointerEvents = "auto";
850
- });
851
- }
852
- }, [
853
- modal
854
- ]);
855
688
  // Effect 1: Track drawer open state
856
689
  React.useEffect(()=>{
857
690
  if (isOpen) {
@@ -892,6 +725,11 @@ function useDrawer(props) {
892
725
  snapPoints,
893
726
  fadeFromIndex
894
727
  ]);
728
+ const stateProps = React.useMemo(()=>domUtils.elementProps({
729
+ "data-open": domUtils.dataAttr(isOpen)
730
+ }), [
731
+ isOpen
732
+ ]);
895
733
  return React.useMemo(()=>({
896
734
  activeSnapPoint,
897
735
  snapPoints,
@@ -914,16 +752,57 @@ function useDrawer(props) {
914
752
  snapPointsOffset,
915
753
  activeSnapPointIndex,
916
754
  direction,
917
- noBodyStyles,
918
755
  container,
919
756
  autoFocus,
920
757
  setHasBeenOpened,
921
758
  setIsOpen,
922
759
  closeOnInteractOutside,
923
760
  closeOnEscape,
761
+ titleId,
762
+ descriptionId,
763
+ lazyMount: lazyMountProp,
764
+ unmountOnExit: unmountOnExitProp,
924
765
  hasAnimationDone,
925
- closeButtonRef,
926
- isCloseButtonRendered
766
+ triggerProps: domUtils.buttonProps({
767
+ ...stateProps,
768
+ onClick: (e)=>{
769
+ if (e.defaultPrevented) return;
770
+ setIsOpen(true, {
771
+ reason: "trigger",
772
+ event: e.nativeEvent
773
+ });
774
+ }
775
+ }),
776
+ positionerProps: domUtils.elementProps({
777
+ ...stateProps,
778
+ style: {
779
+ pointerEvents: isOpen && modal ? undefined : "none"
780
+ }
781
+ }),
782
+ backdropProps: domUtils.elementProps({
783
+ ...stateProps
784
+ }),
785
+ titleProps: domUtils.elementProps({
786
+ id: titleId,
787
+ ...stateProps
788
+ }),
789
+ descriptionProps: domUtils.elementProps({
790
+ id: descriptionId,
791
+ ...stateProps
792
+ }),
793
+ headerProps: domUtils.elementProps({
794
+ ...stateProps
795
+ }),
796
+ closeButtonProps: domUtils.buttonProps({
797
+ ...stateProps,
798
+ onClick: (e)=>{
799
+ if (e.defaultPrevented) return;
800
+ setIsOpen(false, {
801
+ reason: "closeButton",
802
+ event: e.nativeEvent
803
+ });
804
+ }
805
+ })
927
806
  }), [
928
807
  activeSnapPoint,
929
808
  snapPoints,
@@ -940,7 +819,6 @@ function useDrawer(props) {
940
819
  snapPointsOffset,
941
820
  activeSnapPointIndex,
942
821
  direction,
943
- noBodyStyles,
944
822
  container,
945
823
  autoFocus,
946
824
  setIsOpen,
@@ -949,9 +827,12 @@ function useDrawer(props) {
949
827
  onRelease,
950
828
  onDrag,
951
829
  onPress,
830
+ titleId,
831
+ descriptionId,
832
+ lazyMountProp,
833
+ unmountOnExitProp,
952
834
  hasAnimationDone,
953
- closeButtonRef,
954
- isCloseButtonRendered
835
+ stateProps
955
836
  ]);
956
837
  }
957
838
 
@@ -966,71 +847,83 @@ function useDrawerContext() {
966
847
  }
967
848
 
968
849
  const DrawerRoot = (props)=>{
969
- const { children, defaultOpen, dismissible, modal } = props;
970
850
  const api = useDrawer(props);
971
- return /*#__PURE__*/ jsxRuntime.jsx(DialogPrimitive__namespace.Root, {
972
- defaultOpen: defaultOpen,
973
- open: api.isOpen,
974
- onOpenChange: (open)=>{
975
- if (!dismissible && !open) return;
976
- if (open) {
977
- api.setHasBeenOpened(true);
978
- } else {
979
- api.closeDrawer(true);
980
- }
981
- api.setIsOpen(open);
982
- },
983
- modal: modal,
984
- children: /*#__PURE__*/ jsxRuntime.jsx(DrawerProvider, {
985
- value: api,
986
- children: children
987
- })
851
+ return /*#__PURE__*/ jsxRuntime.jsx(DrawerProvider, {
852
+ value: api,
853
+ children: props.children
988
854
  });
989
855
  };
990
- const DrawerTrigger = DialogPrimitive__namespace.Trigger;
856
+ const DrawerTrigger = /*#__PURE__*/ React.forwardRef((props, ref)=>{
857
+ const api = useDrawerContext();
858
+ return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.button, {
859
+ ref: ref,
860
+ ...domUtils.mergeProps(api.triggerProps, props)
861
+ });
862
+ });
863
+ DrawerTrigger.displayName = "DrawerTrigger";
991
864
  const DrawerPositioner = /*#__PURE__*/ React.forwardRef((props, ref)=>{
992
865
  const api = useDrawerContext();
993
866
  return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.div, {
994
867
  ref: ref,
995
- ...props,
996
- style: {
997
- pointerEvents: api.isOpen ? undefined : "none",
998
- ...props.style
999
- }
868
+ ...domUtils.mergeProps(api.positionerProps, props)
1000
869
  });
1001
870
  });
1002
871
  DrawerPositioner.displayName = "DrawerPositioner";
1003
872
  const DrawerBackdrop = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1004
- const { overlayRef, onRelease, modal, snapPoints, isOpen, shouldFade, shouldOverlayAnimate, hasAnimationDone } = useDrawerContext();
1005
- const composedRef = reactComposeRefs.useComposedRefs(ref, overlayRef);
873
+ const { overlayRef, onRelease, modal, snapPoints, isOpen, shouldFade, shouldOverlayAnimate, hasAnimationDone, lazyMount, unmountOnExit } = useDrawerContext();
874
+ const composedRef = reactComposeRefs.composeRefs(ref, overlayRef);
1006
875
  const hasSnapPoints = snapPoints && snapPoints.length > 0;
1007
876
  const onMouseUp = reactUseCallbackRef.useCallbackRef((event)=>onRelease(event));
1008
877
  if (!modal) {
1009
878
  return null;
1010
879
  }
1011
- return /*#__PURE__*/ jsxRuntime.jsx(DialogPrimitive__namespace.Overlay, {
1012
- ref: composedRef,
1013
- onMouseUp: onMouseUp,
1014
- "data-snap-points": isOpen && hasSnapPoints ? "true" : "false",
1015
- "data-snap-points-overlay": isOpen && shouldFade ? "true" : "false",
1016
- "data-should-overlay-animate": shouldOverlayAnimate ? "true" : "false",
1017
- "data-open": domUtils.dataAttr(isOpen),
1018
- "data-animation-done": hasAnimationDone ? "true" : "false",
1019
- ...props
880
+ return /*#__PURE__*/ jsxRuntime.jsx(reactPresence.Presence, {
881
+ present: isOpen,
882
+ unmountOnExit: unmountOnExit,
883
+ lazyMount: lazyMount,
884
+ children: /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.div, {
885
+ ref: composedRef,
886
+ onMouseUp: onMouseUp,
887
+ "data-snap-points": isOpen && hasSnapPoints ? "true" : "false",
888
+ "data-snap-points-overlay": isOpen && shouldFade ? "true" : "false",
889
+ "data-should-overlay-animate": shouldOverlayAnimate ? "true" : "false",
890
+ "data-open": domUtils.dataAttr(isOpen),
891
+ "data-animation-done": hasAnimationDone ? "true" : "false",
892
+ ...props
893
+ })
1020
894
  });
1021
895
  });
1022
896
  DrawerBackdrop.displayName = "DrawerBackdrop";
1023
897
  const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1024
- const { onPointerDownOutside, style, onOpenAutoFocus, ...restProps } = props;
1025
- const { drawerRef, onPress, onRelease, onDrag, keyboardIsOpen, snapPointsOffset, activeSnapPointIndex, modal, isOpen, direction, snapPoints, container, handleOnly, autoFocus, closeDrawer, closeOnInteractOutside, closeOnEscape, dismissible, hasAnimationDone } = useDrawerContext();
898
+ const { style, ...restProps } = props;
899
+ 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();
900
+ // Lock body scroll while the modal drawer is open. Mirrors the `modal` contract: the lock
901
+ // releases the moment the drawer is non-modal or closed.
902
+ reactPreventScroll.usePreventScroll({
903
+ isDisabled: !(modal && isOpen)
904
+ });
905
+ const [contentNode, setContentNode] = React.useState(null);
906
+ const contentRef = React.useCallback((el)=>setContentNode(el), []);
907
+ // aria-hide everything except the content when modal
908
+ React.useEffect(()=>{
909
+ if (!isOpen || !modal || !contentNode) return;
910
+ return ariaHidden.hideOthers(contentNode);
911
+ }, [
912
+ isOpen,
913
+ modal,
914
+ contentNode
915
+ ]);
1026
916
  // Needed to use transition instead of animations
1027
917
  const [delayedSnapPoints, setDelayedSnapPoints] = React.useState(false);
1028
- const composedRef = reactComposeRefs.useComposedRefs(ref, drawerRef);
1029
918
  const pointerStartRef = React.useRef(null);
1030
919
  const lastKnownPointerEventRef = React.useRef(null);
1031
920
  const wasBeyondThePointRef = React.useRef(false);
921
+ // Whether the current gesture moved past the swipe-start threshold. Used to
922
+ // suppress the trailing click so dragging the sheet doesn't also activate the
923
+ // element the gesture started on (iOS still synthesizes a click on release).
924
+ const draggedRef = React.useRef(false);
1032
925
  const hasSnapPoints = snapPoints && snapPoints.length > 0;
1033
- const isDeltaInDirection = (delta, direction, threshold = 0)=>{
926
+ const isDeltaInDirection = (delta, dir, threshold = 0)=>{
1034
927
  if (wasBeyondThePointRef.current) return true;
1035
928
  const deltaY = Math.abs(delta.y);
1036
929
  const deltaX = Math.abs(delta.x);
@@ -1038,8 +931,8 @@ const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1038
931
  const dFactor = [
1039
932
  "bottom",
1040
933
  "right"
1041
- ].includes(direction) ? 1 : -1;
1042
- if (direction === "left" || direction === "right") {
934
+ ].includes(dir) ? 1 : -1;
935
+ if (dir === "left" || dir === "right") {
1043
936
  const isReverseDirection = delta.x * dFactor < 0;
1044
937
  if (!isReverseDirection && deltaX >= 0 && deltaX <= threshold) {
1045
938
  return isDeltaX;
@@ -1065,132 +958,177 @@ const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1065
958
  wasBeyondThePointRef.current = false;
1066
959
  onRelease(event);
1067
960
  }
1068
- return /*#__PURE__*/ jsxRuntime.jsx(DialogPrimitive__namespace.Content, {
1069
- "data-delayed-snap-points": delayedSnapPoints ? "true" : "false",
1070
- "data-drawer-direction": direction,
1071
- "data-open": domUtils.dataAttr(isOpen),
1072
- "data-animation-done": hasAnimationDone ? "true" : "false",
1073
- "data-drawer": "",
1074
- "data-snap-points": isOpen && hasSnapPoints ? "true" : "false",
1075
- "data-custom-container": container ? "true" : "false",
1076
- ...restProps,
1077
- ref: composedRef,
1078
- style: snapPointsOffset && snapPointsOffset.length > 0 ? {
1079
- "--snap-point-height": `${snapPointsOffset[activeSnapPointIndex ?? 0]}px`,
1080
- ...style
1081
- } : style ?? {},
1082
- onPointerDown: (event)=>{
1083
- if (handleOnly) return;
1084
- restProps.onPointerDown?.(event);
1085
- pointerStartRef.current = {
1086
- x: event.pageX,
1087
- y: event.pageY
1088
- };
1089
- onPress(event);
1090
- },
1091
- onOpenAutoFocus: (e)=>{
1092
- onOpenAutoFocus?.(e);
1093
- if (!autoFocus) {
1094
- e.preventDefault();
1095
- }
1096
- },
1097
- onPointerDownOutside: (e)=>{
1098
- onPointerDownOutside?.(e);
1099
- if (!modal || e.defaultPrevented) {
1100
- e.preventDefault();
1101
- return;
1102
- }
1103
- if (keyboardIsOpen.current) {
1104
- keyboardIsOpen.current = false;
1105
- }
1106
- },
1107
- onFocusOutside: (e)=>{
1108
- props.onFocusOutside?.(e);
1109
- // Always prevent focusOutside to avoid conflicts when focus moves between modals
1110
- // (e.g., when Dialog closes and restores focus while BottomSheet is opening)
1111
- e.preventDefault();
1112
- },
1113
- onPointerMove: (event)=>{
1114
- lastKnownPointerEventRef.current = event;
1115
- if (handleOnly) return;
1116
- restProps.onPointerMove?.(event);
1117
- if (!pointerStartRef.current) return;
1118
- const yPosition = event.pageY - pointerStartRef.current.y;
1119
- const xPosition = event.pageX - pointerStartRef.current.x;
1120
- const swipeStartThreshold = event.pointerType === "touch" ? 10 : 2;
1121
- const delta = {
1122
- x: xPosition,
1123
- y: yPosition
1124
- };
1125
- const isAllowedToSwipe = isDeltaInDirection(delta, direction, swipeStartThreshold);
1126
- if (isAllowedToSwipe) onDrag(event);
1127
- else if (Math.abs(xPosition) > swipeStartThreshold || Math.abs(yPosition) > swipeStartThreshold) {
1128
- pointerStartRef.current = null;
1129
- }
1130
- },
1131
- onPointerUp: (event)=>{
1132
- restProps.onPointerUp?.(event);
1133
- pointerStartRef.current = null;
1134
- wasBeyondThePointRef.current = false;
1135
- onRelease(event);
1136
- },
1137
- onPointerOut: (event)=>{
1138
- restProps.onPointerOut?.(event);
1139
- handleOnPointerUp(lastKnownPointerEventRef.current);
1140
- },
1141
- onContextMenu: (event)=>{
1142
- restProps.onContextMenu?.(event);
1143
- if (lastKnownPointerEventRef.current) {
1144
- handleOnPointerUp(lastKnownPointerEventRef.current);
1145
- }
1146
- },
1147
- onInteractOutside: (e)=>{
1148
- // Only close if event is not prevented (e.g., by onFocusOutside or onPointerDownOutside)
1149
- if (dismissible && closeOnInteractOutside && !e.defaultPrevented) {
961
+ return /*#__PURE__*/ jsxRuntime.jsx(reactPresence.Presence, {
962
+ present: isOpen,
963
+ unmountOnExit: unmountOnExit,
964
+ lazyMount: lazyMount,
965
+ children: /*#__PURE__*/ jsxRuntime.jsx(reactDismissibleLayer.DismissibleLayer, {
966
+ enabled: isOpen,
967
+ onEscapeKeyDown: (e)=>{
968
+ if (e.defaultPrevented) return;
969
+ if (!dismissible || !closeOnEscape) return;
1150
970
  closeDrawer(false, {
1151
- reason: "interactOutside",
1152
- event: e.detail.originalEvent
971
+ reason: "escapeKeyDown",
972
+ event: e
1153
973
  });
1154
- }
1155
- props.onInteractOutside?.(e);
1156
- },
1157
- onEscapeKeyDown: (e)=>{
1158
- if (dismissible && closeOnEscape) {
974
+ },
975
+ onPressOutside: (e)=>{
976
+ if (e.defaultPrevented) return;
977
+ if (!modal) return;
978
+ if (keyboardIsOpen.current) keyboardIsOpen.current = false;
979
+ if (!dismissible || !closeOnInteractOutside) return;
1159
980
  closeDrawer(false, {
1160
- reason: "escapeKeyDown",
981
+ reason: "interactOutside",
1161
982
  event: e
1162
983
  });
1163
- }
1164
- props.onEscapeKeyDown?.(e);
1165
- }
984
+ },
985
+ onFocusOutside: ()=>{
986
+ // Focus trapping is handled by FocusScope — nothing to do here.
987
+ // The old Radix architecture needed e.preventDefault() here (PR #1187)
988
+ // to prevent cascade closes, but the new DismissibleLayer handles
989
+ // that via onCascadeDismiss instead.
990
+ },
991
+ onCascadeDismiss: ({ dismissedParent })=>{
992
+ closeDrawer(false, {
993
+ reason: "cascadeDismiss",
994
+ dismissedParent
995
+ });
996
+ },
997
+ children: /*#__PURE__*/ jsxRuntime.jsx(reactFocusScope.FocusScope, {
998
+ asChild: true,
999
+ loop: modal,
1000
+ trapped: modal && isOpen,
1001
+ onMountAutoFocus: (e)=>{
1002
+ // prevent FocusScope's default autoFocus behavior
1003
+ e.preventDefault();
1004
+ // when autoFocus is true, FocusScope sets the focus to the first tabbable element; otherwise content;
1005
+ // the desired behavior is to set the focus to the content regardless of whether there are tabbable elements or not when true]
1006
+ if (autoFocus) {
1007
+ drawerRef.current?.focus();
1008
+ }
1009
+ // later, we can do something like:
1010
+ // when trigger has clicked using keyboard, focus the first tabbable element;
1011
+ // when trigger has clicked using mouse, focus the content
1012
+ // -> matches Menu behavior
1013
+ },
1014
+ children: /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.div, {
1015
+ role: "dialog",
1016
+ "aria-modal": modal,
1017
+ "aria-labelledby": titleId,
1018
+ "aria-describedby": descriptionId,
1019
+ "data-delayed-snap-points": delayedSnapPoints ? "true" : "false",
1020
+ "data-drawer-direction": direction,
1021
+ "data-open": domUtils.dataAttr(isOpen),
1022
+ "data-animation-done": hasAnimationDone ? "true" : "false",
1023
+ "data-drawer": "",
1024
+ "data-snap-points": isOpen && hasSnapPoints ? "true" : "false",
1025
+ "data-custom-container": container ? "true" : "false",
1026
+ ...restProps,
1027
+ ref: reactComposeRefs.composeRefs(ref, drawerRef, contentRef),
1028
+ style: {
1029
+ ...snapPointsOffset && snapPointsOffset.length > 0 ? {
1030
+ "--snap-point-height": `${snapPointsOffset[activeSnapPointIndex ?? 0]}px`,
1031
+ ...style
1032
+ } : style,
1033
+ ...!modal && {
1034
+ pointerEvents: "auto"
1035
+ }
1036
+ },
1037
+ onPointerDown: (event)=>{
1038
+ if (handleOnly) return;
1039
+ restProps.onPointerDown?.(event);
1040
+ pointerStartRef.current = {
1041
+ x: event.pageX,
1042
+ y: event.pageY
1043
+ };
1044
+ draggedRef.current = false;
1045
+ onPress(event);
1046
+ },
1047
+ onPointerMove: (event)=>{
1048
+ lastKnownPointerEventRef.current = event;
1049
+ if (handleOnly) return;
1050
+ restProps.onPointerMove?.(event);
1051
+ if (!pointerStartRef.current) return;
1052
+ const yPosition = event.pageY - pointerStartRef.current.y;
1053
+ const xPosition = event.pageX - pointerStartRef.current.x;
1054
+ const swipeStartThreshold = event.pointerType === "touch" ? 10 : 2;
1055
+ const delta = {
1056
+ x: xPosition,
1057
+ y: yPosition
1058
+ };
1059
+ // Once the pointer travels past the swipe-start threshold the gesture
1060
+ // is a drag, not a tap — remember it so the trailing click can be
1061
+ // suppressed on release (iOS still synthesizes one on the origin).
1062
+ const isBeyondThreshold = Math.abs(xPosition) > swipeStartThreshold || Math.abs(yPosition) > swipeStartThreshold;
1063
+ if (isBeyondThreshold) draggedRef.current = true;
1064
+ const isAllowedToSwipe = isDeltaInDirection(delta, direction, swipeStartThreshold);
1065
+ if (isAllowedToSwipe) onDrag(event);
1066
+ else if (isBeyondThreshold) {
1067
+ pointerStartRef.current = null;
1068
+ }
1069
+ },
1070
+ onPointerUp: (event)=>{
1071
+ restProps.onPointerUp?.(event);
1072
+ pointerStartRef.current = null;
1073
+ wasBeyondThePointRef.current = false;
1074
+ onRelease(event);
1075
+ },
1076
+ onPointerOut: (event)=>{
1077
+ restProps.onPointerOut?.(event);
1078
+ handleOnPointerUp(lastKnownPointerEventRef.current);
1079
+ },
1080
+ onContextMenu: (event)=>{
1081
+ restProps.onContextMenu?.(event);
1082
+ if (lastKnownPointerEventRef.current) {
1083
+ handleOnPointerUp(lastKnownPointerEventRef.current);
1084
+ }
1085
+ },
1086
+ onClickCapture: (event)=>{
1087
+ restProps.onClickCapture?.(event);
1088
+ // Swallow the click that follows a drag so dragging the sheet (e.g. a
1089
+ // swipe-to-dismiss started on an item) doesn't also activate that item.
1090
+ // A plain tap leaves draggedRef false and clicks normally.
1091
+ if (draggedRef.current) {
1092
+ event.preventDefault();
1093
+ event.stopPropagation();
1094
+ draggedRef.current = false;
1095
+ }
1096
+ }
1097
+ })
1098
+ })
1099
+ })
1166
1100
  });
1167
1101
  });
1168
1102
  DrawerContent.displayName = "DrawerContent";
1169
- const DrawerTitle = DialogPrimitive__namespace.Title;
1170
- const DrawerDescription = DialogPrimitive__namespace.Description;
1103
+ const DrawerTitle = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1104
+ const api = useDrawerContext();
1105
+ return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.h2, {
1106
+ ref: ref,
1107
+ ...domUtils.mergeProps(api.titleProps, props)
1108
+ });
1109
+ });
1110
+ DrawerTitle.displayName = "DrawerTitle";
1111
+ const DrawerDescription = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1112
+ const api = useDrawerContext();
1113
+ return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.p, {
1114
+ ref: ref,
1115
+ ...domUtils.mergeProps(api.descriptionProps, props)
1116
+ });
1117
+ });
1118
+ DrawerDescription.displayName = "DrawerDescription";
1171
1119
  const DrawerHeader = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1172
- const { isCloseButtonRendered } = useDrawerContext();
1120
+ const api = useDrawerContext();
1173
1121
  return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.div, {
1174
1122
  ref: ref,
1175
- "data-show-close-button": domUtils.dataAttr(isCloseButtonRendered),
1176
- ...props
1123
+ ...domUtils.mergeProps(api.headerProps, props)
1177
1124
  });
1178
1125
  });
1179
1126
  DrawerHeader.displayName = "DrawerHeader";
1180
1127
  const DrawerCloseButton = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1181
- const { closeButtonRef, setIsOpen } = useDrawerContext();
1182
- const composedRef = reactComposeRefs.useComposedRefs(ref, closeButtonRef);
1128
+ const api = useDrawerContext();
1183
1129
  return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.button, {
1184
- ref: composedRef,
1185
- ...props,
1186
- onClick: (e)=>{
1187
- props.onClick?.(e);
1188
- if (e.defaultPrevented) return;
1189
- setIsOpen(false, {
1190
- reason: "closeButton",
1191
- event: e.nativeEvent
1192
- });
1193
- }
1130
+ ref: ref,
1131
+ ...domUtils.mergeProps(api.closeButtonProps, props)
1194
1132
  });
1195
1133
  });
1196
1134
  const LONG_HANDLE_PRESS_TIMEOUT = 250;