@seed-design/react-drawer 1.0.11 → 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,66 +847,74 @@ 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);
@@ -1034,7 +923,7 @@ const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1034
923
  // element the gesture started on (iOS still synthesizes a click on release).
1035
924
  const draggedRef = React.useRef(false);
1036
925
  const hasSnapPoints = snapPoints && snapPoints.length > 0;
1037
- const isDeltaInDirection = (delta, direction, threshold = 0)=>{
926
+ const isDeltaInDirection = (delta, dir, threshold = 0)=>{
1038
927
  if (wasBeyondThePointRef.current) return true;
1039
928
  const deltaY = Math.abs(delta.y);
1040
929
  const deltaX = Math.abs(delta.x);
@@ -1042,8 +931,8 @@ const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1042
931
  const dFactor = [
1043
932
  "bottom",
1044
933
  "right"
1045
- ].includes(direction) ? 1 : -1;
1046
- if (direction === "left" || direction === "right") {
934
+ ].includes(dir) ? 1 : -1;
935
+ if (dir === "left" || dir === "right") {
1047
936
  const isReverseDirection = delta.x * dFactor < 0;
1048
937
  if (!isReverseDirection && deltaX >= 0 && deltaX <= threshold) {
1049
938
  return isDeltaX;
@@ -1069,149 +958,177 @@ const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1069
958
  wasBeyondThePointRef.current = false;
1070
959
  onRelease(event);
1071
960
  }
1072
- return /*#__PURE__*/ jsxRuntime.jsx(DialogPrimitive__namespace.Content, {
1073
- "data-delayed-snap-points": delayedSnapPoints ? "true" : "false",
1074
- "data-drawer-direction": direction,
1075
- "data-open": domUtils.dataAttr(isOpen),
1076
- "data-animation-done": hasAnimationDone ? "true" : "false",
1077
- "data-drawer": "",
1078
- "data-snap-points": isOpen && hasSnapPoints ? "true" : "false",
1079
- "data-custom-container": container ? "true" : "false",
1080
- ...restProps,
1081
- ref: composedRef,
1082
- style: snapPointsOffset && snapPointsOffset.length > 0 ? {
1083
- "--snap-point-height": `${snapPointsOffset[activeSnapPointIndex ?? 0]}px`,
1084
- ...style
1085
- } : style ?? {},
1086
- onPointerDown: (event)=>{
1087
- if (handleOnly) return;
1088
- restProps.onPointerDown?.(event);
1089
- pointerStartRef.current = {
1090
- x: event.pageX,
1091
- y: event.pageY
1092
- };
1093
- draggedRef.current = false;
1094
- onPress(event);
1095
- },
1096
- onOpenAutoFocus: (e)=>{
1097
- onOpenAutoFocus?.(e);
1098
- if (!autoFocus) {
1099
- e.preventDefault();
1100
- }
1101
- },
1102
- onPointerDownOutside: (e)=>{
1103
- onPointerDownOutside?.(e);
1104
- if (!modal || e.defaultPrevented) {
1105
- e.preventDefault();
1106
- return;
1107
- }
1108
- if (keyboardIsOpen.current) {
1109
- keyboardIsOpen.current = false;
1110
- }
1111
- },
1112
- onFocusOutside: (e)=>{
1113
- props.onFocusOutside?.(e);
1114
- // Always prevent focusOutside to avoid conflicts when focus moves between modals
1115
- // (e.g., when Dialog closes and restores focus while BottomSheet is opening)
1116
- e.preventDefault();
1117
- },
1118
- onPointerMove: (event)=>{
1119
- lastKnownPointerEventRef.current = event;
1120
- if (handleOnly) return;
1121
- restProps.onPointerMove?.(event);
1122
- if (!pointerStartRef.current) return;
1123
- const yPosition = event.pageY - pointerStartRef.current.y;
1124
- const xPosition = event.pageX - pointerStartRef.current.x;
1125
- const swipeStartThreshold = event.pointerType === "touch" ? 10 : 2;
1126
- const delta = {
1127
- x: xPosition,
1128
- y: yPosition
1129
- };
1130
- // Once the pointer travels past the swipe-start threshold the gesture
1131
- // is a drag, not a tap — remember it so the trailing click can be
1132
- // suppressed on release (iOS still synthesizes one on the origin).
1133
- const isBeyondThreshold = Math.abs(xPosition) > swipeStartThreshold || Math.abs(yPosition) > swipeStartThreshold;
1134
- if (isBeyondThreshold) draggedRef.current = true;
1135
- const isAllowedToSwipe = isDeltaInDirection(delta, direction, swipeStartThreshold);
1136
- if (isAllowedToSwipe) onDrag(event);
1137
- else if (isBeyondThreshold) {
1138
- pointerStartRef.current = null;
1139
- }
1140
- },
1141
- onPointerUp: (event)=>{
1142
- restProps.onPointerUp?.(event);
1143
- pointerStartRef.current = null;
1144
- wasBeyondThePointRef.current = false;
1145
- onRelease(event);
1146
- },
1147
- onPointerOut: (event)=>{
1148
- restProps.onPointerOut?.(event);
1149
- handleOnPointerUp(lastKnownPointerEventRef.current);
1150
- },
1151
- onContextMenu: (event)=>{
1152
- restProps.onContextMenu?.(event);
1153
- if (lastKnownPointerEventRef.current) {
1154
- handleOnPointerUp(lastKnownPointerEventRef.current);
1155
- }
1156
- },
1157
- onClickCapture: (event)=>{
1158
- restProps.onClickCapture?.(event);
1159
- // Swallow the click that follows a drag so dragging the sheet (e.g. a
1160
- // swipe-to-dismiss started on an item) doesn't also activate that item.
1161
- // A plain tap leaves draggedRef false and clicks normally.
1162
- if (draggedRef.current) {
1163
- event.preventDefault();
1164
- event.stopPropagation();
1165
- draggedRef.current = false;
1166
- }
1167
- },
1168
- onInteractOutside: (e)=>{
1169
- // Only close if event is not prevented (e.g., by onFocusOutside or onPointerDownOutside)
1170
- 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;
1171
970
  closeDrawer(false, {
1172
- reason: "interactOutside",
1173
- event: e.detail.originalEvent
971
+ reason: "escapeKeyDown",
972
+ event: e
1174
973
  });
1175
- }
1176
- props.onInteractOutside?.(e);
1177
- },
1178
- onEscapeKeyDown: (e)=>{
1179
- 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;
1180
980
  closeDrawer(false, {
1181
- reason: "escapeKeyDown",
981
+ reason: "interactOutside",
1182
982
  event: e
1183
983
  });
1184
- }
1185
- props.onEscapeKeyDown?.(e);
1186
- }
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
+ })
1187
1100
  });
