@seed-design/react-drawer 1.0.11 → 2.0.1
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/{Drawer-12s-Bkn6enz-.cjs → Drawer-12s-B-Yg_SM_.cjs} +268 -351
- package/lib/{Drawer-12s-DxbsRb10.js → Drawer-12s-BDQbAi0U.js} +271 -335
- package/lib/index.cjs +1 -1
- package/lib/index.d.ts +4075 -56
- package/lib/index.js +2 -2
- package/package.json +9 -5
- package/src/Drawer.tsx +218 -191
- package/src/browser.ts +0 -4
- package/src/use-snap-points.ts +1 -1
- package/src/useDrawer.test.tsx +54 -40
- package/src/useDrawer.ts +77 -62
- package/src/use-position-fixed.ts +0 -149
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-
|
|
2
|
-
export { u as useDrawer, j as useDrawerContext } from './Drawer-12s-
|
|
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-BDQbAi0U.js';
|
|
2
|
+
export { u as useDrawer, j as useDrawerContext } from './Drawer-12s-BDQbAi0U.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": "
|
|
3
|
+
"version": "2.0.1",
|
|
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-
|
|
31
|
+
"@radix-ui/react-focus-scope": "^1.1.4",
|
|
32
32
|
"@radix-ui/react-use-callback-ref": "^1.1.0",
|
|
33
|
-
"@seed-design/
|
|
34
|
-
"@seed-design/
|
|
35
|
-
"@seed-design/react-
|
|
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 {
|
|
4
|
-
import
|
|
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 {
|
|
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
|
|
27
|
+
export interface DrawerTriggerProps
|
|
28
|
+
extends PrimitiveProps,
|
|
29
|
+
React.ButtonHTMLAttributes<HTMLButtonElement> {}
|
|
43
30
|
|
|
44
|
-
export const DrawerTrigger =
|
|
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 =
|
|
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
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
152
|
+
const dFactor = ["bottom", "right"].includes(dir) ? 1 : -1;
|
|
150
153
|
|
|
151
|
-
if (
|
|
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
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
|
325
|
+
export interface DrawerTitleProps
|
|
326
|
+
extends PrimitiveProps,
|
|
327
|
+
React.HTMLAttributes<HTMLHeadingElement> {}
|
|
302
328
|
|
|
303
|
-
export const DrawerTitle =
|
|
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
|
|
335
|
+
export interface DrawerDescriptionProps
|
|
336
|
+
extends PrimitiveProps,
|
|
337
|
+
React.HTMLAttributes<HTMLParagraphElement> {}
|
|
306
338
|
|
|
307
|
-
export const DrawerDescription =
|
|
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
|
|
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
|
|
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
|
|
324
|
-
|
|
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/) ||
|
package/src/use-snap-points.ts
CHANGED