@seed-design/react-drawer 1.0.11 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 DrawerHeader, f as DrawerPositioner, g as DrawerRoot, h as DrawerTitle, i as DrawerTrigger } from './Drawer-12s-DxbsRb10.js';
2
- export { u as useDrawer, j as useDrawerContext } from './Drawer-12s-DxbsRb10.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-5syEwTtq.js';
2
+ export { u as useDrawer, j as useDrawerContext } from './Drawer-12s-5syEwTtq.js';
3
3
 
4
4
  var Drawer_namespace = {
5
5
  __proto__: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seed-design/react-drawer",
3
- "version": "1.0.11",
3
+ "version": "2.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/daangn/seed-design.git",
@@ -28,11 +28,15 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@radix-ui/react-compose-refs": "^1.1.2",
31
- "@radix-ui/react-dialog": "^1.1.15",
31
+ "@radix-ui/react-focus-scope": "^1.1.4",
32
32
  "@radix-ui/react-use-callback-ref": "^1.1.0",
33
- "@seed-design/react-use-controllable-state": "1.0.0",
34
- "@seed-design/dom-utils": "1.0.0",
35
- "@seed-design/react-primitive": "1.0.0"
33
+ "@seed-design/dom-utils": "^2.0.0",
34
+ "@seed-design/react-dismissible-layer": "^1.0.0",
35
+ "@seed-design/react-presence": "^1.0.0",
36
+ "@seed-design/react-prevent-scroll": "^1.0.0",
37
+ "@seed-design/react-primitive": "^2.0.0",
38
+ "@seed-design/react-use-controllable-state": "^2.0.0",
39
+ "aria-hidden": "^1.2.6"
36
40
  },