1188
1101
  });
1189
1102
  DrawerContent.displayName = "DrawerContent";
1190
- const DrawerTitle = DialogPrimitive__namespace.Title;
1191
- 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";
1192
1119
  const DrawerHeader = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1193
- const { isCloseButtonRendered } = useDrawerContext();
1120
+ const api = useDrawerContext();
1194
1121
  return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.div, {
1195
1122
  ref: ref,
1196
- "data-show-close-button": domUtils.dataAttr(isCloseButtonRendered),
1197
- ...props
1123
+ ...domUtils.mergeProps(api.headerProps, props)
1198
1124
  });
1199
1125
  });
1200
1126
  DrawerHeader.displayName = "DrawerHeader";
1201
1127
  const DrawerCloseButton = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1202
- const { closeButtonRef, setIsOpen } = useDrawerContext();
1203
- const composedRef = reactComposeRefs.useComposedRefs(ref, closeButtonRef);
1128
+ const api = useDrawerContext();
1204
1129
  return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.button, {
1205
- ref: composedRef,
1206
- ...props,
1207
- onClick: (e)=>{
1208
- props.onClick?.(e);
1209
- if (e.defaultPrevented) return;
1210
- setIsOpen(false, {
1211
- reason: "closeButton",
1212
- event: e.nativeEvent
1213
- });
1214
- }
1130
+ ref: ref,
1131
+ ...domUtils.mergeProps(api.closeButtonProps, props)
1215
1132
  });
1216
1133
  });
1217
1134
  const LONG_HANDLE_PRESS_TIMEOUT = 250;