@seed-design/react-drawer 1.0.5 → 1.0.6

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.
@@ -5,7 +5,7 @@ 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';
8
+ import React, { useState, useCallback, useRef, useEffect, useMemo, createContext, useContext, forwardRef } from 'react';
9
9
  import { useControllableState } from '@radix-ui/react-use-controllable-state';
10
10
 
11
11
  function isMobileFirefox() {
@@ -482,8 +482,13 @@ function useDrawer(props) {
482
482
  }
483
483
  });
484
484
  const [hasBeenOpened, setHasBeenOpened] = useState(false);
485
+ const [hasAnimationDone, setHasAnimationDone] = useState(false);
485
486
  const [isDragging, setIsDragging] = useState(false);
486
487
  const [shouldOverlayAnimate, setShouldOverlayAnimate] = useState(false);
488
+ const [isCloseButtonRendered, setIsCloseButtonRendered] = useState(false);
489
+ const closeButtonRef = useCallback((node)=>{
490
+ setIsCloseButtonRendered(!!node);
491
+ }, []);
487
492
  const overlayRef = useRef(null);
488
493
  const openTime = useRef(null);
489
494
  const dragStartTime = useRef(null);
@@ -806,6 +811,7 @@ function useDrawer(props) {
806
811
  }, [
807
812
  modal
808
813
  ]);