37
41
  "devDependencies": {
38
42
  "@types/react": "^19.1.6",
package/src/Drawer.tsx CHANGED
@@ -1,12 +1,16 @@
1
1
  "use client";
2
2
 
3
- import { useComposedRefs } from "@radix-ui/react-compose-refs";
4
- import * as DialogPrimitive from "@radix-ui/react-dialog";
3
+ import { composeRefs } from "@radix-ui/react-compose-refs";
4
+ import { FocusScope } from "@radix-ui/react-focus-scope";
5
5
  import { useCallbackRef } from "@radix-ui/react-use-callback-ref";
6
- import { dataAttr } from "@seed-design/dom-utils";
6
+ import { hideOthers } from "aria-hidden";
7
+ import { DismissibleLayer } from "@seed-design/react-dismissible-layer";
8
+ import { Presence } from "@seed-design/react-presence";
9
+ import { dataAttr, mergeProps } from "@seed-design/dom-utils";
10
+ import { usePreventScroll } from "@seed-design/react-prevent-scroll";
7
11
  import { Primitive, type PrimitiveProps } from "@seed-design/react-primitive";
8
12
  import type * as React from "react";
9
- import { forwardRef, useEffect, useRef, useState } from "react";
13
+ import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
10
14
  import type { DrawerDirection } from "./types";
11
15
  import { useDrawer, type UseDrawerProps } from "./useDrawer";
12
16
  import { DrawerProvider, useDrawerContext } from "./useDrawerContext";
@@ -16,32 +20,19 @@ export interface DrawerRootProps extends UseDrawerProps {
16
20
  }
17
21
 
18
22
  export const DrawerRoot = (props: DrawerRootProps) => {
19
- const { children, defaultOpen, dismissible, modal } = props;
20
23
  const api = useDrawer(props);
21
- return (
22
- <DialogPrimitive.Root
23
- defaultOpen={defaultOpen}
24
- open={api.isOpen}
25
- onOpenChange={(open) => {
26
- if (!dismissible && !open) return;
27
- if (open) {
28
- api.setHasBeenOpened(true);
29
- } else {
30
- api.closeDrawer(true);
31
- }
32
-
33
- api.setIsOpen(open);
34
- }}
35
- modal={modal}
36
- >
37
- <DrawerProvider value={api}>{children}</DrawerProvider>
38
- </DialogPrimitive.Root>
39
- );
24
+ return <DrawerProvider value={api}>{props.children}</DrawerProvider>;
40
25
  };
41
26
 
42
- export interface DrawerTriggerProps extends DialogPrimitive.DialogTriggerProps {}
27
+ export interface DrawerTriggerProps
28
+ extends PrimitiveProps,
29
+ React.ButtonHTMLAttributes<HTMLButtonElement> {}
43
30
 
44
- export const DrawerTrigger = DialogPrimitive.Trigger;
31
+ export const DrawerTrigger = forwardRef<HTMLButtonElement, DrawerTriggerProps>((props, ref) => {
32
+ const api = useDrawerContext();
33
+ return <Primitive.button ref={ref} {...mergeProps(api.triggerProps, props)} />;
34
+ });
35
+ DrawerTrigger.displayName = "DrawerTrigger";
45
36
 
46
37
  export interface DrawerPositionerProps
47
38
  extends PrimitiveProps,
@@ -49,19 +40,11 @@ export interface DrawerPositionerProps
49
40
 
50
41
  export const DrawerPositioner = forwardRef<HTMLDivElement, DrawerPositionerProps>((props, ref) => {
51
42
  const api = useDrawerContext();
52
- return (
53
- <Primitive.div
54
- ref={ref}
55
- {...props}
56
- style={{ pointerEvents: api.isOpen ? undefined : "none", ...props.style }}
57
- />
58
- );
43
+ return <Primitive.div ref={ref} {...mergeProps(api.positionerProps, props)} />;
59
44
  });
60
45
  DrawerPositioner.displayName = "DrawerPositioner";
61
46
 
62
- export interface DrawerBackdropProps
63
- extends DialogPrimitive.DialogOverlayProps,
64
- React.HTMLAttributes<HTMLDivElement> {}
47
+ export interface DrawerBackdropProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {}
65
48
 
66
49
  export const DrawerBackdrop = forwardRef<HTMLDivElement, DrawerBackdropProps>((props, ref) => {
67
50
  const {
@@ -73,8 +56,10 @@ export const DrawerBackdrop = forwardRef<HTMLDivElement, DrawerBackdropProps>((p
73
56
  shouldFade,
74
57
  shouldOverlayAnimate,
75
58
  hasAnimationDone,
59
+ lazyMount,
60
+ unmountOnExit,
76
61
  } = useDrawerContext();
77
- const composedRef = useComposedRefs(ref, overlayRef);
62
+ const composedRef = composeRefs(ref, overlayRef);
78
63
  const hasSnapPoints = snapPoints && snapPoints.length > 0;
79
64
  const onMouseUp = useCallbackRef((event: React.PointerEvent<HTMLDivElement>) => onRelease(event));
80
65
 
@@ -83,26 +68,27 @@ export const DrawerBackdrop = forwardRef<HTMLDivElement, DrawerBackdropProps>((p
83
68
  }
84
69
 
85
70
  return (
86
- <DialogPrimitive.Overlay
87
- ref={composedRef}
88
- onMouseUp={onMouseUp}
89
- data-snap-points={isOpen && hasSnapPoints ? "true" : "false"}
90
- data-snap-points-overlay={isOpen && shouldFade ? "true" : "false"}
91
- data-should-overlay-animate={shouldOverlayAnimate ? "true" : "false"}
92
- data-open={dataAttr(isOpen)}
93
- data-animation-done={hasAnimationDone ? "true" : "false"}
94
- {...props}
95
- />
71
+ <Presence present={isOpen} unmountOnExit={unmountOnExit} lazyMount={lazyMount}>
72
+ <Primitive.div
73
+ ref={composedRef}
74
+ onMouseUp={onMouseUp}
75
+ data-snap-points={isOpen && hasSnapPoints ? "true" : "false"}
76
+ data-snap-points-overlay={isOpen && shouldFade ? "true" : "false"}
77
+ data-should-overlay-animate={shouldOverlayAnimate ? "true" : "false"}
78
+ data-open={dataAttr(isOpen)}
79
+ data-animation-done={hasAnimationDone ? "true" : "false"}
80
+ {...props}
81
+ />
82
+ </Presence>
96
83
  );
97
84
  });
98
85
  DrawerBackdrop.displayName = "DrawerBackdrop";
99
86
 
100
- export interface DrawerContentProps
101
- extends DialogPrimitive.DialogContentProps,
102
- React.HTMLAttributes<HTMLDivElement> {}
87
+ export interface DrawerContentProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {}
103
88
 
104
89
  export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>((props, ref) => {
105
- const { onPointerDownOutside, style, onOpenAutoFocus, ...restProps } = props;
90
+ const { style, ...restProps } = props;
91
+
106
92
  const {
107
93
  drawerRef,
108
94
  onPress,
@@ -123,10 +109,27 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>((pro
123
109
  closeOnEscape,
124
110
  dismissible,
125
111
  hasAnimationDone,
112
+ titleId,
113
+ descriptionId,
114
+ lazyMount,
115
+ unmountOnExit,
126
116
  } = useDrawerContext();
117
+
118
+ // Lock body scroll while the modal drawer is open. Mirrors the `modal` contract: the lock
119
+ // releases the moment the drawer is non-modal or closed.
120
+ usePreventScroll({ isDisabled: !(modal && isOpen) });
121
+
122
+ const [contentNode, setContentNode] = useState<HTMLDivElement | null>(null);
123
+ const contentRef = useCallback((el: HTMLDivElement | null) => setContentNode(el), []);
124
+
125
+ // aria-hide everything except the content when modal
126
+ useEffect(() => {
127
+ if (!isOpen || !modal || !contentNode) return;
128
+ return hideOthers(contentNode);
129
+ }, [isOpen, modal, contentNode]);
130
+
127
131
  // Needed to use transition instead of animations
128
132
  const [delayedSnapPoints, setDelayedSnapPoints] = useState(false);
129
- const composedRef = useComposedRefs(ref, drawerRef);
130
133
  const pointerStartRef = useRef<{ x: number; y: number } | null>(null);
131
134
  const lastKnownPointerEventRef = useRef<React.PointerEvent<HTMLDivElement> | null>(null);
132
135
  const wasBeyondThePointRef = useRef(false);
@@ -138,7 +141,7 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>((pro
138
141
 
139
142
  const isDeltaInDirection = (
140
143
  delta: { x: number; y: number },
141
- direction: DrawerDirection,
144
+ dir: DrawerDirection,
142
145
  threshold = 0,
143
146
  ) => {
144
147
  if (wasBeyondThePointRef.current) return true;
@@ -146,9 +149,9 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>((pro
146
149
  const deltaY = Math.abs(delta.y);
147
150
  const deltaX = Math.abs(delta.x);
148
151
  const isDeltaX = deltaX > deltaY;
149
- const dFactor = ["bottom", "right"].includes(direction) ? 1 : -1;
152
+ const dFactor = ["bottom", "right"].includes(dir) ? 1 : -1;
150
153
 
151
- if (direction === "left" || direction === "right") {
154
+ if (dir === "left" || dir === "right") {
152
155
  const isReverseDirection = delta.x * dFactor < 0;
153
156
  if (!isReverseDirection && deltaX >= 0 && deltaX <= threshold) {
154
157
  return isDeltaX;
@@ -179,160 +182,184 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>((pro
179
182
  }
180
183
 
181
184
  return (
182
- <DialogPrimitive.Content
183
- data-delayed-snap-points={delayedSnapPoints ? "true" : "false"}
184
- data-drawer-direction={direction}
185
- data-open={dataAttr(isOpen)}
186
- data-animation-done={hasAnimationDone ? "true" : "false"}
187
- data-drawer=""
188
- data-snap-points={isOpen && hasSnapPoints ? "true" : "false"}
189
- data-custom-container={container ? "true" : "false"}
190
- {...restProps}
191
- ref={composedRef}
192
- style={
193
- snapPointsOffset && snapPointsOffset.length > 0
194
- ? ({
195
- "--snap-point-height": `${snapPointsOffset[activeSnapPointIndex ?? 0]!}px`,
196
- ...style,
197
- } as React.CSSProperties)
198
- : (style ?? {})
199
- }
200
- onPointerDown={(event) => {
201
- if (handleOnly) return;
202
- restProps.onPointerDown?.(event);
203
- pointerStartRef.current = { x: event.pageX, y: event.pageY };
204
- draggedRef.current = false;
205
- onPress(event);
206
- }}
207
- onOpenAutoFocus={(e) => {
208
- onOpenAutoFocus?.(e);
209
-
210
- if (!autoFocus) {
211
- e.preventDefault();
212
- }
213
- }}
214
- onPointerDownOutside={(e) => {
215
- onPointerDownOutside?.(e);
216
-
217
- if (!modal || e.defaultPrevented) {
218
- e.preventDefault();
219
- return;
220
- }
221
-
222
- if (keyboardIsOpen.current) {
223
- keyboardIsOpen.current = false;
224
- }
225
- }}
226
- onFocusOutside={(e) => {
227
- props.onFocusOutside?.(e);
228
- // Always prevent focusOutside to avoid conflicts when focus moves between modals
229
- // (e.g., when Dialog closes and restores focus while BottomSheet is opening)
230
- e.preventDefault();
231
- }}
232
- onPointerMove={(event) => {
233
- lastKnownPointerEventRef.current = event;
234
- if (handleOnly) return;
235
- restProps.onPointerMove?.(event);
236
- if (!pointerStartRef.current) return;
237
- const yPosition = event.pageY - pointerStartRef.current.y;
238
- const xPosition = event.pageX - pointerStartRef.current.x;
239
-
240
- const swipeStartThreshold = event.pointerType === "touch" ? 10 : 2;
241
- const delta = { x: xPosition, y: yPosition };
242
-
243
- // Once the pointer travels past the swipe-start threshold the gesture
244
- // is a drag, not a tap — remember it so the trailing click can be
245
- // suppressed on release (iOS still synthesizes one on the origin).
246
- const isBeyondThreshold =
247
- Math.abs(xPosition) > swipeStartThreshold || Math.abs(yPosition) > swipeStartThreshold;
248
- if (isBeyondThreshold) draggedRef.current = true;
249
-
250
- const isAllowedToSwipe = isDeltaInDirection(delta, direction, swipeStartThreshold);
251
- if (isAllowedToSwipe) onDrag(event);
252
- else if (isBeyondThreshold) {
253
- pointerStartRef.current = null;
254
- }
255
- }}
256
- onPointerUp={(event) => {
257
- restProps.onPointerUp?.(event);
258
- pointerStartRef.current = null;
259
- wasBeyondThePointRef.current = false;
260
- onRelease(event);
261
- }}
262
- onPointerOut={(event) => {
263
- restProps.onPointerOut?.(event);
264
- handleOnPointerUp(lastKnownPointerEventRef.current);
265
- }}
266
- onContextMenu={(event) => {
267
- restProps.onContextMenu?.(event);
268
- if (lastKnownPointerEventRef.current) {
269
- handleOnPointerUp(lastKnownPointerEventRef.current);
270
- }
271
- }}
272
- onClickCapture={(event) => {
273
- restProps.onClickCapture?.(event);
274
- // Swallow the click that follows a drag so dragging the sheet (e.g. a
275
- // swipe-to-dismiss started on an item) doesn't also activate that item.
276
- // A plain tap leaves draggedRef false and clicks normally.
277
- if (draggedRef.current) {
278
- event.preventDefault();
279
- event.stopPropagation();
280
- draggedRef.current = false;
281
- }
282
- }}
283
- onInteractOutside={(e) => {
284
- // Only close if event is not prevented (e.g., by onFocusOutside or onPointerDownOutside)
285
- if (dismissible && closeOnInteractOutside && !e.defaultPrevented) {
286
- closeDrawer(false, { reason: "interactOutside", event: e.detail.originalEvent });
287
- }
288
- props.onInteractOutside?.(e);
289
- }}
290
- onEscapeKeyDown={(e) => {
291
- if (dismissible && closeOnEscape) {
185
+ <Presence present={isOpen} unmountOnExit={unmountOnExit} lazyMount={lazyMount}>
186
+ {/* DismissibleLayer must wrap FocusScope, not the other way around.
187
+ FocusScope asChild uses Slot to forward tabIndex/onKeyDown/ref to the
188
+ DOM element; if DismissibleLayer sits between them, those props are
189
+ swallowed by DismissibleLayer's own destructuring and never reach the DOM. */}
190
+ <DismissibleLayer
191
+ enabled={isOpen}
192
+ onEscapeKeyDown={(e) => {
193
+ if (e.defaultPrevented) return;
194
+ if (!dismissible || !closeOnEscape) return;
292
195
  closeDrawer(false, { reason: "escapeKeyDown", event: e });
293
- }
294
- props.onEscapeKeyDown?.(e);
295
- }}
296
- />
196
+ }}
197
+ onPressOutside={(e) => {
198
+ if (e.defaultPrevented) return;
199
+ if (!modal) return;
200
+ if (keyboardIsOpen.current) keyboardIsOpen.current = false;
201
+
202
+ if (!dismissible || !closeOnInteractOutside) return;
203
+ closeDrawer(false, { reason: "interactOutside", event: e });
204
+ }}
205
+ onFocusOutside={() => {
206
+ // Focus trapping is handled by FocusScope — nothing to do here.
207
+ // The old Radix architecture needed e.preventDefault() here (PR #1187)
208
+ // to prevent cascade closes, but the new DismissibleLayer handles
209
+ // that via onCascadeDismiss instead.
210
+ }}
211
+ onCascadeDismiss={({ dismissedParent }) => {
212
+ closeDrawer(false, { reason: "cascadeDismiss", dismissedParent });
213
+ }}
214
+ >
215
+ <FocusScope
216
+ asChild
217
+ loop={modal}
218
+ trapped={modal && isOpen}
219
+ onMountAutoFocus={(e) => {
220
+ // prevent FocusScope's default autoFocus behavior
221
+ e.preventDefault();
222
+
223
+ // when autoFocus is true, FocusScope sets the focus to the first tabbable element; otherwise content;
224
+ // the desired behavior is to set the focus to the content regardless of whether there are tabbable elements or not when true]
225
+ if (autoFocus) {
226
+ drawerRef.current?.focus();
227
+ }
228
+
229
+ // later, we can do something like:
230
+ // when trigger has clicked using keyboard, focus the first tabbable element;
231
+ // when trigger has clicked using mouse, focus the content
232
+ // -> matches Menu behavior
233
+ }}
234
+ >
235
+ <Primitive.div
236
+ role="dialog"
237
+ aria-modal={modal}
238
+ aria-labelledby={titleId}
239
+ aria-describedby={descriptionId}
240
+ data-delayed-snap-points={delayedSnapPoints ? "true" : "false"}
241
+ data-drawer-direction={direction}
242
+ data-open={dataAttr(isOpen)}
243
+ data-animation-done={hasAnimationDone ? "true" : "false"}
244
+ data-drawer=""
245
+ data-snap-points={isOpen && hasSnapPoints ? "true" : "false"}
246
+ data-custom-container={container ? "true" : "false"}
247
+ {...restProps}
248
+ ref={composeRefs(ref, drawerRef, contentRef)}
249
+ style={{
250
+ ...(snapPointsOffset && snapPointsOffset.length > 0
251
+ ? ({
252
+ "--snap-point-height": `${snapPointsOffset[activeSnapPointIndex ?? 0]!}px`,
253
+ ...style,
254
+ } as React.CSSProperties)
255
+ : style),
256
+ ...(!modal && { pointerEvents: "auto" }),
257
+ }}
258
+ onPointerDown={(event) => {
259
+ if (handleOnly) return;
260
+ restProps.onPointerDown?.(event);
261
+ pointerStartRef.current = { x: event.pageX, y: event.pageY };
262
+ draggedRef.current = false;
263
+ onPress(event);
264
+ }}
265
+ onPointerMove={(event) => {
266
+ lastKnownPointerEventRef.current = event;
267
+ if (handleOnly) return;
268
+ restProps.onPointerMove?.(event);
269
+ if (!pointerStartRef.current) return;
270
+ const yPosition = event.pageY - pointerStartRef.current.y;
271
+ const xPosition = event.pageX - pointerStartRef.current.x;
272
+
273
+ const swipeStartThreshold = event.pointerType === "touch" ? 10 : 2;
274
+ const delta = { x: xPosition, y: yPosition };
275
+
276
+ // Once the pointer travels past the swipe-start threshold the gesture
277
+ // is a drag, not a tap — remember it so the trailing click can be
278
+ // suppressed on release (iOS still synthesizes one on the origin).
279
+ const isBeyondThreshold =
280
+ Math.abs(xPosition) > swipeStartThreshold ||
281
+ Math.abs(yPosition) > swipeStartThreshold;
282
+ if (isBeyondThreshold) draggedRef.current = true;
283
+
284
+ const isAllowedToSwipe = isDeltaInDirection(delta, direction, swipeStartThreshold);
285
+ if (isAllowedToSwipe) onDrag(event);
286
+ else if (isBeyondThreshold) {
287
+ pointerStartRef.current = null;
288
+ }
289
+ }}
290
+ onPointerUp={(event) => {
291
+ restProps.onPointerUp?.(event);
292
+ pointerStartRef.current = null;
293
+ wasBeyondThePointRef.current = false;
294
+ onRelease(event);
295
+ }}
296
+ onPointerOut={(event) => {
297
+ restProps.onPointerOut?.(event);
298
+ handleOnPointerUp(lastKnownPointerEventRef.current);
299
+ }}
300
+ onContextMenu={(event) => {
301
+ restProps.onContextMenu?.(event);
302
+ if (lastKnownPointerEventRef.current) {
303
+ handleOnPointerUp(lastKnownPointerEventRef.current);
304
+ }
305
+ }}
306
+ onClickCapture={(event) => {
307
+ restProps.onClickCapture?.(event);
308
+ // Swallow the click that follows a drag so dragging the sheet (e.g. a
309
+ // swipe-to-dismiss started on an item) doesn't also activate that item.
310
+ // A plain tap leaves draggedRef false and clicks normally.
311
+ if (draggedRef.current) {
312
+ event.preventDefault();
313
+ event.stopPropagation();
314
+ draggedRef.current = false;
315
+ }
316
+ }}
317
+ />
318
+ </FocusScope>
319
+ </DismissibleLayer>
320
+ </Presence>
297
321
  );
298
322
  });
299
323
  DrawerContent.displayName = "DrawerContent";
300
324
 
301
- export interface DrawerTitleProps extends DialogPrimitive.DialogTitleProps {}
325
+ export interface DrawerTitleProps
326
+ extends PrimitiveProps,
327
+ React.HTMLAttributes<HTMLHeadingElement> {}
302
328
 
303
- export const DrawerTitle = DialogPrimitive.Title;
329
+ export const DrawerTitle = forwardRef<HTMLHeadingElement, DrawerTitleProps>((props, ref) => {
330
+ const api = useDrawerContext();
331
+ return <Primitive.h2 ref={ref} {...mergeProps(api.titleProps, props)} />;
332
+ });
333
+ DrawerTitle.displayName = "DrawerTitle";
304
334
 
305
- export interface DrawerDescriptionProps extends DialogPrimitive.DialogDescriptionProps {}
335
+ export interface DrawerDescriptionProps
336
+ extends PrimitiveProps,
337
+ React.HTMLAttributes<HTMLParagraphElement> {}
306
338
 
307
- export const DrawerDescription = DialogPrimitive.Description;
339
+ export const DrawerDescription = forwardRef<HTMLParagraphElement, DrawerDescriptionProps>(
340
+ (props, ref) => {
341
+ const api = useDrawerContext();
342
+ return <Primitive.p ref={ref} {...mergeProps(api.descriptionProps, props)} />;
343
+ },
344
+ );
345
+ DrawerDescription.displayName = "DrawerDescription";
308
346
 
309
347
  export interface DrawerHeaderProps extends PrimitiveProps, React.HTMLAttributes<HTMLDivElement> {}
310
348
 
311
349
  export const DrawerHeader = forwardRef<HTMLDivElement, DrawerHeaderProps>((props, ref) => {
312
- const { isCloseButtonRendered } = useDrawerContext();
313
- return (
314
- <Primitive.div ref={ref} data-show-close-button={dataAttr(isCloseButtonRendered)} {...props} />
315
- );
350
+ const api = useDrawerContext();
351
+ return <Primitive.div ref={ref} {...mergeProps(api.headerProps, props)} />;
316
352
  });
317
353
  DrawerHeader.displayName = "DrawerHeader";
318
354
 
319
- export interface DrawerCloseButtonProps extends DialogPrimitive.DialogCloseProps {}
355
+ export interface DrawerCloseButtonProps
356
+ extends PrimitiveProps,
357
+ React.ButtonHTMLAttributes<HTMLButtonElement> {}
320
358
 
321
359
  export const DrawerCloseButton = forwardRef<HTMLButtonElement, DrawerCloseButtonProps>(
322
360
  (props, ref) => {
323
- const { closeButtonRef, setIsOpen } = useDrawerContext();
324
- const composedRef = useComposedRefs(ref, closeButtonRef);
325
- return (
326
- <Primitive.button
327
- ref={composedRef}
328
- {...props}
329
- onClick={(e) => {
330
- props.onClick?.(e);
331
- if (e.defaultPrevented) return;
332
- setIsOpen(false, { reason: "closeButton", event: e.nativeEvent });
333
- }}
334
- />
335
- );
361
+ const api = useDrawerContext();
362
+ return <Primitive.button ref={ref} {...mergeProps(api.closeButtonProps, props)} />;
336
363
  },
337
364
  );
338
365
 
package/src/browser.ts CHANGED
@@ -14,10 +14,6 @@ export function isIPhone(): boolean | undefined {
14
14
  return testPlatform(/^iPhone/);
15
15
  }
16
16
 
17
- export function isSafari(): boolean | undefined {
18
- return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
19
- }
20
-
21
17
  export function isIPad(): boolean | undefined {
22
18
  return (
23
19
  testPlatform(/^iPad/) ||