@lodev09/react-native-true-sheet 3.11.0-beta.0 → 3.11.0-beta.10
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/LICENSE +1 -1
- package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +3 -3
- package/ios/TrueSheetFooterView.mm +10 -1
- package/ios/TrueSheetView.mm +1 -1
- package/ios/TrueSheetViewController.h +1 -1
- package/ios/TrueSheetViewController.mm +10 -4
- package/lib/module/TrueSheet.js +2 -2
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +226 -51
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/TrueSheetProvider.web.js +5 -2
- package/lib/module/TrueSheetProvider.web.js.map +1 -1
- package/lib/module/fabric/TrueSheetViewNativeComponent.ts +1 -1
- package/lib/module/web/constants.js +1 -1
- package/lib/module/web/constants.js.map +1 -1
- package/lib/module/web/vaul/index.js +102 -127
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/module/web/vaul/style.css +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +6 -4
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheetProvider.web.d.ts +2 -1
- package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +1 -1
- package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/navigation/types.d.ts +1 -1
- package/lib/typescript/src/navigation/types.d.ts.map +1 -1
- package/lib/typescript/src/web/constants.d.ts +1 -1
- package/lib/typescript/src/web/constants.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/index.d.ts +8 -2
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/use-prevent-scroll.d.ts +2 -2
- package/lib/typescript/src/web/vaul/use-prevent-scroll.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/TrueSheet.tsx +2 -2
- package/src/TrueSheet.types.ts +6 -4
- package/src/TrueSheet.web.tsx +225 -55
- package/src/TrueSheetProvider.web.tsx +11 -2
- package/src/fabric/TrueSheetViewNativeComponent.ts +1 -1
- package/src/navigation/types.ts +1 -1
- package/src/web/constants.ts +1 -1
- package/src/web/vaul/index.tsx +550 -552
- package/src/web/vaul/style.css +1 -1
package/src/web/vaul/index.tsx
CHANGED
|
@@ -1,28 +1,32 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
4
3
|
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
|
6
|
+
import { Presence } from '@radix-ui/react-presence';
|
|
7
|
+
|
|
5
8
|
import { DrawerContext, useDrawerContext } from './context';
|
|
6
9
|
import './style.css';
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
import { useSnapPoints } from './use-snap-points';
|
|
10
|
-
import { set, getTranslate, dampenValue, isVertical, reset } from './helpers';
|
|
10
|
+
|
|
11
|
+
import { isIOS, isMobileFirefox } from './browser';
|
|
11
12
|
import {
|
|
12
|
-
TRANSITIONS,
|
|
13
|
-
VELOCITY_THRESHOLD,
|
|
14
|
-
CLOSE_THRESHOLD,
|
|
15
|
-
SCROLL_LOCK_TIMEOUT,
|
|
16
13
|
BORDER_RADIUS,
|
|
14
|
+
CLOSE_THRESHOLD,
|
|
15
|
+
DRAG_CLASS,
|
|
17
16
|
NESTED_DISPLACEMENT,
|
|
17
|
+
SCROLL_LOCK_TIMEOUT,
|
|
18
|
+
TRANSITIONS,
|
|
19
|
+
VELOCITY_THRESHOLD,
|
|
18
20
|
WINDOW_TOP_OFFSET,
|
|
19
|
-
DRAG_CLASS,
|
|
20
21
|
} from './constants';
|
|
22
|
+
import { dampenValue, getTranslate, isVertical, reset, set } from './helpers';
|
|
21
23
|
import type { DrawerDirection } from './types';
|
|
24
|
+
import { useComposedRefs } from './use-composed-refs';
|
|
22
25
|
import { useControllableState } from './use-controllable-state';
|
|
23
|
-
import { useScaleBackground } from './use-scale-background';
|
|
24
26
|
import { usePositionFixed } from './use-position-fixed';
|
|
25
|
-
import {
|
|
27
|
+
import { isInput, usePreventScroll } from './use-prevent-scroll';
|
|
28
|
+
import { useScaleBackground } from './use-scale-background';
|
|
29
|
+
import { useSnapPoints } from './use-snap-points';
|
|
26
30
|
|
|
27
31
|
export interface WithFadeFromProps {
|
|
28
32
|
/**
|
|
@@ -182,6 +186,12 @@ export type DialogProps = {
|
|
|
182
186
|
* the sheet appears at its target detent instantly. Defaults to `true`.
|
|
183
187
|
*/
|
|
184
188
|
initialAnimated?: boolean;
|
|
189
|
+
/**
|
|
190
|
+
* Fires when the auto-size wrapper's measured content height changes.
|
|
191
|
+
* Useful for callers that want to size the surrounding card to fit
|
|
192
|
+
* content (e.g. iPad-style form sheets).
|
|
193
|
+
*/
|
|
194
|
+
onContentHeightChange?: (height: number) => void;
|
|
185
195
|
} & (WithFadeFromProps | WithoutFadeFromProps);
|
|
186
196
|
|
|
187
197
|
export function Root({
|
|
@@ -222,6 +232,7 @@ export function Root({
|
|
|
222
232
|
detachedWrapperStyle,
|
|
223
233
|
maxContentHeight,
|
|
224
234
|
initialAnimated = true,
|
|
235
|
+
onContentHeightChange,
|
|
225
236
|
}: DialogProps) {
|
|
226
237
|
const [isOpen = false, setIsOpen] = useControllableState({
|
|
227
238
|
defaultProp: defaultOpen,
|
|
@@ -236,19 +247,6 @@ export function Root({
|
|
|
236
247
|
setTimeout(() => {
|
|
237
248
|
onAnimationEnd?.(o);
|
|
238
249
|
}, TRANSITIONS.DURATION * 1000);
|
|
239
|
-
|
|
240
|
-
if (o && !modal) {
|
|
241
|
-
if (typeof window !== 'undefined') {
|
|
242
|
-
window.requestAnimationFrame(() => {
|
|
243
|
-
document.body.style.pointerEvents = 'auto';
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (!o) {
|
|
249
|
-
// This will be removed when the exit animation ends (`500ms`)
|
|
250
|
-
document.body.style.pointerEvents = 'auto';
|
|
251
|
-
}
|
|
252
250
|
},
|
|
253
251
|
});
|
|
254
252
|
const [hasBeenOpened, setHasBeenOpened] = React.useState<boolean>(false);
|
|
@@ -271,6 +269,14 @@ export function Root({
|
|
|
271
269
|
const initialDrawerHeight = React.useRef(0);
|
|
272
270
|
const [contentHeight, setContentHeight] = React.useState(0);
|
|
273
271
|
|
|
272
|
+
const onContentHeightChangeRef = React.useRef(onContentHeightChange);
|
|
273
|
+
React.useEffect(() => {
|
|
274
|
+
onContentHeightChangeRef.current = onContentHeightChange;
|
|
275
|
+
});
|
|
276
|
+
React.useEffect(() => {
|
|
277
|
+
onContentHeightChangeRef.current?.(contentHeight);
|
|
278
|
+
}, [contentHeight]);
|
|
279
|
+
|
|
274
280
|
const onSnapPointChange = React.useCallback((activeSnapPointIndex: number) => {
|
|
275
281
|
// Change openTime ref when we reach the last snap point to prevent dragging for 500ms incase it's scrollable.
|
|
276
282
|
if (snapPoints && activeSnapPointIndex === snapPointsOffset.length - 1)
|
|
@@ -563,63 +569,6 @@ export function Root({
|
|
|
563
569
|
onPositionChangeRef.current = onPositionChange;
|
|
564
570
|
});
|
|
565
571
|
|
|
566
|
-
// Radix's DismissableLayer (modal) sets body pointer-events to "none" so the
|
|
567
|
-
// overlay traps clicks. When the active snap is below fadeFromIndex the
|
|
568
|
-
// overlay is fully transparent, and we want clicks to reach the content
|
|
569
|
-
// behind the drawer — so we restore body interactivity. DismissableLayer
|
|
570
|
-
// captures its DOM node via a ref-callback `setNode`, so its body write
|
|
571
|
-
// runs one render later than ours on initial open — re-assert on rAF so
|
|
572
|
-
// the final write is ours.
|
|
573
|
-
React.useEffect(() => {
|
|
574
|
-
if (!modal || !snapPoints || fadeFromIndex === undefined) return;
|
|
575
|
-
// On close, restore body interactivity. `useControllableState.onChange`
|
|
576
|
-
// only fires for internal `setIsOpen` calls — when the parent flips the
|
|
577
|
-
// `open` prop imperatively, the reset at line ~201 never runs.
|
|
578
|
-
if (!isOpen) {
|
|
579
|
-
document.body.style.pointerEvents = 'auto';
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
if (typeof activeSnapPointIndex !== 'number') return;
|
|
583
|
-
const isBelowFade = activeSnapPointIndex < fadeFromIndex;
|
|
584
|
-
const value = isBelowFade ? 'auto' : 'none';
|
|
585
|
-
document.body.style.pointerEvents = value;
|
|
586
|
-
const id = window.requestAnimationFrame(() => {
|
|
587
|
-
document.body.style.pointerEvents = value;
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
// React Navigation keeps unfocused screens mounted with `display: none` on
|
|
591
|
-
// web, so the drawer never unmounts and neither `isOpen` nor this effect's
|
|
592
|
-
// deps change — a lingering `none` body style would then leak to whichever
|
|
593
|
-
// screen becomes visible. Observe the drawer: when an ancestor hides it,
|
|
594
|
-
// restore `auto`; when it returns, re-apply the desired value.
|
|
595
|
-
let observer: IntersectionObserver | null = null;
|
|
596
|
-
let rafId = 0;
|
|
597
|
-
const startObserving = () => {
|
|
598
|
-
const node = drawerRef.current;
|
|
599
|
-
if (!node) {
|
|
600
|
-
rafId = window.requestAnimationFrame(startObserving);
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
observer = new IntersectionObserver((entries) => {
|
|
604
|
-
const entry = entries[entries.length - 1];
|
|
605
|
-
if (!entry) return;
|
|
606
|
-
document.body.style.pointerEvents = entry.isIntersecting ? value : 'auto';
|
|
607
|
-
});
|
|
608
|
-
observer.observe(node);
|
|
609
|
-
};
|
|
610
|
-
rafId = window.requestAnimationFrame(startObserving);
|
|
611
|
-
|
|
612
|
-
// Always restore on cleanup so an unmount while the sheet is still open
|
|
613
|
-
// doesn't leave the page uninteractive. If the effect re-runs for a
|
|
614
|
-
// normal state change, it re-applies the correct value.
|
|
615
|
-
return () => {
|
|
616
|
-
window.cancelAnimationFrame(id);
|
|
617
|
-
window.cancelAnimationFrame(rafId);
|
|
618
|
-
observer?.disconnect();
|
|
619
|
-
document.body.style.pointerEvents = 'auto';
|
|
620
|
-
};
|
|
621
|
-
}, [isOpen, modal, snapPoints, fadeFromIndex, activeSnapPointIndex]);
|
|
622
|
-
|
|
623
572
|
React.useEffect(() => {
|
|
624
573
|
function onVisualViewportChange() {
|
|
625
574
|
if (!drawerRef.current || !repositionInputs) return;
|
|
@@ -669,7 +618,7 @@ export function Root({
|
|
|
669
618
|
}
|
|
670
619
|
|
|
671
620
|
if (snapPoints && snapPoints.length > 0 && !keyboardIsOpen.current) {
|
|
672
|
-
drawerRef.current.style.bottom =
|
|
621
|
+
drawerRef.current.style.bottom = '0px';
|
|
673
622
|
} else {
|
|
674
623
|
// Negative bottom value would never make sense
|
|
675
624
|
drawerRef.current.style.bottom = `${Math.max(diffFromInitial, 0)}px`;
|
|
@@ -888,15 +837,6 @@ export function Root({
|
|
|
888
837
|
}
|
|
889
838
|
}
|
|
890
839
|
|
|
891
|
-
React.useEffect(() => {
|
|
892
|
-
if (!modal) {
|
|
893
|
-
// Need to do this manually unfortunately
|
|
894
|
-
window.requestAnimationFrame(() => {
|
|
895
|
-
document.body.style.pointerEvents = 'auto';
|
|
896
|
-
});
|
|
897
|
-
}
|
|
898
|
-
}, [modal]);
|
|
899
|
-
|
|
900
840
|
return (
|
|
901
841
|
<DialogPrimitive.Root
|
|
902
842
|
defaultOpen={defaultOpen}
|
|
@@ -911,7 +851,14 @@ export function Root({
|
|
|
911
851
|
setIsOpen(open);
|
|
912
852
|
}}
|
|
913
853
|
open={isOpen}
|
|
914
|
-
modal
|
|
854
|
+
// Always non-modal at the Radix level — Radix's modal mode locks
|
|
855
|
+
// body.style.pointerEvents and its restore on layer unmount is buggy
|
|
856
|
+
// (see git history). Vaul's own overlay handles click-trapping; the
|
|
857
|
+
// overlay sets pointer-events: none below fadeFromIndex so clicks
|
|
858
|
+
// pass through to the page (matches native dimmedDetentIndex). Vaul's
|
|
859
|
+
// own `modal` prop still drives overlay rendering and click-outside
|
|
860
|
+
// behavior below.
|
|
861
|
+
modal={false}
|
|
915
862
|
>
|
|
916
863
|
<DrawerContext.Provider
|
|
917
864
|
value={{
|
|
@@ -962,7 +909,7 @@ export function Root({
|
|
|
962
909
|
export const Overlay = React.forwardRef<
|
|
963
910
|
HTMLDivElement,
|
|
964
911
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
965
|
-
>(
|
|
912
|
+
>(({ style, ...rest }, ref) => {
|
|
966
913
|
const {
|
|
967
914
|
overlayRef,
|
|
968
915
|
snapPoints,
|
|
@@ -1007,26 +954,38 @@ export const Overlay = React.forwardRef<
|
|
|
1007
954
|
typeof activeSnapPointIndex === 'number' &&
|
|
1008
955
|
activeSnapPointIndex < fadeFromIndex;
|
|
1009
956
|
|
|
957
|
+
// Render our own overlay element instead of `DialogPrimitive.Overlay`:
|
|
958
|
+
// Radix's Overlay returns null when the Dialog is non-modal, and we run
|
|
959
|
+
// Radix as `modal={false}` to keep it from locking body pointer-events.
|
|
960
|
+
// `Presence` keeps the node mounted through the closing animation
|
|
961
|
+
// (`fadeOut`) before unmounting.
|
|
1010
962
|
return (
|
|
1011
|
-
<
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
963
|
+
<Presence present={isOpen}>
|
|
964
|
+
<div
|
|
965
|
+
onMouseUp={onMouseUp}
|
|
966
|
+
ref={composedRef}
|
|
967
|
+
data-state={isOpen ? 'open' : 'closed'}
|
|
968
|
+
data-vaul-overlay=""
|
|
969
|
+
data-vaul-snap-points={isOpen && hasSnapPoints ? 'true' : 'false'}
|
|
970
|
+
data-vaul-delayed-snap-points={delayedSnapPoints ? 'true' : 'false'}
|
|
971
|
+
data-vaul-snap-points-overlay={isOpen && shouldFade ? 'true' : 'false'}
|
|
972
|
+
data-vaul-animate={shouldAnimate?.current ? 'true' : 'false'}
|
|
973
|
+
{...rest}
|
|
974
|
+
style={overlayStyle(style, !!isBelowFade)}
|
|
975
|
+
/>
|
|
976
|
+
</Presence>
|
|
1022
977
|
);
|
|
1023
978
|
});
|
|
1024
979
|
|
|
1025
980
|
Overlay.displayName = 'Drawer.Overlay';
|
|
1026
981
|
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
982
|
+
const overlayStyle = (
|
|
983
|
+
style: React.CSSProperties | undefined,
|
|
984
|
+
isBelowFade: boolean
|
|
985
|
+
): React.CSSProperties => ({
|
|
986
|
+
...style,
|
|
987
|
+
pointerEvents: isBelowFade ? 'none' : 'auto',
|
|
988
|
+
});
|
|
1030
989
|
|
|
1031
990
|
export type ContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
|
1032
991
|
/**
|
|
@@ -1037,387 +996,427 @@ export type ContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive
|
|
|
1037
996
|
detachedSiblings?: React.ReactNode;
|
|
1038
997
|
};
|
|
1039
998
|
|
|
1040
|
-
export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
snapPointsOffset,
|
|
1051
|
-
activeSnapPointIndex,
|
|
1052
|
-
fadeFromIndex,
|
|
1053
|
-
modal,
|
|
1054
|
-
isOpen,
|
|
1055
|
-
isDragging,
|
|
1056
|
-
direction,
|
|
1057
|
-
snapPoints,
|
|
1058
|
-
container,
|
|
1059
|
-
handleOnly,
|
|
1060
|
-
shouldAnimate,
|
|
1061
|
-
autoFocus,
|
|
1062
|
-
onPositionChangeRef,
|
|
1063
|
-
setContentHeight,
|
|
1064
|
-
detached,
|
|
1065
|
-
detachedOffset,
|
|
1066
|
-
detachedRadius,
|
|
1067
|
-
detachedWrapperStyle: detachedWrapperStyleProp,
|
|
1068
|
-
} = useDrawerContext();
|
|
1069
|
-
const hasAutoSnapPoint = React.useMemo(
|
|
1070
|
-
() => !!snapPoints?.some((p) => p === 'auto'),
|
|
1071
|
-
[snapPoints]
|
|
1072
|
-
);
|
|
1073
|
-
|
|
1074
|
-
// When 'auto' is used as a snap point, we need the natural content height.
|
|
1075
|
-
// The drawer itself may be styled to a fixed viewport height, so we measure
|
|
1076
|
-
// an inner wrapper instead. Ref callback starts the observer as soon as the
|
|
1077
|
-
// node mounts (Radix Presence defers the portal mount past useEffect).
|
|
1078
|
-
const autoRoRef = React.useRef<ResizeObserver | null>(null);
|
|
1079
|
-
const setAutoSizeNode = React.useCallback(
|
|
1080
|
-
(node: HTMLDivElement | null) => {
|
|
1081
|
-
autoRoRef.current?.disconnect();
|
|
1082
|
-
if (!node || !hasAutoSnapPoint) {
|
|
1083
|
-
autoRoRef.current = null;
|
|
1084
|
-
return;
|
|
1085
|
-
}
|
|
1086
|
-
const measure = () => setContentHeight(node.offsetHeight);
|
|
1087
|
-
measure();
|
|
1088
|
-
const ro = new ResizeObserver(measure);
|
|
1089
|
-
ro.observe(node);
|
|
1090
|
-
autoRoRef.current = ro;
|
|
999
|
+
export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
|
|
1000
|
+
(
|
|
1001
|
+
{
|
|
1002
|
+
onPointerDownOutside,
|
|
1003
|
+
onEscapeKeyDown,
|
|
1004
|
+
style,
|
|
1005
|
+
onOpenAutoFocus,
|
|
1006
|
+
children,
|
|
1007
|
+
detachedSiblings,
|
|
1008
|
+
...rest
|
|
1091
1009
|
},
|
|
1092
|
-
|
|
1093
|
-
);
|
|
1094
|
-
|
|
1095
|
-
const isBelowFade =
|
|
1096
|
-
snapPoints !== undefined &&
|
|
1097
|
-
snapPoints !== null &&
|
|
1098
|
-
fadeFromIndex !== undefined &&
|
|
1099
|
-
typeof activeSnapPointIndex === 'number' &&
|
|
1100
|
-
activeSnapPointIndex < fadeFromIndex;
|
|
1101
|
-
// Needed to use transition instead of animations
|
|
1102
|
-
const [delayedSnapPoints, setDelayedSnapPoints] = React.useState(false);
|
|
1103
|
-
const composedRef = useComposedRefs(ref, drawerRef);
|
|
1104
|
-
const pointerStartRef = React.useRef<{ x: number; y: number } | null>(null);
|
|
1105
|
-
const lastKnownPointerEventRef = React.useRef<React.PointerEvent<HTMLDivElement> | null>(null);
|
|
1106
|
-
const wasBeyondThePointRef = React.useRef(false);
|
|
1107
|
-
const hasSnapPoints = snapPoints && snapPoints.length > 0;
|
|
1108
|
-
useScaleBackground();
|
|
1109
|
-
|
|
1110
|
-
const isDeltaInDirection = (
|
|
1111
|
-
delta: { x: number; y: number },
|
|
1112
|
-
dir: DrawerDirection,
|
|
1113
|
-
threshold = 0
|
|
1010
|
+
ref
|
|
1114
1011
|
) => {
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
lastPosition: Number.NaN,
|
|
1165
|
-
start: () => {},
|
|
1166
|
-
stop: () => {},
|
|
1167
|
-
};
|
|
1012
|
+
const {
|
|
1013
|
+
drawerRef,
|
|
1014
|
+
onPress,
|
|
1015
|
+
onRelease,
|
|
1016
|
+
onDrag,
|
|
1017
|
+
keyboardIsOpen,
|
|
1018
|
+
snapPointsOffset,
|
|
1019
|
+
activeSnapPointIndex,
|
|
1020
|
+
fadeFromIndex,
|
|
1021
|
+
modal,
|
|
1022
|
+
isOpen,
|
|
1023
|
+
isDragging,
|
|
1024
|
+
direction,
|
|
1025
|
+
snapPoints,
|
|
1026
|
+
setActiveSnapPoint,
|
|
1027
|
+
dismissible,
|
|
1028
|
+
container,
|
|
1029
|
+
handleOnly,
|
|
1030
|
+
shouldAnimate,
|
|
1031
|
+
autoFocus,
|
|
1032
|
+
onPositionChangeRef,
|
|
1033
|
+
setContentHeight,
|
|
1034
|
+
detached,
|
|
1035
|
+
detachedOffset,
|
|
1036
|
+
detachedRadius,
|
|
1037
|
+
detachedWrapperStyle: detachedWrapperStyleProp,
|
|
1038
|
+
} = useDrawerContext();
|
|
1039
|
+
// Always measure the inner wrapper's natural height. The drawer itself may
|
|
1040
|
+
// be styled to a fixed viewport height, so we measure an inner wrapper
|
|
1041
|
+
// instead. Ref callback starts the observer as soon as the node mounts
|
|
1042
|
+
// (Radix Presence defers the portal mount past useEffect). The measurement
|
|
1043
|
+
// drives the 'auto' snap point and is also exposed via
|
|
1044
|
+
// `onContentHeightChange` for callers that size the surrounding card.
|
|
1045
|
+
const autoRoRef = React.useRef<ResizeObserver | null>(null);
|
|
1046
|
+
const setAutoSizeNode = React.useCallback(
|
|
1047
|
+
(node: HTMLDivElement | null) => {
|
|
1048
|
+
autoRoRef.current?.disconnect();
|
|
1049
|
+
if (!node) {
|
|
1050
|
+
autoRoRef.current = null;
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
const measure = () => setContentHeight(node.offsetHeight);
|
|
1054
|
+
measure();
|
|
1055
|
+
const ro = new ResizeObserver(measure);
|
|
1056
|
+
ro.observe(node);
|
|
1057
|
+
autoRoRef.current = ro;
|
|
1058
|
+
},
|
|
1059
|
+
[setContentHeight]
|
|
1060
|
+
);
|
|
1168
1061
|
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1062
|
+
const isBelowFade =
|
|
1063
|
+
snapPoints !== undefined &&
|
|
1064
|
+
snapPoints !== null &&
|
|
1065
|
+
fadeFromIndex !== undefined &&
|
|
1066
|
+
typeof activeSnapPointIndex === 'number' &&
|
|
1067
|
+
activeSnapPointIndex < fadeFromIndex;
|
|
1068
|
+
// Needed to use transition instead of animations
|
|
1069
|
+
const [delayedSnapPoints, setDelayedSnapPoints] = React.useState(false);
|
|
1070
|
+
const composedRef = useComposedRefs(ref, drawerRef);
|
|
1071
|
+
const pointerStartRef = React.useRef<{ x: number; y: number } | null>(null);
|
|
1072
|
+
const lastKnownPointerEventRef = React.useRef<React.PointerEvent<HTMLDivElement> | null>(null);
|
|
1073
|
+
const wasBeyondThePointRef = React.useRef(false);
|
|
1074
|
+
const hasSnapPoints = snapPoints && snapPoints.length > 0;
|
|
1075
|
+
useScaleBackground();
|
|
1076
|
+
|
|
1077
|
+
const isDeltaInDirection = (
|
|
1078
|
+
delta: { x: number; y: number },
|
|
1079
|
+
dir: DrawerDirection,
|
|
1080
|
+
threshold = 0
|
|
1081
|
+
) => {
|
|
1082
|
+
if (wasBeyondThePointRef.current) return true;
|
|
1083
|
+
|
|
1084
|
+
const deltaY = Math.abs(delta.y);
|
|
1085
|
+
const deltaX = Math.abs(delta.x);
|
|
1086
|
+
const isDeltaX = deltaX > deltaY;
|
|
1087
|
+
const dFactor = ['bottom', 'right'].includes(dir) ? 1 : -1;
|
|
1088
|
+
|
|
1089
|
+
if (dir === 'left' || dir === 'right') {
|
|
1090
|
+
const isReverseDirection = delta.x * dFactor < 0;
|
|
1091
|
+
if (!isReverseDirection && deltaX >= 0 && deltaX <= threshold) {
|
|
1092
|
+
return isDeltaX;
|
|
1093
|
+
}
|
|
1094
|
+
} else {
|
|
1095
|
+
const isReverseDirection = delta.y * dFactor < 0;
|
|
1096
|
+
if (!isReverseDirection && deltaY >= 0 && deltaY <= threshold) {
|
|
1097
|
+
return !isDeltaX;
|
|
1098
|
+
}
|
|
1176
1099
|
}
|
|
1177
|
-
};
|
|
1178
1100
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
state.rafId = state.movingCount > 0 ? window.requestAnimationFrame(tick) : null;
|
|
1101
|
+
wasBeyondThePointRef.current = true;
|
|
1102
|
+
return true;
|
|
1182
1103
|
};
|
|
1183
1104
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1105
|
+
React.useEffect(() => {
|
|
1106
|
+
if (hasSnapPoints) {
|
|
1107
|
+
window.requestAnimationFrame(() => {
|
|
1108
|
+
setDelayedSnapPoints(true);
|
|
1109
|
+
});
|
|
1188
1110
|
}
|
|
1189
|
-
};
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
if (
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
drawer.addEventListener('animationcancel', onAnimationDone);
|
|
1224
|
-
wrapper?.addEventListener('transitionrun', onTransitionRun);
|
|
1225
|
-
wrapper?.addEventListener('transitionend', onTransitionDone);
|
|
1226
|
-
wrapper?.addEventListener('transitioncancel', onTransitionDone);
|
|
1111
|
+
}, []);
|
|
1112
|
+
|
|
1113
|
+
// Event-driven position tracking. We only tick RAF while the drawer is
|
|
1114
|
+
// actually moving (drag / CSS transition / CSS animation). When it's idle at
|
|
1115
|
+
// a snap, no frames run at all.
|
|
1116
|
+
const positionTrackingRef = React.useRef<{
|
|
1117
|
+
rafId: number | null;
|
|
1118
|
+
movingCount: number;
|
|
1119
|
+
lastPosition: number;
|
|
1120
|
+
start: () => void;
|
|
1121
|
+
stop: () => void;
|
|
1122
|
+
} | null>(null);
|
|
1123
|
+
|
|
1124
|
+
React.useEffect(() => {
|
|
1125
|
+
const drawer = drawerRef.current;
|
|
1126
|
+
if (!drawer) return;
|
|
1127
|
+
|
|
1128
|
+
const state = {
|
|
1129
|
+
rafId: null as number | null,
|
|
1130
|
+
movingCount: 0,
|
|
1131
|
+
lastPosition: Number.NaN,
|
|
1132
|
+
start: () => {},
|
|
1133
|
+
stop: () => {},
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1136
|
+
const emit = () => {
|
|
1137
|
+
const cb = onPositionChangeRef.current;
|
|
1138
|
+
if (!cb) return;
|
|
1139
|
+
const position = drawer.getBoundingClientRect().top;
|
|
1140
|
+
if (position !== state.lastPosition) {
|
|
1141
|
+
state.lastPosition = position;
|
|
1142
|
+
cb(position);
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1227
1145
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1146
|
+
const tick = () => {
|
|
1147
|
+
emit();
|
|
1148
|
+
state.rafId = state.movingCount > 0 ? window.requestAnimationFrame(tick) : null;
|
|
1149
|
+
};
|
|
1230
1150
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1151
|
+
state.start = () => {
|
|
1152
|
+
state.movingCount += 1;
|
|
1153
|
+
if (state.rafId === null) {
|
|
1154
|
+
state.rafId = window.requestAnimationFrame(tick);
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
state.stop = () => {
|
|
1159
|
+
state.movingCount = Math.max(0, state.movingCount - 1);
|
|
1160
|
+
// RAF loop will exit on its next tick; emit the settled position now.
|
|
1161
|
+
if (state.movingCount === 0) emit();
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
const wrapper = drawer.closest<HTMLElement>('[data-vaul-detached-wrapper]');
|
|
1165
|
+
|
|
1166
|
+
// Listen on the wrapper too: drag-overshoot snap-back animates the wrapper
|
|
1167
|
+
// alone when the drawer's target is unchanged (e.g. snapping back to the
|
|
1168
|
+
// same detent). Without this, position goes stale mid-animation because
|
|
1169
|
+
// the drawer's `transitionrun` never fires.
|
|
1170
|
+
const onTransitionRun = (e: TransitionEvent) => {
|
|
1171
|
+
if ((e.target === drawer || e.target === wrapper) && e.propertyName === 'transform')
|
|
1172
|
+
state.start();
|
|
1173
|
+
};
|
|
1174
|
+
const onTransitionDone = (e: TransitionEvent) => {
|
|
1175
|
+
if ((e.target === drawer || e.target === wrapper) && e.propertyName === 'transform')
|
|
1176
|
+
state.stop();
|
|
1177
|
+
};
|
|
1178
|
+
const onAnimationStart = (e: AnimationEvent) => {
|
|
1179
|
+
if (e.target === drawer || e.target === wrapper) state.start();
|
|
1180
|
+
};
|
|
1181
|
+
const onAnimationDone = (e: AnimationEvent) => {
|
|
1182
|
+
if (e.target === drawer || e.target === wrapper) state.stop();
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
drawer.addEventListener('transitionrun', onTransitionRun);
|
|
1186
|
+
drawer.addEventListener('transitionend', onTransitionDone);
|
|
1187
|
+
drawer.addEventListener('transitioncancel', onTransitionDone);
|
|
1188
|
+
drawer.addEventListener('animationstart', onAnimationStart);
|
|
1189
|
+
drawer.addEventListener('animationend', onAnimationDone);
|
|
1190
|
+
drawer.addEventListener('animationcancel', onAnimationDone);
|
|
1191
|
+
wrapper?.addEventListener('transitionrun', onTransitionRun);
|
|
1192
|
+
wrapper?.addEventListener('transitionend', onTransitionDone);
|
|
1193
|
+
wrapper?.addEventListener('transitioncancel', onTransitionDone);
|
|
1194
|
+
|
|
1195
|
+
positionTrackingRef.current = state;
|
|
1196
|
+
emit();
|
|
1245
1197
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1198
|
+
return () => {
|
|
1199
|
+
drawer.removeEventListener('transitionrun', onTransitionRun);
|
|
1200
|
+
drawer.removeEventListener('transitionend', onTransitionDone);
|
|
1201
|
+
drawer.removeEventListener('transitioncancel', onTransitionDone);
|
|
1202
|
+
drawer.removeEventListener('animationstart', onAnimationStart);
|
|
1203
|
+
drawer.removeEventListener('animationend', onAnimationDone);
|
|
1204
|
+
drawer.removeEventListener('animationcancel', onAnimationDone);
|
|
1205
|
+
wrapper?.removeEventListener('transitionrun', onTransitionRun);
|
|
1206
|
+
wrapper?.removeEventListener('transitionend', onTransitionDone);
|
|
1207
|
+
wrapper?.removeEventListener('transitioncancel', onTransitionDone);
|
|
1208
|
+
if (state.rafId !== null) window.cancelAnimationFrame(state.rafId);
|
|
1209
|
+
positionTrackingRef.current = null;
|
|
1210
|
+
};
|
|
1211
|
+
}, []);
|
|
1212
|
+
|
|
1213
|
+
React.useEffect(() => {
|
|
1214
|
+
const state = positionTrackingRef.current;
|
|
1215
|
+
if (!state) return;
|
|
1216
|
+
if (isDragging) state.start();
|
|
1217
|
+
else state.stop();
|
|
1218
|
+
}, [isDragging]);
|
|
1219
|
+
|
|
1220
|
+
function handleOnPointerUp(event: React.PointerEvent<HTMLDivElement> | null) {
|
|
1221
|
+
pointerStartRef.current = null;
|
|
1222
|
+
wasBeyondThePointRef.current = false;
|
|
1223
|
+
onRelease(event);
|
|
1224
|
+
}
|
|
1258
1225
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1226
|
+
// The drawer always sits inside a fixed clip wrapper. `contain: paint`
|
|
1227
|
+
// establishes the wrapper as the containing block so the drawer's own
|
|
1228
|
+
// `position: fixed` is constrained here, and `overflow: hidden` keeps any
|
|
1229
|
+
// overshoot out of the viewport. When `detached` the wrapper also floats
|
|
1230
|
+
// with a bottom gap and rounded bottom corners; otherwise it sits flush.
|
|
1231
|
+
// Transform/transition are managed imperatively (via drag overshoot and the
|
|
1232
|
+
// dismiss effect) so React doesn't skip DOM writes for values it thinks it
|
|
1233
|
+
// already owns.
|
|
1234
|
+
const wrapperStyle = React.useMemo<React.CSSProperties>(
|
|
1235
|
+
() => ({
|
|
1236
|
+
position: 'fixed',
|
|
1237
|
+
top: 0,
|
|
1238
|
+
left: 0,
|
|
1239
|
+
right: 0,
|
|
1240
|
+
bottom: detached ? detachedOffset : 0,
|
|
1241
|
+
overflow: 'hidden',
|
|
1242
|
+
contain: 'paint',
|
|
1243
|
+
pointerEvents: 'none',
|
|
1244
|
+
borderBottomLeftRadius: detached ? detachedRadius : 0,
|
|
1245
|
+
borderBottomRightRadius: detached ? detachedRadius : 0,
|
|
1246
|
+
...detachedWrapperStyleProp,
|
|
1247
|
+
}),
|
|
1248
|
+
[detached, detachedOffset, detachedRadius, detachedWrapperStyleProp]
|
|
1249
|
+
);
|
|
1283
1250
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
1296
|
-
const viewportH = typeof window !== 'undefined' ? window.innerHeight : 0;
|
|
1297
|
-
if (!isOpen && wasOpenRef.current) {
|
|
1298
|
-
wrapper.style.transition = transition;
|
|
1299
|
-
wrapper.style.transform = `translate3d(0, ${viewportH}px, 0)`;
|
|
1300
|
-
} else if (isOpen && !wasOpenRef.current) {
|
|
1301
|
-
wrapper.style.transition = 'none';
|
|
1302
|
-
wrapper.style.transform = `translate3d(0, ${viewportH}px, 0)`;
|
|
1303
|
-
// eslint-disable-next-line no-void
|
|
1304
|
-
void wrapper.offsetHeight;
|
|
1305
|
-
wrapper.style.transition = transition;
|
|
1306
|
-
wrapper.style.transform = 'translate3d(0, 0, 0)';
|
|
1307
|
-
} else if (isOpen && !wrapper.style.transform) {
|
|
1308
|
-
// Fresh mount with open=true — ensure the wrapper starts at rest.
|
|
1309
|
-
wrapper.style.transition = transition;
|
|
1310
|
-
wrapper.style.transform = 'translate3d(0, 0, 0)';
|
|
1251
|
+
// Translate the wrapper off-screen on dismiss so the whole card slides out
|
|
1252
|
+
// as one. The reset-then-target pattern on open forces the browser to
|
|
1253
|
+
// record a starting value so the transition actually animates.
|
|
1254
|
+
// Start at `false` so a mount with `isOpen=true` (autopresent via
|
|
1255
|
+
// `initialDetentIndex`) is detected as a false→true transition and runs
|
|
1256
|
+
// the slide-up animation instead of snapping straight to rest.
|
|
1257
|
+
const wasOpenRef = React.useRef(false);
|
|
1258
|
+
React.useEffect(() => {
|
|
1259
|
+
if (!drawerRef.current) {
|
|
1260
|
+
wasOpenRef.current = isOpen;
|
|
1261
|
+
return;
|
|
1311
1262
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
style={
|
|
1328
|
-
snapPointsOffset && snapPointsOffset.length > 0
|
|
1329
|
-
? ({
|
|
1330
|
-
'--snap-point-height': `${snapPointsOffset[activeSnapPointIndex ?? 0]!}px`,
|
|
1331
|
-
...style,
|
|
1332
|
-
'pointerEvents': 'auto',
|
|
1333
|
-
} as React.CSSProperties)
|
|
1334
|
-
: ({ ...style, pointerEvents: 'auto' } as React.CSSProperties)
|
|
1263
|
+
const wrapper = drawerRef.current.closest<HTMLElement>('[data-vaul-detached-wrapper]');
|
|
1264
|
+
if (wrapper) {
|
|
1265
|
+
const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
1266
|
+
const viewportH = typeof window !== 'undefined' ? window.innerHeight : 0;
|
|
1267
|
+
if (!isOpen && wasOpenRef.current) {
|
|
1268
|
+
wrapper.style.transition = transition;
|
|
1269
|
+
wrapper.style.transform = `translate3d(0, ${viewportH}px, 0)`;
|
|
1270
|
+
} else if (isOpen && !wasOpenRef.current) {
|
|
1271
|
+
wrapper.style.transition = 'none';
|
|
1272
|
+
wrapper.style.transform = `translate3d(0, ${viewportH}px, 0)`;
|
|
1273
|
+
// eslint-disable-next-line no-void
|
|
1274
|
+
void wrapper.offsetHeight;
|
|
1275
|
+
wrapper.style.transition = transition;
|
|
1276
|
+
wrapper.style.transform = 'translate3d(0, 0, 0)';
|
|
1277
|
+
}
|
|
1335
1278
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1279
|
+
wasOpenRef.current = isOpen;
|
|
1280
|
+
}, [isOpen, detached, drawerRef]);
|
|
1281
|
+
|
|
1282
|
+
const contentNode = (
|
|
1283
|
+
<DialogPrimitive.Content
|
|
1284
|
+
data-vaul-drawer-direction={direction}
|
|
1285
|
+
data-vaul-drawer=""
|
|
1286
|
+
data-vaul-detached={detached ? 'true' : 'false'}
|
|
1287
|
+
data-vaul-delayed-snap-points={delayedSnapPoints ? 'true' : 'false'}
|
|
1288
|
+
data-vaul-snap-points={isOpen && hasSnapPoints ? 'true' : 'false'}
|
|
1289
|
+
data-vaul-custom-container={container ? 'true' : 'false'}
|
|
1290
|
+
data-vaul-animate={shouldAnimate?.current ? 'true' : 'false'}
|
|
1291
|
+
{...rest}
|
|
1292
|
+
ref={composedRef}
|
|
1293
|
+
style={
|
|
1294
|
+
snapPointsOffset && snapPointsOffset.length > 0
|
|
1295
|
+
? ({
|
|
1296
|
+
'--snap-point-height': `${snapPointsOffset[activeSnapPointIndex ?? 0]!}px`,
|
|
1297
|
+
...style,
|
|
1298
|
+
'pointerEvents': 'auto',
|
|
1299
|
+
} as React.CSSProperties)
|
|
1300
|
+
: ({ ...style, pointerEvents: 'auto' } as React.CSSProperties)
|
|
1347
1301
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1302
|
+
onPointerDown={(event) => {
|
|
1303
|
+
if (handleOnly) return;
|
|
1304
|
+
rest.onPointerDown?.(event);
|
|
1305
|
+
pointerStartRef.current = { x: event.pageX, y: event.pageY };
|
|
1306
|
+
onPress(event);
|
|
1307
|
+
}}
|
|
1308
|
+
onOpenAutoFocus={(e) => {
|
|
1309
|
+
onOpenAutoFocus?.(e);
|
|
1351
1310
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
}
|
|
1311
|
+
if (!autoFocus) {
|
|
1312
|
+
e.preventDefault();
|
|
1313
|
+
}
|
|
1314
|
+
}}
|
|
1315
|
+
onPointerDownOutside={(e) => {
|
|
1316
|
+
onPointerDownOutside?.(e);
|
|
1356
1317
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1318
|
+
if (!modal || e.defaultPrevented || isBelowFade) {
|
|
1319
|
+
e.preventDefault();
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// Non-dismissible + dimmed: collapse to the highest non-dimmed
|
|
1324
|
+
// snap point instead of silently swallowing the dismiss attempt.
|
|
1325
|
+
// Mirrors the native Android dim-tap behavior.
|
|
1326
|
+
if (
|
|
1327
|
+
!dismissible &&
|
|
1328
|
+
hasSnapPoints &&
|
|
1329
|
+
fadeFromIndex !== undefined &&
|
|
1330
|
+
fadeFromIndex > 0 &&
|
|
1331
|
+
typeof activeSnapPointIndex === 'number' &&
|
|
1332
|
+
activeSnapPointIndex >= fadeFromIndex
|
|
1333
|
+
) {
|
|
1334
|
+
e.preventDefault();
|
|
1335
|
+
setActiveSnapPoint(snapPoints![fadeFromIndex - 1]!);
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
if (keyboardIsOpen.current) {
|
|
1340
|
+
keyboardIsOpen.current = false;
|
|
1341
|
+
}
|
|
1342
|
+
}}
|
|
1343
|
+
onEscapeKeyDown={(e) => {
|
|
1344
|
+
onEscapeKeyDown?.(e);
|
|
1345
|
+
if (e.defaultPrevented) return;
|
|
1346
|
+
|
|
1347
|
+
// Non-dismissible: collapse to the first snap point (if above it)
|
|
1348
|
+
// instead of silently swallowing the dismiss attempt. Mirrors the
|
|
1349
|
+
// native Android back-press behavior.
|
|
1350
|
+
if (!dismissible) {
|
|
1351
|
+
e.preventDefault();
|
|
1352
|
+
if (
|
|
1353
|
+
hasSnapPoints &&
|
|
1354
|
+
typeof activeSnapPointIndex === 'number' &&
|
|
1355
|
+
activeSnapPointIndex > 0
|
|
1356
|
+
) {
|
|
1357
|
+
setActiveSnapPoint(snapPoints![0]!);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}}
|
|
1361
|
+
onFocusOutside={(e) => {
|
|
1362
|
+
// Never auto-dismiss the sheet on focus shifts. Without Radix's
|
|
1363
|
+
// FocusScope trap (we run modal={false}), this fires whenever
|
|
1364
|
+
// focus naturally leaves the drawer — e.g. React Navigation's
|
|
1365
|
+
// web driver hides the previous screen via display:none, which
|
|
1366
|
+
// moves focus off the trigger and dismisses the sheet.
|
|
1363
1367
|
e.preventDefault();
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1368
|
+
}}
|
|
1369
|
+
onPointerMove={(event) => {
|
|
1370
|
+
lastKnownPointerEventRef.current = event;
|
|
1371
|
+
if (handleOnly) return;
|
|
1372
|
+
rest.onPointerMove?.(event);
|
|
1373
|
+
if (!pointerStartRef.current) return;
|
|
1374
|
+
const yPosition = event.pageY - pointerStartRef.current.y;
|
|
1375
|
+
const xPosition = event.pageX - pointerStartRef.current.x;
|
|
1376
|
+
|
|
1377
|
+
const swipeStartThreshold = event.pointerType === 'touch' ? 10 : 2;
|
|
1378
|
+
const delta = { x: xPosition, y: yPosition };
|
|
1379
|
+
|
|
1380
|
+
const isAllowedToSwipe = isDeltaInDirection(delta, direction, swipeStartThreshold);
|
|
1381
|
+
if (isAllowedToSwipe) onDrag(event);
|
|
1382
|
+
else if (
|
|
1383
|
+
Math.abs(xPosition) > swipeStartThreshold ||
|
|
1384
|
+
Math.abs(yPosition) > swipeStartThreshold
|
|
1385
|
+
) {
|
|
1386
|
+
pointerStartRef.current = null;
|
|
1387
|
+
}
|
|
1388
|
+
}}
|
|
1389
|
+
onPointerUp={(event) => {
|
|
1390
|
+
rest.onPointerUp?.(event);
|
|
1384
1391
|
pointerStartRef.current = null;
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
wasBeyondThePointRef.current = false;
|
|
1391
|
-
onRelease(event);
|
|
1392
|
-
}}
|
|
1393
|
-
onPointerOut={(event) => {
|
|
1394
|
-
rest.onPointerOut?.(event);
|
|
1395
|
-
handleOnPointerUp(lastKnownPointerEventRef.current);
|
|
1396
|
-
}}
|
|
1397
|
-
onContextMenu={(event) => {
|
|
1398
|
-
rest.onContextMenu?.(event);
|
|
1399
|
-
if (lastKnownPointerEventRef.current) {
|
|
1392
|
+
wasBeyondThePointRef.current = false;
|
|
1393
|
+
onRelease(event);
|
|
1394
|
+
}}
|
|
1395
|
+
onPointerOut={(event) => {
|
|
1396
|
+
rest.onPointerOut?.(event);
|
|
1400
1397
|
handleOnPointerUp(lastKnownPointerEventRef.current);
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1398
|
+
}}
|
|
1399
|
+
onContextMenu={(event) => {
|
|
1400
|
+
rest.onContextMenu?.(event);
|
|
1401
|
+
if (lastKnownPointerEventRef.current) {
|
|
1402
|
+
handleOnPointerUp(lastKnownPointerEventRef.current);
|
|
1403
|
+
}
|
|
1404
|
+
}}
|
|
1405
|
+
>
|
|
1405
1406
|
<div ref={setAutoSizeNode} data-vaul-auto-size-wrapper="" style={autoSizeWrapperStyle}>
|
|
1406
1407
|
{children}
|
|
1407
1408
|
</div>
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
)}
|
|
1411
|
-
</DialogPrimitive.Content>
|
|
1412
|
-
);
|
|
1409
|
+
</DialogPrimitive.Content>
|
|
1410
|
+
);
|
|
1413
1411
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
}
|
|
1412
|
+
return (
|
|
1413
|
+
<div data-vaul-detached-wrapper="" style={wrapperStyle}>
|
|
1414
|
+
{contentNode}
|
|
1415
|
+
{detachedSiblings}
|
|
1416
|
+
</div>
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
);
|
|
1421
1420
|
|
|
1422
1421
|
Content.displayName = 'Drawer.Content';
|
|
1423
1422
|
|
|
@@ -1434,106 +1433,105 @@ export type HandleProps = React.ComponentPropsWithoutRef<'div'> & {
|
|
|
1434
1433
|
const LONG_HANDLE_PRESS_TIMEOUT = 250;
|
|
1435
1434
|
const DOUBLE_TAP_TIMEOUT = 120;
|
|
1436
1435
|
|
|
1437
|
-
export const Handle = React.forwardRef<HTMLDivElement, HandleProps>(
|
|
1438
|
-
{ preventCycle = false, children, ...rest },
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1436
|
+
export const Handle = React.forwardRef<HTMLDivElement, HandleProps>(
|
|
1437
|
+
({ preventCycle = false, children, ...rest }, ref) => {
|
|
1438
|
+
const {
|
|
1439
|
+
closeDrawer,
|
|
1440
|
+
isDragging,
|
|
1441
|
+
snapPoints,
|
|
1442
|
+
activeSnapPoint,
|
|
1443
|
+
setActiveSnapPoint,
|
|
1444
|
+
dismissible,
|
|
1445
|
+
handleOnly,
|
|
1446
|
+
isOpen,
|
|
1447
|
+
onPress,
|
|
1448
|
+
onDrag,
|
|
1449
|
+
} = useDrawerContext();
|
|
1450
|
+
|
|
1451
|
+
const closeTimeoutIdRef = React.useRef<number | null>(null);
|
|
1452
|
+
const shouldCancelInteractionRef = React.useRef(false);
|
|
1453
|
+
|
|
1454
|
+
function handleStartCycle() {
|
|
1455
|
+
// Stop if this is the second click of a double click
|
|
1456
|
+
if (shouldCancelInteractionRef.current) {
|
|
1457
|
+
handleCancelInteraction();
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
window.setTimeout(() => {
|
|
1461
|
+
handleCycleSnapPoints();
|
|
1462
|
+
}, DOUBLE_TAP_TIMEOUT);
|
|
1462
1463
|
}
|
|
1463
|
-
window.setTimeout(() => {
|
|
1464
|
-
handleCycleSnapPoints();
|
|
1465
|
-
}, DOUBLE_TAP_TIMEOUT);
|
|
1466
|
-
}
|
|
1467
1464
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1465
|
+
function handleCycleSnapPoints() {
|
|
1466
|
+
// Prevent accidental taps while resizing drawer
|
|
1467
|
+
if (isDragging || preventCycle || shouldCancelInteractionRef.current) {
|
|
1468
|
+
handleCancelInteraction();
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
// Make sure to clear the timeout id if the user releases the handle before the cancel timeout
|
|
1471
1472
|
handleCancelInteraction();
|
|
1472
|
-
return;
|
|
1473
|
-
}
|
|
1474
|
-
// Make sure to clear the timeout id if the user releases the handle before the cancel timeout
|
|
1475
|
-
handleCancelInteraction();
|
|
1476
1473
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1474
|
+
if (!snapPoints || snapPoints.length === 0) {
|
|
1475
|
+
if (!dismissible) {
|
|
1476
|
+
closeDrawer();
|
|
1477
|
+
}
|
|
1478
|
+
return;
|
|
1480
1479
|
}
|
|
1481
|
-
return;
|
|
1482
|
-
}
|
|
1483
1480
|
|
|
1484
|
-
|
|
1481
|
+
const isLastSnapPoint = activeSnapPoint === snapPoints[snapPoints.length - 1];
|
|
1485
1482
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1483
|
+
if (isLastSnapPoint && dismissible) {
|
|
1484
|
+
closeDrawer();
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1490
1487
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1488
|
+
const currentSnapIndex = snapPoints.findIndex((point) => point === activeSnapPoint);
|
|
1489
|
+
if (currentSnapIndex === -1) return; // activeSnapPoint not found in snapPoints
|
|
1490
|
+
const nextSnapPoint = snapPoints[currentSnapIndex + 1];
|
|
1491
|
+
if (nextSnapPoint === undefined) return;
|
|
1492
|
+
setActiveSnapPoint(nextSnapPoint);
|
|
1493
|
+
}
|
|
1497
1494
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1495
|
+
function handleStartInteraction() {
|
|
1496
|
+
closeTimeoutIdRef.current = window.setTimeout(() => {
|
|
1497
|
+
// Cancel click interaction on a long press
|
|
1498
|
+
shouldCancelInteractionRef.current = true;
|
|
1499
|
+
}, LONG_HANDLE_PRESS_TIMEOUT);
|
|
1500
|
+
}
|
|
1504
1501
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1502
|
+
function handleCancelInteraction() {
|
|
1503
|
+
if (closeTimeoutIdRef.current) {
|
|
1504
|
+
window.clearTimeout(closeTimeoutIdRef.current);
|
|
1505
|
+
}
|
|
1506
|
+
shouldCancelInteractionRef.current = false;
|
|
1508
1507
|
}
|
|
1509
|
-
shouldCancelInteractionRef.current = false;
|
|
1510
|
-
}
|
|
1511
1508
|
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
}
|
|
1509
|
+
return (
|
|
1510
|
+
<div
|
|
1511
|
+
onClick={handleStartCycle}
|
|
1512
|
+
onPointerCancel={handleCancelInteraction}
|
|
1513
|
+
onPointerDown={(e) => {
|
|
1514
|
+
if (handleOnly) onPress(e);
|
|
1515
|
+
handleStartInteraction();
|
|
1516
|
+
}}
|
|
1517
|
+
onPointerMove={(e) => {
|
|
1518
|
+
if (handleOnly) onDrag(e);
|
|
1519
|
+
}}
|
|
1520
|
+
// onPointerUp is already handled by the content component
|
|
1521
|
+
ref={ref}
|
|
1522
|
+
data-vaul-drawer-visible={isOpen ? 'true' : 'false'}
|
|
1523
|
+
data-vaul-handle=""
|
|
1524
|
+
aria-hidden="true"
|
|
1525
|
+
{...rest}
|
|
1526
|
+
>
|
|
1527
|
+
{/* Expand handle's hit area beyond what's visible to ensure a 44x44 tap target for touch devices */}
|
|
1528
|
+
<span data-vaul-handle-hitarea="" aria-hidden="true">
|
|
1529
|
+
{children}
|
|
1530
|
+
</span>
|
|
1531
|
+
</div>
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
);
|
|
1537
1535
|
|
|
1538
1536
|
Handle.displayName = 'Drawer.Handle';
|
|
1539
1537
|
|