814
+ // Effect 1: Track drawer open state
809
815
  useEffect(()=>{
810
816
  if (isOpen) {
811
817
  setHasBeenOpened(true);
@@ -813,6 +819,24 @@ function useDrawer(props) {
813
819
  }, [
814
820
  isOpen
815
821
  ]);
822
+ // Effect 2: Handle animation state and timer
823
+ useEffect(()=>{
824
+ if (isOpen) {
825
+ // Only reset animation state if this is the first open
826
+ if (!hasBeenOpened) {
827
+ setHasAnimationDone(false);
828
+ }
829
+ const timeoutId = setTimeout(()=>{
830
+ setHasAnimationDone(true);
831
+ }, TRANSITIONS.ENTER_DURATION * 1000);
832
+ return ()=>clearTimeout(timeoutId);
833
+ }
834
+ // Reset animation state when drawer closes
835
+ setHasAnimationDone(false);
836
+ }, [
837
+ isOpen,
838
+ hasBeenOpened
839
+ ]);
816
840
  useEffect(()=>{
817
841
  if (isOpen && snapPoints && fadeFromIndex === 0) {
818
842
  setShouldOverlayAnimate(true);
@@ -855,7 +879,10 @@ function useDrawer(props) {
855
879
  setHasBeenOpened,
856
880
  setIsOpen,
857
881
  closeOnInteractOutside,
858
- closeOnEscape
882
+ closeOnEscape,
883
+ hasAnimationDone,
884
+ closeButtonRef,
885
+ isCloseButtonRendered
859
886
  }), [
860
887
  activeSnapPoint,
861
888
  snapPoints,
@@ -880,7 +907,10 @@ function useDrawer(props) {
880
907
  closeOnEscape,
881
908
  onRelease,
882
909
  onDrag,
883
- onPress
910
+ onPress,
911
+ hasAnimationDone,
912
+ closeButtonRef,
913
+ isCloseButtonRendered
884
914
  ]);
885
915
  }
886
916
 
@@ -930,11 +960,10 @@ const DrawerPositioner = /*#__PURE__*/ forwardRef((props, ref)=>{
930
960
  });
931
961
  DrawerPositioner.displayName = "DrawerPositioner";
932
962
  const DrawerBackdrop = /*#__PURE__*/ forwardRef((props, ref)=>{
933
- const { overlayRef, onRelease, modal, snapPoints, isOpen, shouldFade, shouldOverlayAnimate } = useDrawerContext();
963
+ const { overlayRef, onRelease, modal, snapPoints, isOpen, shouldFade, shouldOverlayAnimate, hasAnimationDone } = useDrawerContext();
934
964
  const composedRef = useComposedRefs(ref, overlayRef);
935
965
  const hasSnapPoints = snapPoints && snapPoints.length > 0;
936
966
  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
967
  if (!modal) {
939
968
  return null;
940
969
  }
@@ -945,13 +974,14 @@ const DrawerBackdrop = /*#__PURE__*/ forwardRef((props, ref)=>{
945
974
  "data-snap-points-overlay": isOpen && shouldFade ? "true" : "false",
946
975
  "data-should-overlay-animate": shouldOverlayAnimate ? "true" : "false",
947
976
  "data-open": dataAttr(isOpen),
977
+ "data-animation-done": hasAnimationDone ? "true" : "false",
948
978
  ...props
949
979
  });
950
980
  });
951
981
  DrawerBackdrop.displayName = "DrawerBackdrop";
952
982
  const DrawerContent = /*#__PURE__*/ forwardRef((props, ref)=>{
953
983
  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();
984
+ const { drawerRef, onPress, onRelease, onDrag, keyboardIsOpen, snapPointsOffset, activeSnapPointIndex, modal, isOpen, direction, snapPoints, container, handleOnly, autoFocus, closeDrawer, closeOnInteractOutside, closeOnEscape, dismissible, hasAnimationDone } = useDrawerContext();
955
985
  // Needed to use transition instead of animations
956
986
  const [delayedSnapPoints, setDelayedSnapPoints] = useState(false);
957
987
  const composedRef = useComposedRefs(ref, drawerRef);
@@ -998,6 +1028,7 @@ const DrawerContent = /*#__PURE__*/ forwardRef((props, ref)=>{
998
1028
  "data-delayed-snap-points": delayedSnapPoints ? "true" : "false",
999
1029
  "data-drawer-direction": direction,
1000
1030
  "data-open": dataAttr(isOpen),
1031
+ "data-animation-done": hasAnimationDone ? "true" : "false",
1001
1032
  "data-drawer": "",
1002
1033
  "data-snap-points": isOpen && hasSnapPoints ? "true" : "false",
1003
1034
  "data-custom-container": container ? "true" : "false",
@@ -1033,10 +1064,10 @@ const DrawerContent = /*#__PURE__*/ forwardRef((props, ref)=>{
1033
1064
  }
1034
1065
  },
1035
1066
  onFocusOutside: (e)=>{
1036
- if (!modal) {
1037
- e.preventDefault();
1038
- return;
1039
- }
1067
+ props.onFocusOutside?.(e);
1068
+ // Always prevent focusOutside to avoid conflicts when focus moves between modals
1069
+ // (e.g., when Dialog closes and restores focus while BottomSheet is opening)
1070
+ e.preventDefault();
1040
1071
  },
1041
1072
  onPointerMove: (event)=>{
1042
1073
  lastKnownPointerEventRef.current = event;
@@ -1073,7 +1104,8 @@ const DrawerContent = /*#__PURE__*/ forwardRef((props, ref)=>{
1073
1104
  }
1074
1105
  },
1075
1106
  onInteractOutside: (e)=>{
1076
- if (dismissible && closeOnInteractOutside) {
1107
+ // Only close if event is not prevented (e.g., by onFocusOutside or onPointerDownOutside)
1108
+ if (dismissible && closeOnInteractOutside && !e.defaultPrevented) {
1077
1109
  closeDrawer();
1078
1110
  }
1079
1111
  props.onInteractOutside?.(e);
@@ -1089,15 +1121,25 @@ const DrawerContent = /*#__PURE__*/ forwardRef((props, ref)=>{
1089
1121
  DrawerContent.displayName = "DrawerContent";
1090
1122
  const DrawerTitle = DialogPrimitive.Title;
1091
1123
  const DrawerDescription = DialogPrimitive.Description;
1124
+ const DrawerHeader = /*#__PURE__*/ forwardRef((props, ref)=>{
1125
+ const { isCloseButtonRendered } = useDrawerContext();
1126
+ return /*#__PURE__*/ jsx(Primitive.div, {
1127
+ ref: ref,
1128
+ "data-show-close-button": dataAttr(isCloseButtonRendered),
1129
+ ...props
1130
+ });
1131
+ });
1132
+ DrawerHeader.displayName = "DrawerHeader";
1092
1133
  const DrawerCloseButton = /*#__PURE__*/ forwardRef((props, ref)=>{
1093
- const api = useDrawerContext();
1134
+ const { closeButtonRef, setIsOpen } = useDrawerContext();
1135
+ const composedRef = useComposedRefs(ref, closeButtonRef);
1094
1136
  return /*#__PURE__*/ jsx(Primitive.button, {
1095
- ref: ref,
1137
+ ref: composedRef,
1096
1138
  ...props,
1097
1139
  onClick: (e)=>{
1098
1140
  props.onClick?.(e);
1099
1141
  if (e.defaultPrevented) return;
1100
- api.setIsOpen(false);
1142
+ setIsOpen(false);
1101
1143
  }
1102
1144
  });
1103
1145
  });
@@ -1178,4 +1220,4 @@ const DrawerHandle = /*#__PURE__*/ forwardRef((props, ref)=>{
1178
1220
  });
1179
1221
  DrawerHandle.displayName = "DrawerHandle";
1180
1222
 
1181
- 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 };
1223
+ 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 };
@@ -505,8 +505,13 @@ function useDrawer(props) {
505
505
  }
506
506
  });
507
507
  const [hasBeenOpened, setHasBeenOpened] = React.useState(false);
508
+ const [hasAnimationDone, setHasAnimationDone] = React.useState(false);
508
509
  const [isDragging, setIsDragging] = React.useState(false);
509
510
  const [shouldOverlayAnimate, setShouldOverlayAnimate] = React.useState(false);
511
+ const [isCloseButtonRendered, setIsCloseButtonRendered] = React.useState(false);
512
+ const closeButtonRef = React.useCallback((node)=>{
513
+ setIsCloseButtonRendered(!!node);
514
+ }, []);
510
515
  const overlayRef = React.useRef(null);
511
516
  const openTime = React.useRef(null);
512
517
  const dragStartTime = React.useRef(null);
@@ -829,6 +834,7 @@ function useDrawer(props) {
829
834
  }, [
830
835
  modal
831
836
  ]);
837
+ // Effect 1: Track drawer open state
832
838
  React.useEffect(()=>{
833
839
  if (isOpen) {
834
840
  setHasBeenOpened(true);
@@ -836,6 +842,24 @@ function useDrawer(props) {
836
842
  }, [
837
843
  isOpen
838
844
  ]);
845
+ // Effect 2: Handle animation state and timer
846
+ React.useEffect(()=>{
847
+ if (isOpen) {
848
+ // Only reset animation state if this is the first open
849
+ if (!hasBeenOpened) {
850
+ setHasAnimationDone(false);
851
+ }
852
+ const timeoutId = setTimeout(()=>{
853
+ setHasAnimationDone(true);
854
+ }, TRANSITIONS.ENTER_DURATION * 1000);
855
+ return ()=>clearTimeout(timeoutId);
856
+ }
857
+ // Reset animation state when drawer closes
858
+ setHasAnimationDone(false);
859
+ }, [
860
+ isOpen,
861
+ hasBeenOpened
862
+ ]);
839
863
  React.useEffect(()=>{
840
864
  if (isOpen && snapPoints && fadeFromIndex === 0) {
841
865
  setShouldOverlayAnimate(true);
@@ -878,7 +902,10 @@ function useDrawer(props) {
878
902
  setHasBeenOpened,
879
903
  setIsOpen,
880
904
  closeOnInteractOutside,
881
- closeOnEscape
905
+ closeOnEscape,
906
+ hasAnimationDone,
907
+ closeButtonRef,
908
+ isCloseButtonRendered
882
909
  }), [
883
910
  activeSnapPoint,
884
911
  snapPoints,
@@ -903,7 +930,10 @@ function useDrawer(props) {
903
930
  closeOnEscape,
904
931
  onRelease,
905
932
  onDrag,
906
- onPress
933
+ onPress,
934
+ hasAnimationDone,
935
+ closeButtonRef,
936
+ isCloseButtonRendered
907
937
  ]);
908
938
  }
909
939
 
@@ -953,11 +983,10 @@ const DrawerPositioner = /*#__PURE__*/ React.forwardRef((props, ref)=>{
953
983
  });
954
984
  DrawerPositioner.displayName = "DrawerPositioner";
955
985
  const DrawerBackdrop = /*#__PURE__*/ React.forwardRef((props, ref)=>{
956
- const { overlayRef, onRelease, modal, snapPoints, isOpen, shouldFade, shouldOverlayAnimate } = useDrawerContext();
986
+ const { overlayRef, onRelease, modal, snapPoints, isOpen, shouldFade, shouldOverlayAnimate, hasAnimationDone } = useDrawerContext();
957
987
  const composedRef = reactComposeRefs.useComposedRefs(ref, overlayRef);
958
988
  const hasSnapPoints = snapPoints && snapPoints.length > 0;
959
989
  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
990
  if (!modal) {
962
991
  return null;
963
992
  }
@@ -968,13 +997,14 @@ const DrawerBackdrop = /*#__PURE__*/ React.forwardRef((props, ref)=>{
968
997
  "data-snap-points-overlay": isOpen && shouldFade ? "true" : "false",
969
998
  "data-should-overlay-animate": shouldOverlayAnimate ? "true" : "false",
970
999
  "data-open": domUtils.dataAttr(isOpen),
1000
+ "data-animation-done": hasAnimationDone ? "true" : "false",
971
1001
  ...props
972
1002
  });
973
1003
  });
974
1004
  DrawerBackdrop.displayName = "DrawerBackdrop";
975
1005
  const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
976
1006
  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();
1007
+ const { drawerRef, onPress, onRelease, onDrag, keyboardIsOpen, snapPointsOffset, activeSnapPointIndex, modal, isOpen, direction, snapPoints, container, handleOnly, autoFocus, closeDrawer, closeOnInteractOutside, closeOnEscape, dismissible, hasAnimationDone } = useDrawerContext();
978
1008
  // Needed to use transition instead of animations
979
1009
  const [delayedSnapPoints, setDelayedSnapPoints] = React.useState(false);
980
1010
  const composedRef = reactComposeRefs.useComposedRefs(ref, drawerRef);
@@ -1021,6 +1051,7 @@ const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1021
1051
  "data-delayed-snap-points": delayedSnapPoints ? "true" : "false",
1022
1052
  "data-drawer-direction": direction,
1023
1053
  "data-open": domUtils.dataAttr(isOpen),
1054
+ "data-animation-done": hasAnimationDone ? "true" : "false",
1024
1055
  "data-drawer": "",
1025
1056
  "data-snap-points": isOpen && hasSnapPoints ? "true" : "false",
1026
1057
  "data-custom-container": container ? "true" : "false",
@@ -1056,10 +1087,10 @@ const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1056
1087
  }
1057
1088
  },
1058
1089
  onFocusOutside: (e)=>{
1059
- if (!modal) {
1060
- e.preventDefault();
1061
- return;
1062
- }
1090
+ props.onFocusOutside?.(e);
1091
+ // Always prevent focusOutside to avoid conflicts when focus moves between modals
1092
+ // (e.g., when Dialog closes and restores focus while BottomSheet is opening)
1093
+ e.preventDefault();
1063
1094
  },
1064
1095
  onPointerMove: (event)=>{
1065
1096
  lastKnownPointerEventRef.current = event;
@@ -1096,7 +1127,8 @@ const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1096
1127
  }
1097
1128
  },
1098
1129
  onInteractOutside: (e)=>{
1099
- if (dismissible && closeOnInteractOutside) {
1130
+ // Only close if event is not prevented (e.g., by onFocusOutside or onPointerDownOutside)
1131
+ if (dismissible && closeOnInteractOutside && !e.defaultPrevented) {
1100
1132
  closeDrawer();
1101
1133
  }
1102
1134
  props.onInteractOutside?.(e);
@@ -1112,15 +1144,25 @@ const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1112
1144
  DrawerContent.displayName = "DrawerContent";
1113
1145
  const DrawerTitle = DialogPrimitive__namespace.Title;
1114
1146
  const DrawerDescription = DialogPrimitive__namespace.Description;
1147
+ const DrawerHeader = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1148
+ const { isCloseButtonRendered } = useDrawerContext();
1149
+ return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.div, {
1150
+ ref: ref,
1151
+ "data-show-close-button": domUtils.dataAttr(isCloseButtonRendered),
1152
+ ...props
1153
+ });
1154
+ });
1155
+ DrawerHeader.displayName = "DrawerHeader";
1115
1156
  const DrawerCloseButton = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1116
- const api = useDrawerContext();
1157
+ const { closeButtonRef, setIsOpen } = useDrawerContext();
1158
+ const composedRef = reactComposeRefs.useComposedRefs(ref, closeButtonRef);
1117
1159
  return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.button, {
1118
- ref: ref,
1160
+ ref: composedRef,
1119
1161
  ...props,
1120
1162
  onClick: (e)=>{
1121
1163
  props.onClick?.(e);
1122
1164
  if (e.defaultPrevented) return;
1123
- api.setIsOpen(false);
1165
+ setIsOpen(false);
1124
1166
  }
1125
1167
  });
1126
1168
  });
@@ -1206,6 +1248,7 @@ exports.DrawerCloseButton = DrawerCloseButton;
1206
1248
  exports.DrawerContent = DrawerContent;
1207
1249
  exports.DrawerDescription = DrawerDescription;
1208
1250
  exports.DrawerHandle = DrawerHandle;
1251
+ exports.DrawerHeader = DrawerHeader;
1209
1252
  exports.DrawerPositioner = DrawerPositioner;
1210
1253
  exports.DrawerRoot = DrawerRoot;
1211
1254
  exports.DrawerTitle = DrawerTitle;
package/lib/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- var Drawer12s = require('./Drawer-12s-CTzxFYGf.cjs');
1
+ var Drawer12s = require('./Drawer-12s-DriAYAR2.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,7 +1,7 @@
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
5
 
6
6
  interface UseDrawerProps {
7
7
  activeSnapPoint?: number | string | null;
@@ -106,9 +106,9 @@ interface UseDrawerProps {
106
106
  declare function useDrawer(props: UseDrawerProps): {
107
107
  activeSnapPoint: string | number | null;
108
108
  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>;
109
+ setActiveSnapPoint: (value: React$1.SetStateAction<string | number | null>) => void;
110
+ drawerRef: React$1.RefObject<HTMLDivElement | null>;
111
+ overlayRef: React$1.RefObject<HTMLDivElement | null>;
112
112
  shouldOverlayAnimate: boolean;
113
113
  onOpenChange: ((open: boolean) => void) | undefined;
114
114
  onPress: (event: React.PointerEvent<HTMLDivElement>) => void;
@@ -120,7 +120,7 @@ declare function useDrawer(props: UseDrawerProps): {
120
120
  isDragging: boolean;
121
121
  shouldFade: boolean;
122
122
  closeDrawer: (fromWithin?: boolean) => void;
123
- keyboardIsOpen: react.RefObject<boolean>;
123
+ keyboardIsOpen: React$1.RefObject<boolean>;
124
124
  modal: boolean;
125
125
  snapPointsOffset: number[];
126
126
  activeSnapPointIndex: number | null;
@@ -128,49 +128,55 @@ declare function useDrawer(props: UseDrawerProps): {
128
128
  noBodyStyles: boolean;
129
129
  container: HTMLElement | null | undefined;
130
130
  autoFocus: boolean;
131
- setHasBeenOpened: react.Dispatch<react.SetStateAction<boolean>>;
132
- setIsOpen: (value: react.SetStateAction<boolean>) => void;
131
+ setHasBeenOpened: React$1.Dispatch<React$1.SetStateAction<boolean>>;
132
+ setIsOpen: (value: React$1.SetStateAction<boolean>) => void;
133
133
  closeOnInteractOutside: boolean;
134
134
  closeOnEscape: boolean;
135
+ hasAnimationDone: boolean;
136
+ closeButtonRef: (node: HTMLButtonElement | null) => void;
137
+ isCloseButtonRendered: boolean;
135
138
  };
136
139
 
137
140
  interface DrawerRootProps extends UseDrawerProps {
138
- children?: react.ReactNode;
141
+ children?: React$1.ReactNode;
139
142
  }
140
143
  declare const DrawerRoot: (props: DrawerRootProps) => react_jsx_runtime.JSX.Element;
141
144
  interface DrawerTriggerProps extends DialogPrimitive.DialogTriggerProps {
142
145
  }
143
- declare const DrawerTrigger: react.ForwardRefExoticComponent<DialogPrimitive.DialogTriggerProps & react.RefAttributes<HTMLButtonElement>>;
144
- interface DrawerPositionerProps extends PrimitiveProps, react.HTMLAttributes<HTMLDivElement> {
146
+ declare const DrawerTrigger: React$1.ForwardRefExoticComponent<DialogPrimitive.DialogTriggerProps & React$1.RefAttributes<HTMLButtonElement>>;
147
+ interface DrawerPositionerProps extends PrimitiveProps, React$1.HTMLAttributes<HTMLDivElement> {
145
148
  }
146
- declare const DrawerPositioner: react.ForwardRefExoticComponent<DrawerPositionerProps & react.RefAttributes<HTMLDivElement>>;
147
- interface DrawerBackdropProps extends DialogPrimitive.DialogOverlayProps, react.HTMLAttributes<HTMLDivElement> {
149
+ declare const DrawerPositioner: React$1.ForwardRefExoticComponent<DrawerPositionerProps & React$1.RefAttributes<HTMLDivElement>>;
150
+ interface DrawerBackdropProps extends DialogPrimitive.DialogOverlayProps, React$1.HTMLAttributes<HTMLDivElement> {
148
151
  }
149
- declare const DrawerBackdrop: react.ForwardRefExoticComponent<DrawerBackdropProps & react.RefAttributes<HTMLDivElement>>;
150
- interface DrawerContentProps extends DialogPrimitive.DialogContentProps, react.HTMLAttributes<HTMLDivElement> {
152
+ declare const DrawerBackdrop: React$1.ForwardRefExoticComponent<DrawerBackdropProps & React$1.RefAttributes<HTMLDivElement>>;
153
+ interface DrawerContentProps extends DialogPrimitive.DialogContentProps, React$1.HTMLAttributes<HTMLDivElement> {
151
154
  }
152
- declare const DrawerContent: react.ForwardRefExoticComponent<DrawerContentProps & react.RefAttributes<HTMLDivElement>>;
155
+ declare const DrawerContent: React$1.ForwardRefExoticComponent<DrawerContentProps & React$1.RefAttributes<HTMLDivElement>>;
153
156
  interface DrawerTitleProps extends DialogPrimitive.DialogTitleProps {
154
157
  }
155
- declare const DrawerTitle: react.ForwardRefExoticComponent<DialogPrimitive.DialogTitleProps & react.RefAttributes<HTMLHeadingElement>>;
158
+ declare const DrawerTitle: React$1.ForwardRefExoticComponent<DialogPrimitive.DialogTitleProps & React$1.RefAttributes<HTMLHeadingElement>>;
156
159
  interface DrawerDescriptionProps extends DialogPrimitive.DialogDescriptionProps {
157
160
  }
158
- declare const DrawerDescription: react.ForwardRefExoticComponent<DialogPrimitive.DialogDescriptionProps & react.RefAttributes<HTMLParagraphElement>>;
161
+ declare const DrawerDescription: React$1.ForwardRefExoticComponent<DialogPrimitive.DialogDescriptionProps & React$1.RefAttributes<HTMLParagraphElement>>;
162
+ interface DrawerHeaderProps extends PrimitiveProps, React$1.HTMLAttributes<HTMLDivElement> {
163
+ }
164
+ declare const DrawerHeader: React$1.ForwardRefExoticComponent<DrawerHeaderProps & React$1.RefAttributes<HTMLDivElement>>;
159
165
  interface DrawerCloseButtonProps extends DialogPrimitive.DialogCloseProps {
160
166
  }
161
- declare const DrawerCloseButton: react.ForwardRefExoticComponent<DrawerCloseButtonProps & react.RefAttributes<HTMLButtonElement>>;
162
- interface DrawerHandleProps extends react.HTMLAttributes<HTMLDivElement> {
167
+ declare const DrawerCloseButton: React$1.ForwardRefExoticComponent<DrawerCloseButtonProps & React$1.RefAttributes<HTMLButtonElement>>;
168
+ interface DrawerHandleProps extends React$1.HTMLAttributes<HTMLDivElement> {
163
169
  preventCycle?: boolean;
164
170
  }
165
- declare const DrawerHandle: react.ForwardRefExoticComponent<DrawerHandleProps & react.RefAttributes<HTMLDivElement>>;
171
+ declare const DrawerHandle: React$1.ForwardRefExoticComponent<DrawerHandleProps & React$1.RefAttributes<HTMLDivElement>>;
166
172
 
167
173
  type DrawerContextValue = ReturnType<typeof useDrawer>;
168
174
  declare function useDrawerContext(): {
169
175
  activeSnapPoint: string | number | null;
170
176
  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>;
177
+ setActiveSnapPoint: (value: React$1.SetStateAction<string | number | null>) => void;
178
+ drawerRef: React$1.RefObject<HTMLDivElement | null>;
179
+ overlayRef: React$1.RefObject<HTMLDivElement | null>;
174
180
  shouldOverlayAnimate: boolean;
175
181
  onOpenChange: ((open: boolean) => void) | undefined;
176
182
  onPress: (event: React.PointerEvent<HTMLDivElement>) => void;
@@ -182,7 +188,7 @@ declare function useDrawerContext(): {
182
188
  isDragging: boolean;
183
189
  shouldFade: boolean;
184
190
  closeDrawer: (fromWithin?: boolean) => void;
185
- keyboardIsOpen: react.RefObject<boolean>;
191
+ keyboardIsOpen: React$1.RefObject<boolean>;
186
192
  modal: boolean;
187
193
  snapPointsOffset: number[];
188
194
  activeSnapPointIndex: number | null;
@@ -190,16 +196,19 @@ declare function useDrawerContext(): {
190
196
  noBodyStyles: boolean;
191
197
  container: HTMLElement | null | undefined;
192
198
  autoFocus: boolean;
193
- setHasBeenOpened: react.Dispatch<react.SetStateAction<boolean>>;
194
- setIsOpen: (value: react.SetStateAction<boolean>) => void;
199
+ setHasBeenOpened: React$1.Dispatch<React$1.SetStateAction<boolean>>;
200
+ setIsOpen: (value: React$1.SetStateAction<boolean>) => void;
195
201
  closeOnInteractOutside: boolean;
196
202
  closeOnEscape: boolean;
203
+ hasAnimationDone: boolean;
204
+ closeButtonRef: (node: HTMLButtonElement | null) => void;
205
+ isCloseButtonRendered: boolean;
197
206
  };
198
207
 
199
208
  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 };
209
+ 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 };
210
+ 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
211
  }
203
212
 
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 };
213
+ export { Drawer_namespace as Drawer, DrawerBackdrop, DrawerCloseButton, DrawerContent, DrawerDescription, DrawerHandle, DrawerHeader, DrawerPositioner, DrawerRoot, DrawerTitle, DrawerTrigger, useDrawer, useDrawerContext };
214
+ 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-9vODHlci.js';
2
- export { u as useDrawer, i as useDrawerContext } from './Drawer-12s-9vODHlci.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-BdeYHia6.js';
2
+ export { u as useDrawer, j as useDrawerContext } from './Drawer-12s-BdeYHia6.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.5",
3
+ "version": "1.0.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/daangn/seed-design.git",
@@ -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
@@ -72,12 +72,12 @@ export const DrawerBackdrop = forwardRef<HTMLDivElement, DrawerBackdropProps>((p
72
72
  isOpen,
73
73
  shouldFade,
74
74
  shouldOverlayAnimate,
75
+ hasAnimationDone,
75
76
  } = useDrawerContext();
76
77
  const composedRef = useComposedRefs(ref, overlayRef);
77
78
  const hasSnapPoints = snapPoints && snapPoints.length > 0;
78
79
  const onMouseUp = useCallbackRef((event: React.PointerEvent<HTMLDivElement>) => onRelease(event));
79
80
 
80
- // Overlay is the component that is locking scroll, removing it will unlock the scroll without having to dig into Radix's Dialog library
81
81
  if (!modal) {
82
82
  return null;
83
83
  }
@@ -90,6 +90,7 @@ export const DrawerBackdrop = forwardRef<HTMLDivElement, DrawerBackdropProps>((p
90
90
  data-snap-points-overlay={isOpen && shouldFade ? "true" : "false"}
91
91
  data-should-overlay-animate={shouldOverlayAnimate ? "true" : "false"}
92
92
  data-open={dataAttr(isOpen)}
93
+ data-animation-done={hasAnimationDone ? "true" : "false"}
93
94
  {...props}
94
95
  />
95
96
  );
@@ -121,6 +122,7 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>((pro
121
122
  closeOnInteractOutside,
122
123
  closeOnEscape,
123
124
  dismissible,
125
+ hasAnimationDone,
124
126
  } = useDrawerContext();
125
127
  // Needed to use transition instead of animations
126
128
  const [delayedSnapPoints, setDelayedSnapPoints] = useState(false);
@@ -177,6 +179,7 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>((pro
177
179
  data-delayed-snap-points={delayedSnapPoints ? "true" : "false"}
178
180
  data-drawer-direction={direction}
179
181
  data-open={dataAttr(isOpen)}
182
+ data-animation-done={hasAnimationDone ? "true" : "false"}
180
183
  data-drawer=""
181
184
  data-snap-points={isOpen && hasSnapPoints ? "true" : "false"}
182
185
  data-custom-container={container ? "true" : "false"}
@@ -216,10 +219,10 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>((pro
216
219
  }
217
220
  }}
218
221
  onFocusOutside={(e) => {
219
- if (!modal) {
220
- e.preventDefault();
221
- return;
222
- }
222
+ props.onFocusOutside?.(e);
223
+ // Always prevent focusOutside to avoid conflicts when focus moves between modals
224
+ // (e.g., when Dialog closes and restores focus while BottomSheet is opening)
225
+ e.preventDefault();
223
226
  }}
224
227
  onPointerMove={(event) => {
225
228
  lastKnownPointerEventRef.current = event;
@@ -258,7 +261,8 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>((pro
258
261
  }
259
262
  }}
260
263
  onInteractOutside={(e) => {
261
- if (dismissible && closeOnInteractOutside) {
264
+ // Only close if event is not prevented (e.g., by onFocusOutside or onPointerDownOutside)
265
+ if (dismissible && closeOnInteractOutside && !e.defaultPrevented) {
262
266
  closeDrawer();
263
267
  }
264
268
  props.onInteractOutside?.(e);
@@ -282,19 +286,30 @@ export interface DrawerDescriptionProps extends DialogPrimitive.DialogDescriptio
282
286
 
283
287
  export const DrawerDescription = DialogPrimitive.Description;
284
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
+
285
299
  export interface DrawerCloseButtonProps extends DialogPrimitive.DialogCloseProps {}
286
300
 
287
301
  export const DrawerCloseButton = forwardRef<HTMLButtonElement, DrawerCloseButtonProps>(
288
302
  (props, ref) => {
289
- const api = useDrawerContext();
303
+ const { closeButtonRef, setIsOpen } = useDrawerContext();
304
+ const composedRef = useComposedRefs(ref, closeButtonRef);
290
305
  return (
291
306
  <Primitive.button
292
- ref={ref}
307
+ ref={composedRef}
293
308
  {...props}
294
309
  onClick={(e) => {
295
310
  props.onClick?.(e);
296
311
  if (e.defaultPrevented) return;
297
- api.setIsOpen(false);
312
+ setIsOpen(false);
298
313
  }}
299
314
  />
300
315
  );
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,
package/src/useDrawer.ts CHANGED
@@ -116,7 +116,6 @@ export interface UseDrawerProps {
116
116
  * @default true
117
117
  */
118
118
  closeOnEscape?: boolean;
119
-
120
119
  }
121
120
 
122
121
  export function useDrawer(props: UseDrawerProps) {
@@ -179,9 +178,15 @@ export function useDrawer(props: UseDrawerProps) {
179
178
  });
180
179
 
181
180
  const [hasBeenOpened, setHasBeenOpened] = useState<boolean>(false);
181
+ const [hasAnimationDone, setHasAnimationDone] = useState<boolean>(false);
182
182
  const [isDragging, setIsDragging] = useState<boolean>(false);
183
183
  const [shouldOverlayAnimate, setShouldOverlayAnimate] = useState<boolean>(false);
184
184
 
185
+ const [isCloseButtonRendered, setIsCloseButtonRendered] = useState<boolean>(false);
186
+ const closeButtonRef = useCallback((node: HTMLButtonElement | null) => {
187
+ setIsCloseButtonRendered(!!node);
188
+ }, []);
189
+
185
190
  const overlayRef = useRef<HTMLDivElement>(null);
186
191
  const openTime = useRef<Date | null>(null);
187
192
  const dragStartTime = useRef<Date | null>(null);
@@ -543,7 +548,6 @@ export function useDrawer(props: UseDrawerProps) {
543
548
  };
544
549
  }, [isOpen]);
545
550
 
546
-
547
551
  useEffect(() => {
548
552
  function onVisualViewportChange() {
549
553
  if (!drawerRef.current || !repositionInputs) return;
@@ -609,12 +613,32 @@ export function useDrawer(props: UseDrawerProps) {
609
613
  }
610
614
  }, [modal]);
611
615
 
616
+ // Effect 1: Track drawer open state
612
617
  useEffect(() => {
613
618
  if (isOpen) {
614
619
  setHasBeenOpened(true);
615
620
  }
616
621
  }, [isOpen]);
617
622
 
623
+ // Effect 2: Handle animation state and timer
624
+ useEffect(() => {
625
+ if (isOpen) {
626
+ // Only reset animation state if this is the first open
627
+ if (!hasBeenOpened) {
628
+ setHasAnimationDone(false);
629
+ }
630
+
631
+ const timeoutId = setTimeout(() => {
632
+ setHasAnimationDone(true);
633
+ }, TRANSITIONS.ENTER_DURATION * 1000);
634
+
635
+ return () => clearTimeout(timeoutId);
636
+ }
637
+
638
+ // Reset animation state when drawer closes
639
+ setHasAnimationDone(false);
640
+ }, [isOpen, hasBeenOpened]);
641
+
618
642
  useEffect(() => {
619
643
  if (isOpen && snapPoints && fadeFromIndex === 0) {
620
644
  setShouldOverlayAnimate(true);
@@ -659,6 +683,9 @@ export function useDrawer(props: UseDrawerProps) {
659
683
  setIsOpen,
660
684
  closeOnInteractOutside,
661
685
  closeOnEscape,
686
+ hasAnimationDone,
687
+ closeButtonRef,
688
+ isCloseButtonRendered,
662
689
  }),
663
690
  [
664
691
  activeSnapPoint,
@@ -685,6 +712,9 @@ export function useDrawer(props: UseDrawerProps) {
685
712
  onRelease,
686
713
  onDrag,
687
714
  onPress,
715
+ hasAnimationDone,
716
+ closeButtonRef,
717
+ isCloseButtonRendered,
688
718
  ],
689
719
  );
690
720
  }