@seed-design/react-drawer 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/useDrawer.ts CHANGED
@@ -1,6 +1,7 @@
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
- import { isIOS, isMobileFirefox } from "./browser";
4
+ import { isAndroid, isIOS, isMobileFirefox } from "./browser";
4
5
  import {
5
6
  CLOSE_THRESHOLD,
6
7
  DRAG_CLASS,
@@ -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
  }
@@ -583,7 +606,7 @@ export function useDrawer(props: UseDrawerProps) {
583
606
  } else {
584
607
  drawerRef.current.style.height = `${Math.max(newDrawerHeight, visualViewportHeight - offsetFromTop)}px`;
585
608
  }
586
- } else if (!isMobileFirefox()) {
609
+ } else if (!isMobileFirefox() && !isAndroid()) {
587
610
  drawerRef.current.style.height = `${initialDrawerHeight.current}px`;
588
611
  }
589
612
 
@@ -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
  }