@seed-design/react-drawer 1.0.7 → 1.0.8

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.
@@ -6,7 +6,7 @@ var reactUseCallbackRef = require('@radix-ui/react-use-callback-ref');
6
6
  var domUtils = require('@seed-design/dom-utils');
7
7
  var reactPrimitive = require('@seed-design/react-primitive');
8
8
  var React = require('react');
9
- var reactUseControllableState = require('@radix-ui/react-use-controllable-state');
9
+ var reactUseControllableState = require('@seed-design/react-use-controllable-state');
10
10
 
11
11
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
12
 
@@ -380,7 +380,7 @@ function useSnapPoints({ activeSnapPointProp, setActiveSnapPointProp, snapPoints
380
380
  snapPointsOffset,
381
381
  snapToPoint
382
382
  ]);
383
- function onRelease({ draggedDistance, closeDrawer, velocity, dismissible }) {
383
+ function onRelease({ draggedDistance, closeDrawer, velocity, dismissible, event }) {
384
384
  if (fadeFromIndex === undefined) return;
385
385
  const currentPosition = direction === "bottom" || direction === "right" ? (activeSnapPointOffset ?? 0) - draggedDistance : (activeSnapPointOffset ?? 0) + draggedDistance;
386
386
  const isOverlaySnapPoint = activeSnapPointIndex === fadeFromIndex - 1;
@@ -392,7 +392,10 @@ function useSnapPoints({ activeSnapPointProp, setActiveSnapPointProp, snapPoints
392
392
  });
393
393
  }
394
394
  if (!snapToSequentialPoint && velocity > 2 && !hasDraggedUp) {
395
- if (dismissible) closeDrawer();
395
+ if (dismissible) closeDrawer(false, {
396
+ reason: "drag",
397
+ event
398
+ });
396
399
  else snapToPoint(snapPointsOffset[0]); // snap to initial point
397
400
  return;
398
401
  }
@@ -414,7 +417,10 @@ function useSnapPoints({ activeSnapPointProp, setActiveSnapPointProp, snapPoints
414
417
  return;
415
418
  }
416
419
  if (isFirst && dragDirection < 0 && dismissible) {
417
- closeDrawer();
420
+ closeDrawer(false, {
421
+ reason: "drag",
422
+ event
423
+ });
418
424
  }
419
425
  if (activeSnapPointIndex === null) return;
420
426
  snapToPoint(snapPointsOffset[activeSnapPointIndex + dragDirection]);
@@ -484,8 +490,8 @@ function useDrawer(props) {
484
490
  const [isOpen = false, setIsOpen] = reactUseControllableState.useControllableState({
485
491
  defaultProp: defaultOpen,
486
492
  prop: openProp,
487
- onChange: (o)=>{
488
- onOpenChange?.(o);
493
+ onChange: (o, details)=>{
494
+ onOpenChange?.(o, details);
489
495
  if (!o && !nested) {
490
496
  restorePositionSetting();
491
497
  }
@@ -505,8 +511,13 @@ function useDrawer(props) {
505
511
  }
506
512
  });
507
513
  const [hasBeenOpened, setHasBeenOpened] = React.useState(false);
514
+ const [hasAnimationDone, setHasAnimationDone] = React.useState(false);
508
515
  const [isDragging, setIsDragging] = React.useState(false);
509
516
  const [shouldOverlayAnimate, setShouldOverlayAnimate] = React.useState(false);
517
+ const [isCloseButtonRendered, setIsCloseButtonRendered] = React.useState(false);
518
+ const closeButtonRef = React.useCallback((node)=>{
519
+ setIsCloseButtonRendered(!!node);
520
+ }, []);
510
521
  const overlayRef = React.useRef(null);
511
522
  const openTime = React.useRef(null);
512
523
  const dragStartTime = React.useRef(null);
@@ -675,11 +686,11 @@ function useDrawer(props) {
675
686
  }, [
676
687
  isDragging
677
688
  ]);
678
- const closeDrawer = React.useCallback((fromWithin)=>{
689
+ const closeDrawer = React.useCallback((fromWithin, details)=>{
679
690
  cancelDrag();
680
691
  onClose?.();
681
692
  if (!fromWithin) {
682
- setIsOpen(false);
693
+ setIsOpen(false, details);
683
694
  }
684
695
  if (fadeFromIndex !== undefined && fadeFromIndex > 0 && activeSnapPointIndex === 0) {
685
696
  set(overlayRef.current, {
@@ -729,7 +740,8 @@ function useDrawer(props) {
729
740
  draggedDistance: distMoved * directionMultiplier,
730
741
  closeDrawer,
731
742
  velocity,
732
- dismissible
743
+ dismissible,
744
+ event: event.nativeEvent
733
745
  });
734
746
  onReleaseProp?.(event, true);
735
747
  return;
@@ -740,7 +752,10 @@ function useDrawer(props) {
740
752
  return;
741
753
  }
742
754
  if (velocity > VELOCITY_THRESHOLD) {
743
- closeDrawer();
755
+ closeDrawer(false, {
756
+ reason: "drag",
757
+ event: event.nativeEvent
758
+ });
744
759
  onReleaseProp?.(event, false);
745
760
  return;
746
761
  }
@@ -748,7 +763,10 @@ function useDrawer(props) {
748
763
  const visibleDrawerWidth = Math.min(drawerRef.current.getBoundingClientRect().width ?? 0, window.innerWidth);
749
764
  const isHorizontalSwipe = direction === "left" || direction === "right";
750
765
  if (Math.abs(swipeAmount) >= (isHorizontalSwipe ? visibleDrawerWidth : visibleDrawerHeight) * closeThreshold) {
751
- closeDrawer();
766
+ closeDrawer(false, {
767
+ reason: "drag",
768
+ event: event.nativeEvent
769
+ });
752
770
  onReleaseProp?.(event, false);
753
771
  return;
754
772
  }
@@ -829,6 +847,7 @@ function useDrawer(props) {
829
847
  }, [
830
848
  modal
831
849
  ]);
850
+ // Effect 1: Track drawer open state
832
851
  React.useEffect(()=>{
833
852
  if (isOpen) {
834
853
  setHasBeenOpened(true);
@@ -836,6 +855,24 @@ function useDrawer(props) {
836
855
  }, [
837
856
  isOpen
838
857
  ]);
858
+ // Effect 2: Handle animation state and timer
859
+ React.useEffect(()=>{
860
+ if (isOpen) {
861
+ // Only reset animation state if this is the first open
862
+ if (!hasBeenOpened) {
863
+ setHasAnimationDone(false);
864
+ }
865
+ const timeoutId = setTimeout(()=>{
866
+ setHasAnimationDone(true);
867
+ }, TRANSITIONS.ENTER_DURATION * 1000);
868
+ return ()=>clearTimeout(timeoutId);
869
+ }
870
+ // Reset animation state when drawer closes
871
+ setHasAnimationDone(false);
872
+ }, [
873
+ isOpen,
874
+ hasBeenOpened
875
+ ]);
839
876
  React.useEffect(()=>{
840
877
  if (isOpen && snapPoints && fadeFromIndex === 0) {
841
878
  setShouldOverlayAnimate(true);
@@ -878,7 +915,10 @@ function useDrawer(props) {
878
915
  setHasBeenOpened,
879
916
  setIsOpen,
880
917
  closeOnInteractOutside,
881
- closeOnEscape
918
+ closeOnEscape,
919
+ hasAnimationDone,
920
+ closeButtonRef,
921
+ isCloseButtonRendered
882
922
  }), [
883
923
  activeSnapPoint,
884
924
  snapPoints,
@@ -903,7 +943,10 @@ function useDrawer(props) {
903
943
  closeOnEscape,
904
944
  onRelease,
905
945
  onDrag,
906
- onPress
946
+ onPress,
947
+ hasAnimationDone,
948
+ closeButtonRef,
949
+ isCloseButtonRendered
907
950
  ]);
908
951
  }
909
952
 
@@ -953,11 +996,10 @@ const DrawerPositioner = /*#__PURE__*/ React.forwardRef((props, ref)=>{
953
996
  });
954
997
  DrawerPositioner.displayName = "DrawerPositioner";
955
998
  const DrawerBackdrop = /*#__PURE__*/ React.forwardRef((props, ref)=>{
956
- const { overlayRef, onRelease, modal, snapPoints, isOpen, shouldFade, shouldOverlayAnimate } = useDrawerContext();
999
+ const { overlayRef, onRelease, modal, snapPoints, isOpen, shouldFade, shouldOverlayAnimate, hasAnimationDone } = useDrawerContext();
957
1000
  const composedRef = reactComposeRefs.useComposedRefs(ref, overlayRef);
958
1001
  const hasSnapPoints = snapPoints && snapPoints.length > 0;
959
1002
  const onMouseUp = reactUseCallbackRef.useCallbackRef((event)=>onRelease(event));
960
- // Overlay is the component that is locking scroll, removing it will unlock the scroll without having to dig into Radix's Dialog library
961
1003
  if (!modal) {
962
1004
  return null;
963
1005
  }
@@ -968,13 +1010,14 @@ const DrawerBackdrop = /*#__PURE__*/ React.forwardRef((props, ref)=>{
968
1010
  "data-snap-points-overlay": isOpen && shouldFade ? "true" : "false",
969
1011
  "data-should-overlay-animate": shouldOverlayAnimate ? "true" : "false",
970
1012
  "data-open": domUtils.dataAttr(isOpen),
1013
+ "data-animation-done": hasAnimationDone ? "true" : "false",
971
1014
  ...props
972
1015
  });
973
1016
  });
974
1017
  DrawerBackdrop.displayName = "DrawerBackdrop";
975
1018
  const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
976
1019
  const { onPointerDownOutside, style, onOpenAutoFocus, ...restProps } = props;
977
- const { drawerRef, onPress, onRelease, onDrag, keyboardIsOpen, snapPointsOffset, activeSnapPointIndex, modal, isOpen, direction, snapPoints, container, handleOnly, autoFocus, closeDrawer, closeOnInteractOutside, closeOnEscape, dismissible } = useDrawerContext();
1020
+ const { drawerRef, onPress, onRelease, onDrag, keyboardIsOpen, snapPointsOffset, activeSnapPointIndex, modal, isOpen, direction, snapPoints, container, handleOnly, autoFocus, closeDrawer, closeOnInteractOutside, closeOnEscape, dismissible, hasAnimationDone } = useDrawerContext();
978
1021
  // Needed to use transition instead of animations
979
1022
  const [delayedSnapPoints, setDelayedSnapPoints] = React.useState(false);
980
1023
  const composedRef = reactComposeRefs.useComposedRefs(ref, drawerRef);
@@ -1021,6 +1064,7 @@ const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1021
1064
  "data-delayed-snap-points": delayedSnapPoints ? "true" : "false",
1022
1065
  "data-drawer-direction": direction,
1023
1066
  "data-open": domUtils.dataAttr(isOpen),
1067
+ "data-animation-done": hasAnimationDone ? "true" : "false",
1024
1068
  "data-drawer": "",
1025
1069
  "data-snap-points": isOpen && hasSnapPoints ? "true" : "false",
1026
1070
  "data-custom-container": container ? "true" : "false",
@@ -1098,13 +1142,19 @@ const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1098
1142
  onInteractOutside: (e)=>{
1099
1143
  // Only close if event is not prevented (e.g., by onFocusOutside or onPointerDownOutside)
1100
1144
  if (dismissible && closeOnInteractOutside && !e.defaultPrevented) {
1101
- closeDrawer();
1145
+ closeDrawer(false, {
1146
+ reason: "interactOutside",
1147
+ event: e.detail.originalEvent
1148
+ });
1102
1149
  }
1103
1150
  props.onInteractOutside?.(e);
1104
1151
  },
1105
1152
  onEscapeKeyDown: (e)=>{
1106
1153
  if (dismissible && closeOnEscape) {
1107
- closeDrawer();
1154
+ closeDrawer(false, {
1155
+ reason: "escapeKeyDown",
1156
+ event: e
1157
+ });
1108
1158
  }
1109
1159
  props.onEscapeKeyDown?.(e);
1110
1160
  }
@@ -1113,15 +1163,28 @@ const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1113
1163
  DrawerContent.displayName = "DrawerContent";
1114
1164
  const DrawerTitle = DialogPrimitive__namespace.Title;
1115
1165
  const DrawerDescription = DialogPrimitive__namespace.Description;
1166
+ const DrawerHeader = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1167
+ const { isCloseButtonRendered } = useDrawerContext();
1168
+ return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.div, {
1169
+ ref: ref,
1170
+ "data-show-close-button": domUtils.dataAttr(isCloseButtonRendered),
1171
+ ...props
1172
+ });
1173
+ });
1174
+ DrawerHeader.displayName = "DrawerHeader";
1116
1175
  const DrawerCloseButton = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1117
- const api = useDrawerContext();
1176
+ const { closeButtonRef, setIsOpen } = useDrawerContext();
1177
+ const composedRef = reactComposeRefs.useComposedRefs(ref, closeButtonRef);
1118
1178
  return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.button, {
1119
- ref: ref,
1179
+ ref: composedRef,
1120
1180
  ...props,
1121
1181
  onClick: (e)=>{
1122
1182
  props.onClick?.(e);
1123
1183
  if (e.defaultPrevented) return;
1124
- api.setIsOpen(false);
1184
+ setIsOpen(false, {
1185
+ reason: "closeButton",
1186
+ event: e.nativeEvent
1187
+ });
1125
1188
  }
1126
1189
  });
1127
1190
  });
@@ -1132,17 +1195,17 @@ const DrawerHandle = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1132
1195
  const { closeDrawer, isDragging, snapPoints, activeSnapPoint, setActiveSnapPoint, dismissible, handleOnly, isOpen, onPress, onDrag, onRelease } = useDrawerContext();
1133
1196
  const closeTimeoutIdRef = React.useRef(null);
1134
1197
  const shouldCancelInteractionRef = React.useRef(false);
1135
- function handleStartCycle() {
1198
+ function handleStartCycle(event) {
1136
1199
  // Stop if this is the second click of a double click
1137
1200
  if (shouldCancelInteractionRef.current) {
1138
1201
  handleCancelInteraction();
1139
1202
  return;
1140
1203
  }
1141
1204
  window.setTimeout(()=>{
1142
- handleCycleSnapPoints();
1205
+ handleCycleSnapPoints(event);
1143
1206
  }, DOUBLE_TAP_TIMEOUT);
1144
1207
  }
1145
- function handleCycleSnapPoints() {
1208
+ function handleCycleSnapPoints(event) {
1146
1209
  // Prevent accidental taps while resizing drawer
1147
1210
  if (isDragging || preventCycle || shouldCancelInteractionRef.current) {
1148
1211
  handleCancelInteraction();
@@ -1152,13 +1215,19 @@ const DrawerHandle = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1152
1215
  handleCancelInteraction();
1153
1216
  if (!snapPoints || snapPoints.length === 0) {
1154
1217
  if (!dismissible) {
1155
- closeDrawer();
1218
+ closeDrawer(false, {
1219
+ reason: "handleClickOnLastSnapPoint",
1220
+ event: event.nativeEvent
1221
+ });
1156
1222
  }
1157
1223
  return;
1158
1224
  }
1159
1225
  const isLastSnapPoint = activeSnapPoint === snapPoints[snapPoints.length - 1];
1160
1226
  if (isLastSnapPoint && dismissible) {
1161
- closeDrawer();
1227
+ closeDrawer(false, {
1228
+ reason: "handleClickOnLastSnapPoint",
1229
+ event: event.nativeEvent
1230
+ });
1162
1231
  return;
1163
1232
  }
1164
1233
  const currentSnapIndex = snapPoints.findIndex((point)=>point === activeSnapPoint);
@@ -1207,6 +1276,7 @@ exports.DrawerCloseButton = DrawerCloseButton;
1207
1276
  exports.DrawerContent = DrawerContent;
1208
1277
  exports.DrawerDescription = DrawerDescription;
1209
1278
  exports.DrawerHandle = DrawerHandle;
1279
+ exports.DrawerHeader = DrawerHeader;
1210
1280
  exports.DrawerPositioner = DrawerPositioner;
1211
1281
  exports.DrawerRoot = DrawerRoot;
1212
1282
  exports.DrawerTitle = DrawerTitle;
@@ -5,8 +5,8 @@ import * as DialogPrimitive from '@radix-ui/react-dialog';
5
5
  import { useCallbackRef } from '@radix-ui/react-use-callback-ref';
6
6
  import { dataAttr } from '@seed-design/dom-utils';
7
7
  import { Primitive } from '@seed-design/react-primitive';
8
- import React, { useState, useRef, useCallback, useEffect, useMemo, createContext, useContext, forwardRef } from 'react';
9
- import { useControllableState } from '@radix-ui/react-use-controllable-state';
8
+ import React, { useState, useCallback, useRef, useEffect, useMemo, createContext, useContext, forwardRef } from 'react';
9
+ import { useControllableState } from '@seed-design/react-use-controllable-state';
10
10
 
11
11
  function isMobileFirefox() {
12
12
  if (typeof window === "undefined" || typeof navigator === "undefined") return false;
@@ -357,7 +357,7 @@ function useSnapPoints({ activeSnapPointProp, setActiveSnapPointProp, snapPoints
357
357
  snapPointsOffset,
358
358
  snapToPoint
359
359
  ]);
360
- function onRelease({ draggedDistance, closeDrawer, velocity, dismissible }) {
360
+ function onRelease({ draggedDistance, closeDrawer, velocity, dismissible, event }) {
361
361
  if (fadeFromIndex === undefined) return;
362
362
  const currentPosition = direction === "bottom" || direction === "right" ? (activeSnapPointOffset ?? 0) - draggedDistance : (activeSnapPointOffset ?? 0) + draggedDistance;
363
363
  const isOverlaySnapPoint = activeSnapPointIndex === fadeFromIndex - 1;
@@ -369,7 +369,10 @@ function useSnapPoints({ activeSnapPointProp, setActiveSnapPointProp, snapPoints
369
369
  });
370
370
  }
371
371
  if (!snapToSequentialPoint && velocity > 2 && !hasDraggedUp) {
372
- if (dismissible) closeDrawer();
372
+ if (dismissible) closeDrawer(false, {
373
+ reason: "drag",
374
+ event
375
+ });
373
376
  else snapToPoint(snapPointsOffset[0]); // snap to initial point
374
377
  return;
375
378
  }
@@ -391,7 +394,10 @@ function useSnapPoints({ activeSnapPointProp, setActiveSnapPointProp, snapPoints
391
394
  return;
392
395
  }
393
396
  if (isFirst && dragDirection < 0 && dismissible) {
394
- closeDrawer();
397
+ closeDrawer(false, {
398
+ reason: "drag",
399
+ event
400
+ });
395
401
  }
396
402
  if (activeSnapPointIndex === null) return;
397
403
  snapToPoint(snapPointsOffset[activeSnapPointIndex + dragDirection]);
@@ -461,8 +467,8 @@ function useDrawer(props) {
461
467
  const [isOpen = false, setIsOpen] = useControllableState({
462
468
  defaultProp: defaultOpen,
463
469
  prop: openProp,
464
- onChange: (o)=>{
465
- onOpenChange?.(o);
470
+ onChange: (o, details)=>{
471
+ onOpenChange?.(o, details);
466
472
  if (!o && !nested) {
467
473
  restorePositionSetting();
468
474
  }
@@ -482,8 +488,13 @@ function useDrawer(props) {
482
488
  }
483
489
  });
484
490
  const [hasBeenOpened, setHasBeenOpened] = useState(false);
491
+ const [hasAnimationDone, setHasAnimationDone] = useState(false);
485
492
  const [isDragging, setIsDragging] = useState(false);
486
493
  const [shouldOverlayAnimate, setShouldOverlayAnimate] = useState(false);
494
+ const [isCloseButtonRendered, setIsCloseButtonRendered] = useState(false);
495
+ const closeButtonRef = useCallback((node)=>{
496
+ setIsCloseButtonRendered(!!node);
497
+ }, []);
487
498
  const overlayRef = useRef(null);
488
499
  const openTime = useRef(null);
489
500
  const dragStartTime = useRef(null);
@@ -652,11 +663,11 @@ function useDrawer(props) {
652
663
  }, [
653
664
  isDragging
654
665
  ]);
655
- const closeDrawer = useCallback((fromWithin)=>{
666
+ const closeDrawer = useCallback((fromWithin, details)=>{
656
667
  cancelDrag();
657
668
  onClose?.();
658
669
  if (!fromWithin) {
659
- setIsOpen(false);
670
+ setIsOpen(false, details);
660
671
  }
661
672
  if (fadeFromIndex !== undefined && fadeFromIndex > 0 && activeSnapPointIndex === 0) {
662
673
  set(overlayRef.current, {
@@ -706,7 +717,8 @@ function useDrawer(props) {
706
717
  draggedDistance: distMoved * directionMultiplier,
707
718
  closeDrawer,
708
719
  velocity,
709
- dismissible
720
+ dismissible,
721
+ event: event.nativeEvent
710
722
  });
711
723
  onReleaseProp?.(event, true);
712
724
  return;
@@ -717,7 +729,10 @@ function useDrawer(props) {
717
729
  return;
718
730
  }
719
731
  if (velocity > VELOCITY_THRESHOLD) {
720
- closeDrawer();
732
+ closeDrawer(false, {
733
+ reason: "drag",
734
+ event: event.nativeEvent
735
+ });
721
736
  onReleaseProp?.(event, false);
722
737
  return;
723
738
  }
@@ -725,7 +740,10 @@ function useDrawer(props) {
725
740
  const visibleDrawerWidth = Math.min(drawerRef.current.getBoundingClientRect().width ?? 0, window.innerWidth);
726
741
  const isHorizontalSwipe = direction === "left" || direction === "right";
727
742
  if (Math.abs(swipeAmount) >= (isHorizontalSwipe ? visibleDrawerWidth : visibleDrawerHeight) * closeThreshold) {
728
- closeDrawer();
743
+ closeDrawer(false, {
744
+ reason: "drag",
745
+ event: event.nativeEvent
746
+ });
729
747
  onReleaseProp?.(event, false);
730
748
  return;
731
749
  }
@@ -806,6 +824,7 @@ function useDrawer(props) {
806
824
  }, [
807
825
  modal
808
826
  ]);
827
+ // Effect 1: Track drawer open state
809
828
  useEffect(()=>{
810
829
  if (isOpen) {
811
830
  setHasBeenOpened(true);
@@ -813,6 +832,24 @@ function useDrawer(props) {
813
832
  }, [
814
833
  isOpen
815
834
  ]);
835
+ // Effect 2: Handle animation state and timer
836
+ useEffect(()=>{
837
+ if (isOpen) {
838
+ // Only reset animation state if this is the first open
839
+ if (!hasBeenOpened) {
840
+ setHasAnimationDone(false);
841
+ }
842
+ const timeoutId = setTimeout(()=>{
843
+ setHasAnimationDone(true);
844
+ }, TRANSITIONS.ENTER_DURATION * 1000);
845
+ return ()=>clearTimeout(timeoutId);
846
+ }
847
+ // Reset animation state when drawer closes
848
+ setHasAnimationDone(false);
849
+ }, [
850
+ isOpen,
851
+ hasBeenOpened
852
+ ]);
816
853
  useEffect(()=>{
817
854
  if (isOpen && snapPoints && fadeFromIndex === 0) {
818
855
  setShouldOverlayAnimate(true);
@@ -855,7 +892,10 @@ function useDrawer(props) {
855
892
  setHasBeenOpened,
856
893
  setIsOpen,
857
894
  closeOnInteractOutside,
858
- closeOnEscape
895
+ closeOnEscape,
896
+ hasAnimationDone,
897
+ closeButtonRef,
898
+ isCloseButtonRendered
859
899
  }), [
860
900
  activeSnapPoint,
861
901
  snapPoints,
@@ -880,7 +920,10 @@ function useDrawer(props) {
880
920
  closeOnEscape,
881
921
  onRelease,
882
922
  onDrag,
883
- onPress
923
+ onPress,
924
+ hasAnimationDone,
925
+ closeButtonRef,
926
+ isCloseButtonRendered
884
927
  ]);
885
928
  }
886
929
 
@@ -930,11 +973,10 @@ const DrawerPositioner = /*#__PURE__*/ forwardRef((props, ref)=>{
930
973
  });
931
974
  DrawerPositioner.displayName = "DrawerPositioner";
932
975
  const DrawerBackdrop = /*#__PURE__*/ forwardRef((props, ref)=>{
933
- const { overlayRef, onRelease, modal, snapPoints, isOpen, shouldFade, shouldOverlayAnimate } = useDrawerContext();
976
+ const { overlayRef, onRelease, modal, snapPoints, isOpen, shouldFade, shouldOverlayAnimate, hasAnimationDone } = useDrawerContext();
934
977
  const composedRef = useComposedRefs(ref, overlayRef);
935
978
  const hasSnapPoints = snapPoints && snapPoints.length > 0;
936
979
  const onMouseUp = useCallbackRef((event)=>onRelease(event));
937
- // Overlay is the component that is locking scroll, removing it will unlock the scroll without having to dig into Radix's Dialog library
938
980
  if (!modal) {
939
981
  return null;
940
982
  }
@@ -945,13 +987,14 @@ const DrawerBackdrop = /*#__PURE__*/ forwardRef((props, ref)=>{
945
987
  "data-snap-points-overlay": isOpen && shouldFade ? "true" : "false",
946
988
  "data-should-overlay-animate": shouldOverlayAnimate ? "true" : "false",
947
989
  "data-open": dataAttr(isOpen),
990
+ "data-animation-done": hasAnimationDone ? "true" : "false",
948
991
  ...props
949
992
  });
950
993
  });
951
994
  DrawerBackdrop.displayName = "DrawerBackdrop";
952
995
  const DrawerContent = /*#__PURE__*/ forwardRef((props, ref)=>{
953
996
  const { onPointerDownOutside, style, onOpenAutoFocus, ...restProps } = props;
954
- const { drawerRef, onPress, onRelease, onDrag, keyboardIsOpen, snapPointsOffset, activeSnapPointIndex, modal, isOpen, direction, snapPoints, container, handleOnly, autoFocus, closeDrawer, closeOnInteractOutside, closeOnEscape, dismissible } = useDrawerContext();
997
+ const { drawerRef, onPress, onRelease, onDrag, keyboardIsOpen, snapPointsOffset, activeSnapPointIndex, modal, isOpen, direction, snapPoints, container, handleOnly, autoFocus, closeDrawer, closeOnInteractOutside, closeOnEscape, dismissible, hasAnimationDone } = useDrawerContext();
955
998
  // Needed to use transition instead of animations
956
999
  const [delayedSnapPoints, setDelayedSnapPoints] = useState(false);
957
1000
  const composedRef = useComposedRefs(ref, drawerRef);
@@ -998,6 +1041,7 @@ const DrawerContent = /*#__PURE__*/ forwardRef((props, ref)=>{
998
1041
  "data-delayed-snap-points": delayedSnapPoints ? "true" : "false",
999
1042
  "data-drawer-direction": direction,
1000
1043
  "data-open": dataAttr(isOpen),
1044
+ "data-animation-done": hasAnimationDone ? "true" : "false",
1001
1045
  "data-drawer": "",
1002
1046
  "data-snap-points": isOpen && hasSnapPoints ? "true" : "false",
1003
1047
  "data-custom-container": container ? "true" : "false",
@@ -1075,13 +1119,19 @@ const DrawerContent = /*#__PURE__*/ forwardRef((props, ref)=>{
1075
1119
  onInteractOutside: (e)=>{
1076
1120
  // Only close if event is not prevented (e.g., by onFocusOutside or onPointerDownOutside)
1077
1121
  if (dismissible && closeOnInteractOutside && !e.defaultPrevented) {
1078
- closeDrawer();
1122
+ closeDrawer(false, {
1123
+ reason: "interactOutside",
1124
+ event: e.detail.originalEvent
1125
+ });
1079
1126
  }
1080
1127
  props.onInteractOutside?.(e);
1081
1128
  },
1082
1129
  onEscapeKeyDown: (e)=>{
1083
1130
  if (dismissible && closeOnEscape) {
1084
- closeDrawer();
1131
+ closeDrawer(false, {
1132
+ reason: "escapeKeyDown",
1133
+ event: e
1134
+ });
1085
1135
  }
1086
1136
  props.onEscapeKeyDown?.(e);
1087
1137
  }
@@ -1090,15 +1140,28 @@ const DrawerContent = /*#__PURE__*/ forwardRef((props, ref)=>{
1090
1140
  DrawerContent.displayName = "DrawerContent";
1091
1141
  const DrawerTitle = DialogPrimitive.Title;
1092
1142
  const DrawerDescription = DialogPrimitive.Description;
1143
+ const DrawerHeader = /*#__PURE__*/ forwardRef((props, ref)=>{
1144
+ const { isCloseButtonRendered } = useDrawerContext();
1145
+ return /*#__PURE__*/ jsx(Primitive.div, {
1146
+ ref: ref,
1147
+ "data-show-close-button": dataAttr(isCloseButtonRendered),
1148
+ ...props
1149
+ });
1150
+ });
1151
+ DrawerHeader.displayName = "DrawerHeader";
1093
1152
  const DrawerCloseButton = /*#__PURE__*/ forwardRef((props, ref)=>{
1094
- const api = useDrawerContext();
1153
+ const { closeButtonRef, setIsOpen } = useDrawerContext();
1154
+ const composedRef = useComposedRefs(ref, closeButtonRef);
1095
1155
  return /*#__PURE__*/ jsx(Primitive.button, {
1096
- ref: ref,
1156
+ ref: composedRef,
1097
1157
  ...props,
1098
1158
  onClick: (e)=>{
1099
1159
  props.onClick?.(e);
1100
1160
  if (e.defaultPrevented) return;
1101
- api.setIsOpen(false);
1161
+ setIsOpen(false, {
1162
+ reason: "closeButton",
1163
+ event: e.nativeEvent
1164
+ });
1102
1165
  }
1103
1166
  });
1104
1167
  });
@@ -1109,17 +1172,17 @@ const DrawerHandle = /*#__PURE__*/ forwardRef((props, ref)=>{
1109
1172
  const { closeDrawer, isDragging, snapPoints, activeSnapPoint, setActiveSnapPoint, dismissible, handleOnly, isOpen, onPress, onDrag, onRelease } = useDrawerContext();
1110
1173
  const closeTimeoutIdRef = useRef(null);
1111
1174
  const shouldCancelInteractionRef = useRef(false);
1112
- function handleStartCycle() {
1175
+ function handleStartCycle(event) {
1113
1176
  // Stop if this is the second click of a double click
1114
1177
  if (shouldCancelInteractionRef.current) {
1115
1178
  handleCancelInteraction();
1116
1179
  return;
1117
1180
  }
1118
1181
  window.setTimeout(()=>{
1119
- handleCycleSnapPoints();
1182
+ handleCycleSnapPoints(event);
1120
1183
  }, DOUBLE_TAP_TIMEOUT);
1121
1184
  }
1122
- function handleCycleSnapPoints() {
1185
+ function handleCycleSnapPoints(event) {
1123
1186
  // Prevent accidental taps while resizing drawer
1124
1187
  if (isDragging || preventCycle || shouldCancelInteractionRef.current) {
1125
1188
  handleCancelInteraction();
@@ -1129,13 +1192,19 @@ const DrawerHandle = /*#__PURE__*/ forwardRef((props, ref)=>{
1129
1192
  handleCancelInteraction();
1130
1193
  if (!snapPoints || snapPoints.length === 0) {
1131
1194
  if (!dismissible) {
1132
- closeDrawer();
1195
+ closeDrawer(false, {
1196
+ reason: "handleClickOnLastSnapPoint",
1197
+ event: event.nativeEvent
1198
+ });
1133
1199
  }
1134
1200
  return;
1135
1201
  }
1136
1202
  const isLastSnapPoint = activeSnapPoint === snapPoints[snapPoints.length - 1];
1137
1203
  if (isLastSnapPoint && dismissible) {
1138
- closeDrawer();
1204
+ closeDrawer(false, {
1205
+ reason: "handleClickOnLastSnapPoint",
1206
+ event: event.nativeEvent
1207
+ });
1139
1208
  return;
1140
1209
  }
1141
1210
  const currentSnapIndex = snapPoints.findIndex((point)=>point === activeSnapPoint);
@@ -1179,4 +1248,4 @@ const DrawerHandle = /*#__PURE__*/ forwardRef((props, ref)=>{
1179
1248
  });
1180
1249
  DrawerHandle.displayName = "DrawerHandle";
1181
1250
 
1182
- export { DrawerBackdrop as D, DrawerCloseButton as a, DrawerContent as b, DrawerDescription as c, DrawerHandle as d, DrawerPositioner as e, DrawerRoot as f, DrawerTitle as g, DrawerTrigger as h, useDrawerContext as i, useDrawer as u };
1251
+ export { DrawerBackdrop as D, DrawerCloseButton as a, DrawerContent as b, DrawerDescription as c, DrawerHandle as d, DrawerHeader as e, DrawerPositioner as f, DrawerRoot as g, DrawerTitle as h, DrawerTrigger as i, useDrawerContext as j, useDrawer as u };
package/lib/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- var Drawer12s = require('./Drawer-12s-L4KZ1h_g.cjs');
1
+ var Drawer12s = require('./Drawer-12s-DXUv7jE4.cjs');
2
2
 
3
3
  var Drawer_namespace = {
4
4
  __proto__: null,
@@ -7,6 +7,7 @@ var Drawer_namespace = {
7
7
  Content: Drawer12s.DrawerContent,
8
8
  Description: Drawer12s.DrawerDescription,
9
9
  Handle: Drawer12s.DrawerHandle,
10
+ Header: Drawer12s.DrawerHeader,
10
11
  Positioner: Drawer12s.DrawerPositioner,
11
12
  Root: Drawer12s.DrawerRoot,
12
13
  Title: Drawer12s.DrawerTitle,
@@ -18,6 +19,7 @@ exports.DrawerCloseButton = Drawer12s.DrawerCloseButton;
18
19
  exports.DrawerContent = Drawer12s.DrawerContent;
19
20
  exports.DrawerDescription = Drawer12s.DrawerDescription;
20
21
  exports.DrawerHandle = Drawer12s.DrawerHandle;
22
+ exports.DrawerHeader = Drawer12s.DrawerHeader;
21
23
  exports.DrawerPositioner = Drawer12s.DrawerPositioner;
22
24
  exports.DrawerRoot = Drawer12s.DrawerRoot;
23
25
  exports.DrawerTitle = Drawer12s.DrawerTitle;
package/lib/index.d.ts CHANGED
@@ -1,12 +1,35 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as DialogPrimitive from '@radix-ui/react-dialog';
3
3
  import { PrimitiveProps } from '@seed-design/react-primitive';
4
- import * as react from 'react';
4
+ import * as React$1 from 'react';
5
+ import React__default from 'react';
5
6
 
7
+ interface DrawerReasonToDetailMap {
8
+ closeButton: {
9
+ event: MouseEvent;
10
+ };
11
+ escapeKeyDown: {
12
+ event: KeyboardEvent;
13
+ };
14
+ interactOutside: {
15
+ event: PointerEvent | FocusEvent;
16
+ };
17
+ drag: {
18
+ event: PointerEvent;
19
+ };
20
+ handleClickOnLastSnapPoint: {
21
+ event: MouseEvent;
22
+ };
23
+ }
24
+ type DrawerChangeDetails = {
25
+ [R in keyof DrawerReasonToDetailMap]: {
26
+ reason?: R;
27
+ } & DrawerReasonToDetailMap[R];
28
+ }[keyof DrawerReasonToDetailMap];
6
29
  interface UseDrawerProps {
7
30
  activeSnapPoint?: number | string | null;
8
31
  setActiveSnapPoint?: (snapPoint: number | string | null) => void;
9
- children?: React.ReactNode;
32
+ children?: React__default.ReactNode;
10
33
  open?: boolean;
11
34
  /**
12
35
  * Number between 0 and 1 that determines when the drawer should be closed.
@@ -19,7 +42,7 @@ interface UseDrawerProps {
19
42
  * @default true
20
43
  */
21
44
  noBodyStyles?: boolean;
22
- onOpenChange?: (open: boolean) => void;
45
+ onOpenChange?: (open: boolean, details?: DrawerChangeDetails) => void;
23
46
  /**
24
47
  * Duration for which the drawer is not draggable after scrolling content inside of the drawer.
25
48
  * @default 500ms
@@ -40,8 +63,8 @@ interface UseDrawerProps {
40
63
  * @default true
41
64
  */
42
65
  dismissible?: boolean;
43
- onDrag?: (event: React.PointerEvent<HTMLDivElement>, percentageDragged: number) => void;
44
- onRelease?: (event: React.PointerEvent<HTMLDivElement>, open: boolean) => void;
66
+ onDrag?: (event: React__default.PointerEvent<HTMLDivElement>, percentageDragged: number) => void;
67
+ onRelease?: (event: React__default.PointerEvent<HTMLDivElement>, open: boolean) => void;
45
68
  /**
46
69
  * When `false` it allows to interact with elements outside of the drawer without closing it.
47
70
  * @default true
@@ -106,21 +129,21 @@ interface UseDrawerProps {
106
129
  declare function useDrawer(props: UseDrawerProps): {
107
130
  activeSnapPoint: string | number | null;
108
131
  snapPoints: (string | number)[] | undefined;
109
- setActiveSnapPoint: (value: react.SetStateAction<string | number | null>) => void;
110
- drawerRef: react.RefObject<HTMLDivElement | null>;
111
- overlayRef: react.RefObject<HTMLDivElement | null>;
132
+ setActiveSnapPoint: (value: string | number | ((prev: string | number | null) => string | number | null) | null, details?: undefined) => void;
133
+ drawerRef: React__default.RefObject<HTMLDivElement | null>;
134
+ overlayRef: React__default.RefObject<HTMLDivElement | null>;
112
135
  shouldOverlayAnimate: boolean;
113
- onOpenChange: ((open: boolean) => void) | undefined;
114
- onPress: (event: React.PointerEvent<HTMLDivElement>) => void;
115
- onRelease: (event: React.PointerEvent<HTMLDivElement> | null) => void;
116
- onDrag: (event: React.PointerEvent<HTMLDivElement>) => void;
136
+ onOpenChange: ((open: boolean, details?: DrawerChangeDetails) => void) | undefined;
137
+ onPress: (event: React__default.PointerEvent<HTMLDivElement>) => void;
138
+ onRelease: (event: React__default.PointerEvent<HTMLDivElement> | null) => void;
139
+ onDrag: (event: React__default.PointerEvent<HTMLDivElement>) => void;
117
140
  dismissible: boolean;
118
141
  handleOnly: boolean;
119
142
  isOpen: boolean;
120
143
  isDragging: boolean;
121
144
  shouldFade: boolean;
122
- closeDrawer: (fromWithin?: boolean) => void;
123
- keyboardIsOpen: react.RefObject<boolean>;
145
+ closeDrawer: (fromWithin?: boolean, details?: DrawerChangeDetails) => void;
146
+ keyboardIsOpen: React__default.RefObject<boolean>;
124
147
  modal: boolean;
125
148
  snapPointsOffset: number[];
126
149
  activeSnapPointIndex: number | null;
@@ -128,51 +151,77 @@ declare function useDrawer(props: UseDrawerProps): {
128
151
  noBodyStyles: boolean;
129
152
  container: HTMLElement | null | undefined;
130
153
  autoFocus: boolean;
131
- setHasBeenOpened: react.Dispatch<react.SetStateAction<boolean>>;
132
- setIsOpen: (value: react.SetStateAction<boolean>) => void;
154
+ setHasBeenOpened: React__default.Dispatch<React__default.SetStateAction<boolean>>;
155
+ setIsOpen: (value: boolean | ((prev: boolean) => boolean), details?: DrawerChangeDetails | undefined) => void;
133
156
  closeOnInteractOutside: boolean;
134
157
  closeOnEscape: boolean;
158
+ hasAnimationDone: boolean;
159
+ closeButtonRef: (node: HTMLButtonElement | null) => void;
160
+ isCloseButtonRendered: boolean;
135
161
  };
136
162
 
137
163
  interface DrawerRootProps extends UseDrawerProps {
138
- children?: react.ReactNode;
164
+ children?: React$1.ReactNode;
139
165
  }
140
166
  declare const DrawerRoot: (props: DrawerRootProps) => react_jsx_runtime.JSX.Element;
141
167
  interface DrawerTriggerProps extends DialogPrimitive.DialogTriggerProps {
142
168
  }
143
- declare const DrawerTrigger: react.ForwardRefExoticComponent<DialogPrimitive.DialogTriggerProps & react.RefAttributes<HTMLButtonElement>>;
144
- interface DrawerPositionerProps extends PrimitiveProps, react.HTMLAttributes<HTMLDivElement> {
169
+ declare const DrawerTrigger: React$1.ForwardRefExoticComponent<DialogPrimitive.DialogTriggerProps & React$1.RefAttributes<HTMLButtonElement>>;
170
+ interface DrawerPositionerProps extends PrimitiveProps, React$1.HTMLAttributes<HTMLDivElement> {
145
171
  }
146
- declare const DrawerPositioner: react.ForwardRefExoticComponent<DrawerPositionerProps & react.RefAttributes<HTMLDivElement>>;
147
- interface DrawerBackdropProps extends DialogPrimitive.DialogOverlayProps, react.HTMLAttributes<HTMLDivElement> {
172
+ declare const DrawerPositioner: React$1.ForwardRefExoticComponent<DrawerPositionerProps & React$1.RefAttributes<HTMLDivElement>>;
173
+ interface DrawerBackdropProps extends DialogPrimitive.DialogOverlayProps, React$1.HTMLAttributes<HTMLDivElement> {
148
174
  }
149
- declare const DrawerBackdrop: react.ForwardRefExoticComponent<DrawerBackdropProps & react.RefAttributes<HTMLDivElement>>;
150
- interface DrawerContentProps extends DialogPrimitive.DialogContentProps, react.HTMLAttributes<HTMLDivElement> {
175
+ declare const DrawerBackdrop: React$1.ForwardRefExoticComponent<DrawerBackdropProps & React$1.RefAttributes<HTMLDivElement>>;
176
+ interface DrawerContentProps extends DialogPrimitive.DialogContentProps, React$1.HTMLAttributes<HTMLDivElement> {
151
177
  }
152
- declare const DrawerContent: react.ForwardRefExoticComponent<DrawerContentProps & react.RefAttributes<HTMLDivElement>>;
178
+ declare const DrawerContent: React$1.ForwardRefExoticComponent<DrawerContentProps & React$1.RefAttributes<HTMLDivElement>>;
153
179
  interface DrawerTitleProps extends DialogPrimitive.DialogTitleProps {
154
180
  }
155
- declare const DrawerTitle: react.ForwardRefExoticComponent<DialogPrimitive.DialogTitleProps & react.RefAttributes<HTMLHeadingElement>>;
181
+ declare const DrawerTitle: React$1.ForwardRefExoticComponent<DialogPrimitive.DialogTitleProps & React$1.RefAttributes<HTMLHeadingElement>>;
156
182
  interface DrawerDescriptionProps extends DialogPrimitive.DialogDescriptionProps {
157
183
  }
158
- declare const DrawerDescription: react.ForwardRefExoticComponent<DialogPrimitive.DialogDescriptionProps & react.RefAttributes<HTMLParagraphElement>>;
184
+ declare const DrawerDescription: React$1.ForwardRefExoticComponent<DialogPrimitive.DialogDescriptionProps & React$1.RefAttributes<HTMLParagraphElement>>;
185
+ interface DrawerHeaderProps extends PrimitiveProps, React$1.HTMLAttributes<HTMLDivElement> {
186
+ }
187
+ declare const DrawerHeader: React$1.ForwardRefExoticComponent<DrawerHeaderProps & React$1.RefAttributes<HTMLDivElement>>;
159
188
  interface DrawerCloseButtonProps extends DialogPrimitive.DialogCloseProps {
160
189
  }
161
- declare const DrawerCloseButton: react.ForwardRefExoticComponent<DrawerCloseButtonProps & react.RefAttributes<HTMLButtonElement>>;
162
- interface DrawerHandleProps extends react.HTMLAttributes<HTMLDivElement> {
190
+ declare const DrawerCloseButton: React$1.ForwardRefExoticComponent<DrawerCloseButtonProps & React$1.RefAttributes<HTMLButtonElement>>;
191
+ interface DrawerHandleProps extends React$1.HTMLAttributes<HTMLDivElement> {
163
192
  preventCycle?: boolean;
164
193
  }
165
- declare const DrawerHandle: react.ForwardRefExoticComponent<DrawerHandleProps & react.RefAttributes<HTMLDivElement>>;
194
+ declare const DrawerHandle: React$1.ForwardRefExoticComponent<DrawerHandleProps & React$1.RefAttributes<HTMLDivElement>>;
166
195
 
167
196
  type DrawerContextValue = ReturnType<typeof useDrawer>;
168
197
  declare function useDrawerContext(): {
169
198
  activeSnapPoint: string | number | null;
170
199
  snapPoints: (string | number)[] | undefined;
171
- setActiveSnapPoint: (value: react.SetStateAction<string | number | null>) => void;
172
- drawerRef: react.RefObject<HTMLDivElement | null>;
173
- overlayRef: react.RefObject<HTMLDivElement | null>;
200
+ setActiveSnapPoint: (value: string | number | ((prev: string | number | null) => string | number | null) | null, details?: undefined) => void;
201
+ drawerRef: React$1.RefObject<HTMLDivElement | null>;
202
+ overlayRef: React$1.RefObject<HTMLDivElement | null>;
174
203
  shouldOverlayAnimate: boolean;
175
- onOpenChange: ((open: boolean) => void) | undefined;
204
+ onOpenChange: ((open: boolean, details?: ({
205
+ reason?: "closeButton" | undefined;
206
+ } & {
207
+ event: MouseEvent;
208
+ }) | ({
209
+ reason?: "escapeKeyDown" | undefined;
210
+ } & {
211
+ event: KeyboardEvent;
212
+ }) | ({
213
+ reason?: "interactOutside" | undefined;
214
+ } & {
215
+ event: PointerEvent | FocusEvent;
216
+ }) | ({
217
+ reason?: "drag" | undefined;
218
+ } & {
219
+ event: PointerEvent;
220
+ }) | ({
221
+ reason?: "handleClickOnLastSnapPoint" | undefined;
222
+ } & {
223
+ event: MouseEvent;
224
+ })) => void) | undefined;
176
225
  onPress: (event: React.PointerEvent<HTMLDivElement>) => void;
177
226
  onRelease: (event: React.PointerEvent<HTMLDivElement> | null) => void;
178
227
  onDrag: (event: React.PointerEvent<HTMLDivElement>) => void;
@@ -181,8 +230,28 @@ declare function useDrawerContext(): {
181
230
  isOpen: boolean;
182
231
  isDragging: boolean;
183
232
  shouldFade: boolean;
184
- closeDrawer: (fromWithin?: boolean) => void;
185
- keyboardIsOpen: react.RefObject<boolean>;
233
+ closeDrawer: (fromWithin?: boolean, details?: ({
234
+ reason?: "closeButton" | undefined;
235
+ } & {
236
+ event: MouseEvent;
237
+ }) | ({
238
+ reason?: "escapeKeyDown" | undefined;
239
+ } & {
240
+ event: KeyboardEvent;
241
+ }) | ({
242
+ reason?: "interactOutside" | undefined;
243
+ } & {
244
+ event: PointerEvent | FocusEvent;
245
+ }) | ({
246
+ reason?: "drag" | undefined;
247
+ } & {
248
+ event: PointerEvent;
249
+ }) | ({
250
+ reason?: "handleClickOnLastSnapPoint" | undefined;
251
+ } & {
252
+ event: MouseEvent;
253
+ })) => void;
254
+ keyboardIsOpen: React$1.RefObject<boolean>;
186
255
  modal: boolean;
187
256
  snapPointsOffset: number[];
188
257
  activeSnapPointIndex: number | null;
@@ -190,16 +259,39 @@ declare function useDrawerContext(): {
190
259
  noBodyStyles: boolean;
191
260
  container: HTMLElement | null | undefined;
192
261
  autoFocus: boolean;
193
- setHasBeenOpened: react.Dispatch<react.SetStateAction<boolean>>;
194
- setIsOpen: (value: react.SetStateAction<boolean>) => void;
262
+ setHasBeenOpened: React$1.Dispatch<React$1.SetStateAction<boolean>>;
263
+ setIsOpen: (value: boolean | ((prev: boolean) => boolean), details?: (({
264
+ reason?: "closeButton" | undefined;
265
+ } & {
266
+ event: MouseEvent;
267
+ }) | ({
268
+ reason?: "escapeKeyDown" | undefined;
269
+ } & {
270
+ event: KeyboardEvent;
271
+ }) | ({
272
+ reason?: "interactOutside" | undefined;
273
+ } & {
274
+ event: PointerEvent | FocusEvent;
275
+ }) | ({
276
+ reason?: "drag" | undefined;
277
+ } & {
278
+ event: PointerEvent;
279
+ }) | ({
280
+ reason?: "handleClickOnLastSnapPoint" | undefined;
281
+ } & {
282
+ event: MouseEvent;
283
+ })) | undefined) => void;
195
284
  closeOnInteractOutside: boolean;
196
285
  closeOnEscape: boolean;
286
+ hasAnimationDone: boolean;
287
+ closeButtonRef: (node: HTMLButtonElement | null) => void;
288
+ isCloseButtonRendered: boolean;
197
289
  };
198
290
 
199
291
  declare namespace Drawer_namespace {
200
- export { DrawerBackdrop as Backdrop, DrawerCloseButton as CloseButton, DrawerContent as Content, DrawerDescription as Description, DrawerHandle as Handle, DrawerPositioner as Positioner, DrawerRoot as Root, DrawerTitle as Title, DrawerTrigger as Trigger };
201
- export type { DrawerBackdropProps as BackdropProps, DrawerCloseButtonProps as CloseButtonProps, DrawerContentProps as ContentProps, DrawerDescriptionProps as DescriptionProps, DrawerHandleProps as HandleProps, DrawerPositionerProps as PositionerProps, DrawerRootProps as RootProps, DrawerTitleProps as TitleProps, DrawerTriggerProps as TriggerProps };
292
+ export { DrawerBackdrop as Backdrop, DrawerCloseButton as CloseButton, DrawerContent as Content, DrawerDescription as Description, DrawerHandle as Handle, DrawerHeader as Header, DrawerPositioner as Positioner, DrawerRoot as Root, DrawerTitle as Title, DrawerTrigger as Trigger };
293
+ export type { DrawerBackdropProps as BackdropProps, DrawerCloseButtonProps as CloseButtonProps, DrawerContentProps as ContentProps, DrawerDescriptionProps as DescriptionProps, DrawerHandleProps as HandleProps, DrawerHeaderProps as HeaderProps, DrawerPositionerProps as PositionerProps, DrawerRootProps as RootProps, DrawerTitleProps as TitleProps, DrawerTriggerProps as TriggerProps };
202
294
  }
203
295
 
204
- export { Drawer_namespace as Drawer, DrawerBackdrop, DrawerCloseButton, DrawerContent, DrawerDescription, DrawerHandle, DrawerPositioner, DrawerRoot, DrawerTitle, DrawerTrigger, useDrawer, useDrawerContext };
205
- export type { DrawerBackdropProps, DrawerCloseButtonProps, DrawerContentProps, DrawerContextValue, DrawerDescriptionProps, DrawerHandleProps, DrawerPositionerProps, DrawerRootProps, DrawerTitleProps, DrawerTriggerProps, UseDrawerProps };
296
+ export { Drawer_namespace as Drawer, DrawerBackdrop, DrawerCloseButton, DrawerContent, DrawerDescription, DrawerHandle, DrawerHeader, DrawerPositioner, DrawerRoot, DrawerTitle, DrawerTrigger, useDrawer, useDrawerContext };
297
+ export type { DrawerBackdropProps, DrawerCloseButtonProps, DrawerContentProps, DrawerContextValue, DrawerDescriptionProps, DrawerHandleProps, DrawerHeaderProps, DrawerPositionerProps, DrawerRootProps, DrawerTitleProps, DrawerTriggerProps, UseDrawerProps };
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { D as DrawerBackdrop, a as DrawerCloseButton, b as DrawerContent, c as DrawerDescription, d as DrawerHandle, e as DrawerPositioner, f as DrawerRoot, g as DrawerTitle, h as DrawerTrigger } from './Drawer-12s-1gup1cyj.js';
2
- export { u as useDrawer, i as useDrawerContext } from './Drawer-12s-1gup1cyj.js';
1
+ import { D as DrawerBackdrop, a as DrawerCloseButton, b as DrawerContent, c as DrawerDescription, d as DrawerHandle, e as DrawerHeader, f as DrawerPositioner, g as DrawerRoot, h as DrawerTitle, i as DrawerTrigger } from './Drawer-12s-DnXlK1K0.js';
2
+ export { u as useDrawer, j as useDrawerContext } from './Drawer-12s-DnXlK1K0.js';
3
3
 
4
4
  var Drawer_namespace = {
5
5
  __proto__: null,
@@ -8,10 +8,11 @@ var Drawer_namespace = {
8
8
  Content: DrawerContent,
9
9
  Description: DrawerDescription,
10
10
  Handle: DrawerHandle,
11
+ Header: DrawerHeader,
11
12
  Positioner: DrawerPositioner,
12
13
  Root: DrawerRoot,
13
14
  Title: DrawerTitle,
14
15
  Trigger: DrawerTrigger
15
16
  };
16
17
 
17
- export { Drawer_namespace as Drawer, DrawerBackdrop, DrawerCloseButton, DrawerContent, DrawerDescription, DrawerHandle, DrawerPositioner, DrawerRoot, DrawerTitle, DrawerTrigger };
18
+ export { Drawer_namespace as Drawer, DrawerBackdrop, DrawerCloseButton, DrawerContent, DrawerDescription, DrawerHandle, DrawerHeader, DrawerPositioner, DrawerRoot, DrawerTitle, DrawerTrigger };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seed-design/react-drawer",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/daangn/seed-design.git",
@@ -30,7 +30,7 @@
30
30
  "@radix-ui/react-compose-refs": "^1.1.2",
31
31
  "@radix-ui/react-dialog": "^1.1.15",
32
32
  "@radix-ui/react-use-callback-ref": "^1.1.0",
33
- "@radix-ui/react-use-controllable-state": "^1.2.2",
33
+ "@seed-design/react-use-controllable-state": "1.0.0",
34
34
  "@seed-design/dom-utils": "1.0.0",
35
35
  "@seed-design/react-primitive": "1.0.0"
36
36
  },
@@ -4,6 +4,7 @@ export {
4
4
  DrawerContent as Content,
5
5
  DrawerDescription as Description,
6
6
  DrawerHandle as Handle,
7
+ DrawerHeader as Header,
7
8
  DrawerPositioner as Positioner,
8
9
  DrawerRoot as Root,
9
10
  DrawerTitle as Title,
@@ -13,6 +14,7 @@ export {
13
14
  type DrawerContentProps as ContentProps,
14
15
  type DrawerDescriptionProps as DescriptionProps,
15
16
  type DrawerHandleProps as HandleProps,
17
+ type DrawerHeaderProps as HeaderProps,
16
18
  type DrawerPositionerProps as PositionerProps,
17
19
  type DrawerRootProps as RootProps,
18
20
  type DrawerTitleProps as TitleProps,
package/src/Drawer.tsx CHANGED
@@ -64,13 +64,20 @@ export interface DrawerBackdropProps
64
64
  React.HTMLAttributes<HTMLDivElement> {}
65
65
 
66
66
  export const DrawerBackdrop = forwardRef<HTMLDivElement, DrawerBackdropProps>((props, ref) => {
67
- const { overlayRef, onRelease, modal, snapPoints, isOpen, shouldFade, shouldOverlayAnimate } =
68
- useDrawerContext();
67
+ const {
68
+ overlayRef,
69
+ onRelease,
70
+ modal,
71
+ snapPoints,
72
+ isOpen,
73
+ shouldFade,
74
+ shouldOverlayAnimate,
75
+ hasAnimationDone,
76
+ } = useDrawerContext();
69
77
  const composedRef = useComposedRefs(ref, overlayRef);
70
78
  const hasSnapPoints = snapPoints && snapPoints.length > 0;
71
79
  const onMouseUp = useCallbackRef((event: React.PointerEvent<HTMLDivElement>) => onRelease(event));
72
80
 
73
- // Overlay is the component that is locking scroll, removing it will unlock the scroll without having to dig into Radix's Dialog library
74
81
  if (!modal) {
75
82
  return null;
76
83
  }
@@ -83,6 +90,7 @@ export const DrawerBackdrop = forwardRef<HTMLDivElement, DrawerBackdropProps>((p
83
90
  data-snap-points-overlay={isOpen && shouldFade ? "true" : "false"}
84
91
  data-should-overlay-animate={shouldOverlayAnimate ? "true" : "false"}
85
92
  data-open={dataAttr(isOpen)}
93
+ data-animation-done={hasAnimationDone ? "true" : "false"}
86
94
  {...props}
87
95
  />
88
96
  );
@@ -114,6 +122,7 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>((pro
114
122
  closeOnInteractOutside,
115
123
  closeOnEscape,
116
124
  dismissible,
125
+ hasAnimationDone,
117
126
  } = useDrawerContext();
118
127
  // Needed to use transition instead of animations
119
128
  const [delayedSnapPoints, setDelayedSnapPoints] = useState(false);
@@ -170,6 +179,7 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>((pro
170
179
  data-delayed-snap-points={delayedSnapPoints ? "true" : "false"}
171
180
  data-drawer-direction={direction}
172
181
  data-open={dataAttr(isOpen)}
182
+ data-animation-done={hasAnimationDone ? "true" : "false"}
173
183
  data-drawer=""
174
184
  data-snap-points={isOpen && hasSnapPoints ? "true" : "false"}
175
185
  data-custom-container={container ? "true" : "false"}
@@ -253,13 +263,13 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>((pro
253
263
  onInteractOutside={(e) => {
254
264
  // Only close if event is not prevented (e.g., by onFocusOutside or onPointerDownOutside)
255
265
  if (dismissible && closeOnInteractOutside && !e.defaultPrevented) {
256
- closeDrawer();
266
+ closeDrawer(false, { reason: "interactOutside", event: e.detail.originalEvent });
257
267
  }
258
268
  props.onInteractOutside?.(e);
259
269
  }}
260
270
  onEscapeKeyDown={(e) => {
261
271
  if (dismissible && closeOnEscape) {
262
- closeDrawer();
272
+ closeDrawer(false, { reason: "escapeKeyDown", event: e });
263
273
  }
264
274
  props.onEscapeKeyDown?.(e);
265
275
  }}
@@ -276,19 +286,30 @@ export interface DrawerDescriptionProps extends DialogPrimitive.DialogDescriptio
276
286
 
277
287
  export const DrawerDescription = DialogPrimitive.Description;
278
288
 
289
+ export interface DrawerHeaderProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {}
290
+
291
+ export const DrawerHeader = forwardRef<HTMLDivElement, DrawerHeaderProps>((props, ref) => {
292
+ const { isCloseButtonRendered } = useDrawerContext();
293
+ return (
294
+ <Primitive.div ref={ref} data-show-close-button={dataAttr(isCloseButtonRendered)} {...props} />
295
+ );
296
+ });
297
+ DrawerHeader.displayName = "DrawerHeader";
298
+
279
299
  export interface DrawerCloseButtonProps extends DialogPrimitive.DialogCloseProps {}
280
300
 
281
301
  export const DrawerCloseButton = forwardRef<HTMLButtonElement, DrawerCloseButtonProps>(
282
302
  (props, ref) => {
283
- const api = useDrawerContext();
303
+ const { closeButtonRef, setIsOpen } = useDrawerContext();
304
+ const composedRef = useComposedRefs(ref, closeButtonRef);
284
305
  return (
285
306
  <Primitive.button
286
- ref={ref}
307
+ ref={composedRef}
287
308
  {...props}
288
309
  onClick={(e) => {
289
310
  props.onClick?.(e);
290
311
  if (e.defaultPrevented) return;
291
- api.setIsOpen(false);
312
+ setIsOpen(false, { reason: "closeButton", event: e.nativeEvent });
292
313
  }}
293
314
  />
294
315
  );
@@ -321,18 +342,18 @@ export const DrawerHandle = forwardRef<HTMLDivElement, DrawerHandleProps>((props
321
342
  const closeTimeoutIdRef = useRef<number | null>(null);
322
343
  const shouldCancelInteractionRef = useRef(false);
323
344
 
324
- function handleStartCycle() {
345
+ function handleStartCycle(event: React.MouseEvent<HTMLDivElement>) {
325
346
  // Stop if this is the second click of a double click
326
347
  if (shouldCancelInteractionRef.current) {
327
348
  handleCancelInteraction();
328
349
  return;
329
350
  }
330
351
  window.setTimeout(() => {
331
- handleCycleSnapPoints();
352
+ handleCycleSnapPoints(event);
332
353
  }, DOUBLE_TAP_TIMEOUT);
333
354
  }
334
355
 
335
- function handleCycleSnapPoints() {
356
+ function handleCycleSnapPoints(event: React.MouseEvent<HTMLDivElement>) {
336
357
  // Prevent accidental taps while resizing drawer
337
358
  if (isDragging || preventCycle || shouldCancelInteractionRef.current) {
338
359
  handleCancelInteraction();
@@ -343,7 +364,7 @@ export const DrawerHandle = forwardRef<HTMLDivElement, DrawerHandleProps>((props
343
364
 
344
365
  if (!snapPoints || snapPoints.length === 0) {
345
366
  if (!dismissible) {
346
- closeDrawer();
367
+ closeDrawer(false, { reason: "handleClickOnLastSnapPoint", event: event.nativeEvent });
347
368
  }
348
369
  return;
349
370
  }
@@ -351,7 +372,7 @@ export const DrawerHandle = forwardRef<HTMLDivElement, DrawerHandleProps>((props
351
372
  const isLastSnapPoint = activeSnapPoint === snapPoints[snapPoints.length - 1];
352
373
 
353
374
  if (isLastSnapPoint && dismissible) {
354
- closeDrawer();
375
+ closeDrawer(false, { reason: "handleClickOnLastSnapPoint", event: event.nativeEvent });
355
376
  return;
356
377
  }
357
378
 
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export {
4
4
  DrawerContent,
5
5
  DrawerDescription,
6
6
  DrawerHandle,
7
+ DrawerHeader,
7
8
  DrawerPositioner,
8
9
  DrawerRoot,
9
10
  DrawerTitle,
@@ -13,6 +14,7 @@ export {
13
14
  type DrawerContentProps,
14
15
  type DrawerDescriptionProps,
15
16
  type DrawerHandleProps,
17
+ type DrawerHeaderProps,
16
18
  type DrawerPositionerProps,
17
19
  type DrawerRootProps,
18
20
  type DrawerTitleProps,
@@ -1,4 +1,4 @@
1
- import { useControllableState } from "@radix-ui/react-use-controllable-state";
1
+ import { useControllableState } from "@seed-design/react-use-controllable-state";
2
2
  import React from "react";
3
3
  import { TRANSITIONS, VELOCITY_THRESHOLD } from "./constants";
4
4
  import { isVertical, set } from "./helpers";
@@ -196,11 +196,13 @@ export function useSnapPoints({
196
196
  closeDrawer,
197
197
  velocity,
198
198
  dismissible,
199
+ event,
199
200
  }: {
200
201
  draggedDistance: number;
201
- closeDrawer: () => void;
202
+ closeDrawer: (fromWithin: boolean, details: { reason: "drag"; event: PointerEvent }) => void;
202
203
  velocity: number;
203
204
  dismissible: boolean;
205
+ event: PointerEvent;
204
206
  }) {
205
207
  if (fadeFromIndex === undefined) return;
206
208
 
@@ -219,7 +221,7 @@ export function useSnapPoints({
219
221
  }
220
222
 
221
223
  if (!snapToSequentialPoint && velocity > 2 && !hasDraggedUp) {
222
- if (dismissible) closeDrawer();
224
+ if (dismissible) closeDrawer(false, { reason: "drag", event });
223
225
  else snapToPoint(snapPointsOffset[0]); // snap to initial point
224
226
  return;
225
227
  }
@@ -247,7 +249,7 @@ export function useSnapPoints({
247
249
  }
248
250
 
249
251
  if (isFirst && dragDirection < 0 && dismissible) {
250
- closeDrawer();
252
+ closeDrawer(false, { reason: "drag", event });
251
253
  }
252
254
 
253
255
  if (activeSnapPointIndex === null) return;
package/src/useDrawer.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { useControllableState } from "@radix-ui/react-use-controllable-state";
1
+ import { useControllableState } from "@seed-design/react-use-controllable-state";
2
+ import type React from "react";
2
3
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
4
  import { isIOS, isMobileFirefox } from "./browser";
4
5
  import {
@@ -13,6 +14,21 @@ import { dampenValue, getTranslate, isInput, isVertical, reset, set } from "./he
13
14
  import { usePositionFixed } from "./use-position-fixed";
14
15
  import { useSnapPoints } from "./use-snap-points";
15
16
 
17
+ interface DrawerReasonToDetailMap {
18
+ // we might add synthetic events later if needed; currently we aim consistency; DismissableLayer gives us native events
19
+ closeButton: { event: MouseEvent };
20
+ escapeKeyDown: { event: KeyboardEvent };
21
+ interactOutside: { event: PointerEvent | FocusEvent };
22
+ drag: { event: PointerEvent };
23
+ handleClickOnLastSnapPoint: { event: MouseEvent };
24
+ }
25
+
26
+ type DrawerChangeDetails = {
27
+ [R in keyof DrawerReasonToDetailMap]: {
28
+ reason?: R;
29
+ } & DrawerReasonToDetailMap[R];
30
+ }[keyof DrawerReasonToDetailMap];
31
+
16
32
  export interface UseDrawerProps {
17
33
  activeSnapPoint?: number | string | null;
18
34
  setActiveSnapPoint?: (snapPoint: number | string | null) => void;
@@ -29,7 +45,7 @@ export interface UseDrawerProps {
29
45
  * @default true
30
46
  */
31
47
  noBodyStyles?: boolean;
32
- onOpenChange?: (open: boolean) => void;
48
+ onOpenChange?: (open: boolean, details?: DrawerChangeDetails) => void;
33
49
  /**
34
50
  * Duration for which the drawer is not draggable after scrolling content inside of the drawer.
35
51
  * @default 500ms
@@ -149,11 +165,11 @@ export function useDrawer(props: UseDrawerProps) {
149
165
  closeOnEscape = true,
150
166
  } = props;
151
167
 
152
- const [isOpen = false, setIsOpen] = useControllableState({
168
+ const [isOpen = false, setIsOpen] = useControllableState<boolean, DrawerChangeDetails>({
153
169
  defaultProp: defaultOpen,
154
170
  prop: openProp,
155
- onChange: (o: boolean) => {
156
- onOpenChange?.(o);
171
+ onChange: (o: boolean, details?: DrawerChangeDetails) => {
172
+ onOpenChange?.(o, details);
157
173
 
158
174
  if (!o && !nested) {
159
175
  restorePositionSetting();
@@ -178,9 +194,15 @@ export function useDrawer(props: UseDrawerProps) {
178
194
  });
179
195
 
180
196
  const [hasBeenOpened, setHasBeenOpened] = useState<boolean>(false);
197
+ const [hasAnimationDone, setHasAnimationDone] = useState<boolean>(false);
181
198
  const [isDragging, setIsDragging] = useState<boolean>(false);
182
199
  const [shouldOverlayAnimate, setShouldOverlayAnimate] = useState<boolean>(false);
183
200
 
201
+ const [isCloseButtonRendered, setIsCloseButtonRendered] = useState<boolean>(false);
202
+ const closeButtonRef = useCallback((node: HTMLButtonElement | null) => {
203
+ setIsCloseButtonRendered(!!node);
204
+ }, []);
205
+
184
206
  const overlayRef = useRef<HTMLDivElement>(null);
185
207
  const openTime = useRef<Date | null>(null);
186
208
  const dragStartTime = useRef<Date | null>(null);
@@ -419,12 +441,12 @@ export function useDrawer(props: UseDrawerProps) {
419
441
  }, [isDragging]);
420
442
 
421
443
  const closeDrawer = useCallback(
422
- (fromWithin?: boolean) => {
444
+ (fromWithin?: boolean, details?: DrawerChangeDetails) => {
423
445
  cancelDrag();
424
446
  onClose?.();
425
447
 
426
448
  if (!fromWithin) {
427
- setIsOpen(false);
449
+ setIsOpen(false, details);
428
450
  }
429
451
 
430
452
  if (fadeFromIndex !== undefined && fadeFromIndex > 0 && activeSnapPointIndex === 0) {
@@ -489,6 +511,7 @@ export function useDrawer(props: UseDrawerProps) {
489
511
  closeDrawer,
490
512
  velocity,
491
513
  dismissible,
514
+ event: event.nativeEvent,
492
515
  });
493
516
  onReleaseProp?.(event, true);
494
517
  return;
@@ -501,7 +524,7 @@ export function useDrawer(props: UseDrawerProps) {
501
524
  }
502
525
 
503
526
  if (velocity > VELOCITY_THRESHOLD) {
504
- closeDrawer();
527
+ closeDrawer(false, { reason: "drag", event: event.nativeEvent });
505
528
  onReleaseProp?.(event, false);
506
529
  return;
507
530
  }
@@ -520,7 +543,7 @@ export function useDrawer(props: UseDrawerProps) {
520
543
  Math.abs(swipeAmount) >=
521
544
  (isHorizontalSwipe ? visibleDrawerWidth : visibleDrawerHeight) * closeThreshold
522
545
  ) {
523
- closeDrawer();
546
+ closeDrawer(false, { reason: "drag", event: event.nativeEvent });
524
547
  onReleaseProp?.(event, false);
525
548
  return;
526
549
  }
@@ -607,12 +630,32 @@ export function useDrawer(props: UseDrawerProps) {
607
630
  }
608
631
  }, [modal]);
609
632
 
633
+ // Effect 1: Track drawer open state
610
634
  useEffect(() => {
611
635
  if (isOpen) {
612
636
  setHasBeenOpened(true);
613
637
  }
614
638
  }, [isOpen]);
615
639
 
640
+ // Effect 2: Handle animation state and timer
641
+ useEffect(() => {
642
+ if (isOpen) {
643
+ // Only reset animation state if this is the first open
644
+ if (!hasBeenOpened) {
645
+ setHasAnimationDone(false);
646
+ }
647
+
648
+ const timeoutId = setTimeout(() => {
649
+ setHasAnimationDone(true);
650
+ }, TRANSITIONS.ENTER_DURATION * 1000);
651
+
652
+ return () => clearTimeout(timeoutId);
653
+ }
654
+
655
+ // Reset animation state when drawer closes
656
+ setHasAnimationDone(false);
657
+ }, [isOpen, hasBeenOpened]);
658
+
616
659
  useEffect(() => {
617
660
  if (isOpen && snapPoints && fadeFromIndex === 0) {
618
661
  setShouldOverlayAnimate(true);
@@ -657,6 +700,9 @@ export function useDrawer(props: UseDrawerProps) {
657
700
  setIsOpen,
658
701
  closeOnInteractOutside,
659
702
  closeOnEscape,
703
+ hasAnimationDone,
704
+ closeButtonRef,
705
+ isCloseButtonRendered,
660
706
  }),
661
707
  [
662
708
  activeSnapPoint,
@@ -683,6 +729,9 @@ export function useDrawer(props: UseDrawerProps) {
683
729
  onRelease,
684
730
  onDrag,
685
731
  onPress,
732
+ hasAnimationDone,
733
+ closeButtonRef,
734
+ isCloseButtonRendered,
686
735
  ],
687
736
  );
688
737
  }