@lodev09/react-native-true-sheet 3.11.0-beta.0 → 3.11.0-beta.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/module/TrueSheet.web.js +8 -5
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/web/vaul/index.js +34 -14
- package/lib/module/web/vaul/index.js.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/web/vaul/index.d.ts +1 -1
- 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 +1 -1
- package/src/TrueSheet.web.tsx +8 -6
- package/src/web/vaul/index.tsx +489 -463
package/src/web/vaul/index.tsx
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
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
|
+
|
|
5
7
|
import { DrawerContext, useDrawerContext } from './context';
|
|
6
8
|
import './style.css';
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
import { useSnapPoints } from './use-snap-points';
|
|
10
|
-
import { set, getTranslate, dampenValue, isVertical, reset } from './helpers';
|
|
9
|
+
|
|
10
|
+
import { isIOS, isMobileFirefox } from './browser';
|
|
11
11
|
import {
|
|
12
|
-
TRANSITIONS,
|
|
13
|
-
VELOCITY_THRESHOLD,
|
|
14
|
-
CLOSE_THRESHOLD,
|
|
15
|
-
SCROLL_LOCK_TIMEOUT,
|
|
16
12
|
BORDER_RADIUS,
|
|
13
|
+
CLOSE_THRESHOLD,
|
|
14
|
+
DRAG_CLASS,
|
|
17
15
|
NESTED_DISPLACEMENT,
|
|
16
|
+
SCROLL_LOCK_TIMEOUT,
|
|
17
|
+
TRANSITIONS,
|
|
18
|
+
VELOCITY_THRESHOLD,
|
|
18
19
|
WINDOW_TOP_OFFSET,
|
|
19
|
-
DRAG_CLASS,
|
|
20
20
|
} from './constants';
|
|
21
|
+
import { dampenValue, getTranslate, isVertical, reset, set } from './helpers';
|
|
21
22
|
import type { DrawerDirection } from './types';
|
|
23
|
+
import { useComposedRefs } from './use-composed-refs';
|
|
22
24
|
import { useControllableState } from './use-controllable-state';
|
|
23
|
-
import { useScaleBackground } from './use-scale-background';
|
|
24
25
|
import { usePositionFixed } from './use-position-fixed';
|
|
25
|
-
import {
|
|
26
|
+
import { isInput, usePreventScroll } from './use-prevent-scroll';
|
|
27
|
+
import { useScaleBackground } from './use-scale-background';
|
|
28
|
+
import { useSnapPoints } from './use-snap-points';
|
|
26
29
|
|
|
27
30
|
export interface WithFadeFromProps {
|
|
28
31
|
/**
|
|
@@ -620,6 +623,31 @@ export function Root({
|
|
|
620
623
|
};
|
|
621
624
|
}, [isOpen, modal, snapPoints, fadeFromIndex, activeSnapPointIndex]);
|
|
622
625
|
|
|
626
|
+
// Radix DismissableLayer's body-restore on unmount is broken: its two
|
|
627
|
+
// useEffects (see @radix-ui/react-dismissable-layer dist/index.mjs:68-92)
|
|
628
|
+
// run cleanups in reverse declaration order, so the layers-set decrement
|
|
629
|
+
// (effect B) runs BEFORE the size-1 body-restore check (effect A). When
|
|
630
|
+
// the last layer unmounts, A sees size=0 and skips the restore — body
|
|
631
|
+
// stays 'none'. Take ownership of the restore here on Drawer.Root unmount.
|
|
632
|
+
// Skipped when nested or non-modal so an inner drawer's unmount doesn't
|
|
633
|
+
// unlock an outer drawer's modal trap. Also bail if any other Radix
|
|
634
|
+
// dismissable layer is still open (sibling Dialog/AlertDialog/Popover) —
|
|
635
|
+
// that one wants the lock kept.
|
|
636
|
+
React.useEffect(() => {
|
|
637
|
+
if (nested || !modal) return;
|
|
638
|
+
return () => {
|
|
639
|
+
if (typeof document === 'undefined') return;
|
|
640
|
+
if (document.body.style.pointerEvents !== 'none') return;
|
|
641
|
+
if (
|
|
642
|
+
document.querySelector(
|
|
643
|
+
'[role="dialog"][data-state="open"], [role="alertdialog"][data-state="open"]'
|
|
644
|
+
)
|
|
645
|
+
)
|
|
646
|
+
return;
|
|
647
|
+
document.body.style.pointerEvents = 'auto';
|
|
648
|
+
};
|
|
649
|
+
}, [nested, modal]);
|
|
650
|
+
|
|
623
651
|
React.useEffect(() => {
|
|
624
652
|
function onVisualViewportChange() {
|
|
625
653
|
if (!drawerRef.current || !repositionInputs) return;
|
|
@@ -669,7 +697,7 @@ export function Root({
|
|
|
669
697
|
}
|
|
670
698
|
|
|
671
699
|
if (snapPoints && snapPoints.length > 0 && !keyboardIsOpen.current) {
|
|
672
|
-
drawerRef.current.style.bottom =
|
|
700
|
+
drawerRef.current.style.bottom = '0px';
|
|
673
701
|
} else {
|
|
674
702
|
// Negative bottom value would never make sense
|
|
675
703
|
drawerRef.current.style.bottom = `${Math.max(diffFromInitial, 0)}px`;
|
|
@@ -962,7 +990,7 @@ export function Root({
|
|
|
962
990
|
export const Overlay = React.forwardRef<
|
|
963
991
|
HTMLDivElement,
|
|
964
992
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
965
|
-
>(
|
|
993
|
+
>(({ style, ...rest }, ref) => {
|
|
966
994
|
const {
|
|
967
995
|
overlayRef,
|
|
968
996
|
snapPoints,
|
|
@@ -1037,387 +1065,386 @@ export type ContentProps = React.ComponentPropsWithoutRef<typeof DialogPrimitive
|
|
|
1037
1065
|
detachedSiblings?: React.ReactNode;
|
|
1038
1066
|
};
|
|
1039
1067
|
|
|
1040
|
-
export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
|
|
1041
|
-
{ onPointerDownOutside, style, onOpenAutoFocus, children, detachedSiblings, ...rest },
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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;
|
|
1091
|
-
},
|
|
1092
|
-
[hasAutoSnapPoint, setContentHeight]
|
|
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
|
|
1114
|
-
) => {
|
|
1115
|
-
if (wasBeyondThePointRef.current) return true;
|
|
1116
|
-
|
|
1117
|
-
const deltaY = Math.abs(delta.y);
|
|
1118
|
-
const deltaX = Math.abs(delta.x);
|
|
1119
|
-
const isDeltaX = deltaX > deltaY;
|
|
1120
|
-
const dFactor = ['bottom', 'right'].includes(dir) ? 1 : -1;
|
|
1121
|
-
|
|
1122
|
-
if (dir === 'left' || dir === 'right') {
|
|
1123
|
-
const isReverseDirection = delta.x * dFactor < 0;
|
|
1124
|
-
if (!isReverseDirection && deltaX >= 0 && deltaX <= threshold) {
|
|
1125
|
-
return isDeltaX;
|
|
1126
|
-
}
|
|
1127
|
-
} else {
|
|
1128
|
-
const isReverseDirection = delta.y * dFactor < 0;
|
|
1129
|
-
if (!isReverseDirection && deltaY >= 0 && deltaY <= threshold) {
|
|
1130
|
-
return !isDeltaX;
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
wasBeyondThePointRef.current = true;
|
|
1135
|
-
return true;
|
|
1136
|
-
};
|
|
1137
|
-
|
|
1138
|
-
React.useEffect(() => {
|
|
1139
|
-
if (hasSnapPoints) {
|
|
1140
|
-
window.requestAnimationFrame(() => {
|
|
1141
|
-
setDelayedSnapPoints(true);
|
|
1142
|
-
});
|
|
1143
|
-
}
|
|
1144
|
-
}, []);
|
|
1145
|
-
|
|
1146
|
-
// Event-driven position tracking. We only tick RAF while the drawer is
|
|
1147
|
-
// actually moving (drag / CSS transition / CSS animation). When it's idle at
|
|
1148
|
-
// a snap, no frames run at all.
|
|
1149
|
-
const positionTrackingRef = React.useRef<{
|
|
1150
|
-
rafId: number | null;
|
|
1151
|
-
movingCount: number;
|
|
1152
|
-
lastPosition: number;
|
|
1153
|
-
start: () => void;
|
|
1154
|
-
stop: () => void;
|
|
1155
|
-
} | null>(null);
|
|
1068
|
+
export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
|
|
1069
|
+
({ onPointerDownOutside, style, onOpenAutoFocus, children, detachedSiblings, ...rest }, ref) => {
|
|
1070
|
+
const {
|
|
1071
|
+
drawerRef,
|
|
1072
|
+
onPress,
|
|
1073
|
+
onRelease,
|
|
1074
|
+
onDrag,
|
|
1075
|
+
keyboardIsOpen,
|
|
1076
|
+
snapPointsOffset,
|
|
1077
|
+
activeSnapPointIndex,
|
|
1078
|
+
fadeFromIndex,
|
|
1079
|
+
modal,
|
|
1080
|
+
isOpen,
|
|
1081
|
+
isDragging,
|
|
1082
|
+
direction,
|
|
1083
|
+
snapPoints,
|
|
1084
|
+
container,
|
|
1085
|
+
handleOnly,
|
|
1086
|
+
shouldAnimate,
|
|
1087
|
+
autoFocus,
|
|
1088
|
+
onPositionChangeRef,
|
|
1089
|
+
setContentHeight,
|
|
1090
|
+
detached,
|
|
1091
|
+
detachedOffset,
|
|
1092
|
+
detachedRadius,
|
|
1093
|
+
detachedWrapperStyle: detachedWrapperStyleProp,
|
|
1094
|
+
} = useDrawerContext();
|
|
1095
|
+
const hasAutoSnapPoint = React.useMemo(
|
|
1096
|
+
() => !!snapPoints?.some((p) => p === 'auto'),
|
|
1097
|
+
[snapPoints]
|
|
1098
|
+
);
|
|
1156
1099
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1100
|
+
// When 'auto' is used as a snap point, we need the natural content height.
|
|
1101
|
+
// The drawer itself may be styled to a fixed viewport height, so we measure
|
|
1102
|
+
// an inner wrapper instead. Ref callback starts the observer as soon as the
|
|
1103
|
+
// node mounts (Radix Presence defers the portal mount past useEffect).
|
|
1104
|
+
const autoRoRef = React.useRef<ResizeObserver | null>(null);
|
|
1105
|
+
const setAutoSizeNode = React.useCallback(
|
|
1106
|
+
(node: HTMLDivElement | null) => {
|
|
1107
|
+
autoRoRef.current?.disconnect();
|
|
1108
|
+
if (!node || !hasAutoSnapPoint) {
|
|
1109
|
+
autoRoRef.current = null;
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const measure = () => setContentHeight(node.offsetHeight);
|
|
1113
|
+
measure();
|
|
1114
|
+
const ro = new ResizeObserver(measure);
|
|
1115
|
+
ro.observe(node);
|
|
1116
|
+
autoRoRef.current = ro;
|
|
1117
|
+
},
|
|
1118
|
+
[hasAutoSnapPoint, setContentHeight]
|
|
1119
|
+
);
|
|
1168
1120
|
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1121
|
+
const isBelowFade =
|
|
1122
|
+
snapPoints !== undefined &&
|
|
1123
|
+
snapPoints !== null &&
|
|
1124
|
+
fadeFromIndex !== undefined &&
|
|
1125
|
+
typeof activeSnapPointIndex === 'number' &&
|
|
1126
|
+
activeSnapPointIndex < fadeFromIndex;
|
|
1127
|
+
// Needed to use transition instead of animations
|
|
1128
|
+
const [delayedSnapPoints, setDelayedSnapPoints] = React.useState(false);
|
|
1129
|
+
const composedRef = useComposedRefs(ref, drawerRef);
|
|
1130
|
+
const pointerStartRef = React.useRef<{ x: number; y: number } | null>(null);
|
|
1131
|
+
const lastKnownPointerEventRef = React.useRef<React.PointerEvent<HTMLDivElement> | null>(null);
|
|
1132
|
+
const wasBeyondThePointRef = React.useRef(false);
|
|
1133
|
+
const hasSnapPoints = snapPoints && snapPoints.length > 0;
|
|
1134
|
+
useScaleBackground();
|
|
1135
|
+
|
|
1136
|
+
const isDeltaInDirection = (
|
|
1137
|
+
delta: { x: number; y: number },
|
|
1138
|
+
dir: DrawerDirection,
|
|
1139
|
+
threshold = 0
|
|
1140
|
+
) => {
|
|
1141
|
+
if (wasBeyondThePointRef.current) return true;
|
|
1142
|
+
|
|
1143
|
+
const deltaY = Math.abs(delta.y);
|
|
1144
|
+
const deltaX = Math.abs(delta.x);
|
|
1145
|
+
const isDeltaX = deltaX > deltaY;
|
|
1146
|
+
const dFactor = ['bottom', 'right'].includes(dir) ? 1 : -1;
|
|
1147
|
+
|
|
1148
|
+
if (dir === 'left' || dir === 'right') {
|
|
1149
|
+
const isReverseDirection = delta.x * dFactor < 0;
|
|
1150
|
+
if (!isReverseDirection && deltaX >= 0 && deltaX <= threshold) {
|
|
1151
|
+
return isDeltaX;
|
|
1152
|
+
}
|
|
1153
|
+
} else {
|
|
1154
|
+
const isReverseDirection = delta.y * dFactor < 0;
|
|
1155
|
+
if (!isReverseDirection && deltaY >= 0 && deltaY <= threshold) {
|
|
1156
|
+
return !isDeltaX;
|
|
1157
|
+
}
|
|
1176
1158
|
}
|
|
1177
|
-
};
|
|
1178
1159
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
state.rafId = state.movingCount > 0 ? window.requestAnimationFrame(tick) : null;
|
|
1160
|
+
wasBeyondThePointRef.current = true;
|
|
1161
|
+
return true;
|
|
1182
1162
|
};
|
|
1183
1163
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1164
|
+
React.useEffect(() => {
|
|
1165
|
+
if (hasSnapPoints) {
|
|
1166
|
+
window.requestAnimationFrame(() => {
|
|
1167
|
+
setDelayedSnapPoints(true);
|
|
1168
|
+
});
|
|
1188
1169
|
}
|
|
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);
|
|
1170
|
+
}, []);
|
|
1171
|
+
|
|
1172
|
+
// Event-driven position tracking. We only tick RAF while the drawer is
|
|
1173
|
+
// actually moving (drag / CSS transition / CSS animation). When it's idle at
|
|
1174
|
+
// a snap, no frames run at all.
|
|
1175
|
+
const positionTrackingRef = React.useRef<{
|
|
1176
|
+
rafId: number | null;
|
|
1177
|
+
movingCount: number;
|
|
1178
|
+
lastPosition: number;
|
|
1179
|
+
start: () => void;
|
|
1180
|
+
stop: () => void;
|
|
1181
|
+
} | null>(null);
|
|
1182
|
+
|
|
1183
|
+
React.useEffect(() => {
|
|
1184
|
+
const drawer = drawerRef.current;
|
|
1185
|
+
if (!drawer) return;
|
|
1186
|
+
|
|
1187
|
+
const state = {
|
|
1188
|
+
rafId: null as number | null,
|
|
1189
|
+
movingCount: 0,
|
|
1190
|
+
lastPosition: Number.NaN,
|
|
1191
|
+
start: () => {},
|
|
1192
|
+
stop: () => {},
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
const emit = () => {
|
|
1196
|
+
const cb = onPositionChangeRef.current;
|
|
1197
|
+
if (!cb) return;
|
|
1198
|
+
const position = drawer.getBoundingClientRect().top;
|
|
1199
|
+
if (position !== state.lastPosition) {
|
|
1200
|
+
state.lastPosition = position;
|
|
1201
|
+
cb(position);
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1227
1204
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1205
|
+
const tick = () => {
|
|
1206
|
+
emit();
|
|
1207
|
+
state.rafId = state.movingCount > 0 ? window.requestAnimationFrame(tick) : null;
|
|
1208
|
+
};
|
|
1230
1209
|
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1210
|
+
state.start = () => {
|
|
1211
|
+
state.movingCount += 1;
|
|
1212
|
+
if (state.rafId === null) {
|
|
1213
|
+
state.rafId = window.requestAnimationFrame(tick);
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
state.stop = () => {
|
|
1218
|
+
state.movingCount = Math.max(0, state.movingCount - 1);
|
|
1219
|
+
// RAF loop will exit on its next tick; emit the settled position now.
|
|
1220
|
+
if (state.movingCount === 0) emit();
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
const wrapper = drawer.closest<HTMLElement>('[data-vaul-detached-wrapper]');
|
|
1224
|
+
|
|
1225
|
+
// Listen on the wrapper too: drag-overshoot snap-back animates the wrapper
|
|
1226
|
+
// alone when the drawer's target is unchanged (e.g. snapping back to the
|
|
1227
|
+
// same detent). Without this, position goes stale mid-animation because
|
|
1228
|
+
// the drawer's `transitionrun` never fires.
|
|
1229
|
+
const onTransitionRun = (e: TransitionEvent) => {
|
|
1230
|
+
if ((e.target === drawer || e.target === wrapper) && e.propertyName === 'transform')
|
|
1231
|
+
state.start();
|
|
1232
|
+
};
|
|
1233
|
+
const onTransitionDone = (e: TransitionEvent) => {
|
|
1234
|
+
if ((e.target === drawer || e.target === wrapper) && e.propertyName === 'transform')
|
|
1235
|
+
state.stop();
|
|
1236
|
+
};
|
|
1237
|
+
const onAnimationStart = (e: AnimationEvent) => {
|
|
1238
|
+
if (e.target === drawer || e.target === wrapper) state.start();
|
|
1239
|
+
};
|
|
1240
|
+
const onAnimationDone = (e: AnimationEvent) => {
|
|
1241
|
+
if (e.target === drawer || e.target === wrapper) state.stop();
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
drawer.addEventListener('transitionrun', onTransitionRun);
|
|
1245
|
+
drawer.addEventListener('transitionend', onTransitionDone);
|
|
1246
|
+
drawer.addEventListener('transitioncancel', onTransitionDone);
|
|
1247
|
+
drawer.addEventListener('animationstart', onAnimationStart);
|
|
1248
|
+
drawer.addEventListener('animationend', onAnimationDone);
|
|
1249
|
+
drawer.addEventListener('animationcancel', onAnimationDone);
|
|
1250
|
+
wrapper?.addEventListener('transitionrun', onTransitionRun);
|
|
1251
|
+
wrapper?.addEventListener('transitionend', onTransitionDone);
|
|
1252
|
+
wrapper?.addEventListener('transitioncancel', onTransitionDone);
|
|
1253
|
+
|
|
1254
|
+
positionTrackingRef.current = state;
|
|
1255
|
+
emit();
|
|
1245
1256
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1257
|
+
return () => {
|
|
1258
|
+
drawer.removeEventListener('transitionrun', onTransitionRun);
|
|
1259
|
+
drawer.removeEventListener('transitionend', onTransitionDone);
|
|
1260
|
+
drawer.removeEventListener('transitioncancel', onTransitionDone);
|
|
1261
|
+
drawer.removeEventListener('animationstart', onAnimationStart);
|
|
1262
|
+
drawer.removeEventListener('animationend', onAnimationDone);
|
|
1263
|
+
drawer.removeEventListener('animationcancel', onAnimationDone);
|
|
1264
|
+
wrapper?.removeEventListener('transitionrun', onTransitionRun);
|
|
1265
|
+
wrapper?.removeEventListener('transitionend', onTransitionDone);
|
|
1266
|
+
wrapper?.removeEventListener('transitioncancel', onTransitionDone);
|
|
1267
|
+
if (state.rafId !== null) window.cancelAnimationFrame(state.rafId);
|
|
1268
|
+
positionTrackingRef.current = null;
|
|
1269
|
+
};
|
|
1270
|
+
}, []);
|
|
1271
|
+
|
|
1272
|
+
React.useEffect(() => {
|
|
1273
|
+
const state = positionTrackingRef.current;
|
|
1274
|
+
if (!state) return;
|
|
1275
|
+
if (isDragging) state.start();
|
|
1276
|
+
else state.stop();
|
|
1277
|
+
}, [isDragging]);
|
|
1278
|
+
|
|
1279
|
+
function handleOnPointerUp(event: React.PointerEvent<HTMLDivElement> | null) {
|
|
1280
|
+
pointerStartRef.current = null;
|
|
1281
|
+
wasBeyondThePointRef.current = false;
|
|
1282
|
+
onRelease(event);
|
|
1283
|
+
}
|
|
1258
1284
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1285
|
+
// The drawer always sits inside a fixed clip wrapper. `contain: paint`
|
|
1286
|
+
// establishes the wrapper as the containing block so the drawer's own
|
|
1287
|
+
// `position: fixed` is constrained here, and `overflow: hidden` keeps any
|
|
1288
|
+
// overshoot out of the viewport. When `detached` the wrapper also floats
|
|
1289
|
+
// with a bottom gap and rounded bottom corners; otherwise it sits flush.
|
|
1290
|
+
// Transform/transition are managed imperatively (via drag overshoot and the
|
|
1291
|
+
// dismiss effect) so React doesn't skip DOM writes for values it thinks it
|
|
1292
|
+
// already owns.
|
|
1293
|
+
const wrapperStyle = React.useMemo<React.CSSProperties>(
|
|
1294
|
+
() => ({
|
|
1295
|
+
position: 'fixed',
|
|
1296
|
+
top: 0,
|
|
1297
|
+
left: 0,
|
|
1298
|
+
right: 0,
|
|
1299
|
+
bottom: detached ? detachedOffset : 0,
|
|
1300
|
+
overflow: 'hidden',
|
|
1301
|
+
contain: 'paint',
|
|
1302
|
+
pointerEvents: 'none',
|
|
1303
|
+
borderBottomLeftRadius: detached ? detachedRadius : 0,
|
|
1304
|
+
borderBottomRightRadius: detached ? detachedRadius : 0,
|
|
1305
|
+
...detachedWrapperStyleProp,
|
|
1306
|
+
}),
|
|
1307
|
+
[detached, detachedOffset, detachedRadius, detachedWrapperStyleProp]
|
|
1308
|
+
);
|
|
1283
1309
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
}
|
|
1293
|
-
const wrapper = drawerRef.current.closest<HTMLElement>('[data-vaul-detached-wrapper]');
|
|
1294
|
-
if (wrapper) {
|
|
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)';
|
|
1310
|
+
// Translate the wrapper off-screen on dismiss so the whole card slides out
|
|
1311
|
+
// as one. The reset-then-target pattern on open forces the browser to
|
|
1312
|
+
// record a starting value so the transition actually animates.
|
|
1313
|
+
const wasOpenRef = React.useRef(isOpen);
|
|
1314
|
+
React.useEffect(() => {
|
|
1315
|
+
if (!drawerRef.current) {
|
|
1316
|
+
wasOpenRef.current = isOpen;
|
|
1317
|
+
return;
|
|
1311
1318
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
...style,
|
|
1332
|
-
'pointerEvents': 'auto',
|
|
1333
|
-
} as React.CSSProperties)
|
|
1334
|
-
: ({ ...style, pointerEvents: 'auto' } as React.CSSProperties)
|
|
1319
|
+
const wrapper = drawerRef.current.closest<HTMLElement>('[data-vaul-detached-wrapper]');
|
|
1320
|
+
if (wrapper) {
|
|
1321
|
+
const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
1322
|
+
const viewportH = typeof window !== 'undefined' ? window.innerHeight : 0;
|
|
1323
|
+
if (!isOpen && wasOpenRef.current) {
|
|
1324
|
+
wrapper.style.transition = transition;
|
|
1325
|
+
wrapper.style.transform = `translate3d(0, ${viewportH}px, 0)`;
|
|
1326
|
+
} else if (isOpen && !wasOpenRef.current) {
|
|
1327
|
+
wrapper.style.transition = 'none';
|
|
1328
|
+
wrapper.style.transform = `translate3d(0, ${viewportH}px, 0)`;
|
|
1329
|
+
// eslint-disable-next-line no-void
|
|
1330
|
+
void wrapper.offsetHeight;
|
|
1331
|
+
wrapper.style.transition = transition;
|
|
1332
|
+
wrapper.style.transform = 'translate3d(0, 0, 0)';
|
|
1333
|
+
} else if (isOpen && !wrapper.style.transform) {
|
|
1334
|
+
// Fresh mount with open=true — ensure the wrapper starts at rest.
|
|
1335
|
+
wrapper.style.transition = transition;
|
|
1336
|
+
wrapper.style.transform = 'translate3d(0, 0, 0)';
|
|
1337
|
+
}
|
|
1335
1338
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1339
|
+
wasOpenRef.current = isOpen;
|
|
1340
|
+
}, [isOpen, detached, drawerRef]);
|
|
1341
|
+
|
|
1342
|
+
const contentNode = (
|
|
1343
|
+
<DialogPrimitive.Content
|
|
1344
|
+
data-vaul-drawer-direction={direction}
|
|
1345
|
+
data-vaul-drawer=""
|
|
1346
|
+
data-vaul-detached={detached ? 'true' : 'false'}
|
|
1347
|
+
data-vaul-delayed-snap-points={delayedSnapPoints ? 'true' : 'false'}
|
|
1348
|
+
data-vaul-snap-points={isOpen && hasSnapPoints ? 'true' : 'false'}
|
|
1349
|
+
data-vaul-custom-container={container ? 'true' : 'false'}
|
|
1350
|
+
data-vaul-animate={shouldAnimate?.current ? 'true' : 'false'}
|
|
1351
|
+
{...rest}
|
|
1352
|
+
ref={composedRef}
|
|
1353
|
+
style={
|
|
1354
|
+
snapPointsOffset && snapPointsOffset.length > 0
|
|
1355
|
+
? ({
|
|
1356
|
+
'--snap-point-height': `${snapPointsOffset[activeSnapPointIndex ?? 0]!}px`,
|
|
1357
|
+
...style,
|
|
1358
|
+
'pointerEvents': 'auto',
|
|
1359
|
+
} as React.CSSProperties)
|
|
1360
|
+
: ({ ...style, pointerEvents: 'auto' } as React.CSSProperties)
|
|
1347
1361
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1362
|
+
onPointerDown={(event) => {
|
|
1363
|
+
if (handleOnly) return;
|
|
1364
|
+
rest.onPointerDown?.(event);
|
|
1365
|
+
pointerStartRef.current = { x: event.pageX, y: event.pageY };
|
|
1366
|
+
onPress(event);
|
|
1367
|
+
}}
|
|
1368
|
+
onOpenAutoFocus={(e) => {
|
|
1369
|
+
onOpenAutoFocus?.(e);
|
|
1351
1370
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
}
|
|
1371
|
+
if (!autoFocus) {
|
|
1372
|
+
e.preventDefault();
|
|
1373
|
+
}
|
|
1374
|
+
}}
|
|
1375
|
+
onPointerDownOutside={(e) => {
|
|
1376
|
+
onPointerDownOutside?.(e);
|
|
1356
1377
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1378
|
+
if (!modal || e.defaultPrevented || isBelowFade) {
|
|
1379
|
+
e.preventDefault();
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
if (keyboardIsOpen.current) {
|
|
1384
|
+
keyboardIsOpen.current = false;
|
|
1385
|
+
}
|
|
1386
|
+
}}
|
|
1387
|
+
onFocusOutside={(e) => {
|
|
1388
|
+
if (!modal || isBelowFade) {
|
|
1389
|
+
e.preventDefault();
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
}}
|
|
1393
|
+
onPointerMove={(event) => {
|
|
1394
|
+
lastKnownPointerEventRef.current = event;
|
|
1395
|
+
if (handleOnly) return;
|
|
1396
|
+
rest.onPointerMove?.(event);
|
|
1397
|
+
if (!pointerStartRef.current) return;
|
|
1398
|
+
const yPosition = event.pageY - pointerStartRef.current.y;
|
|
1399
|
+
const xPosition = event.pageX - pointerStartRef.current.x;
|
|
1400
|
+
|
|
1401
|
+
const swipeStartThreshold = event.pointerType === 'touch' ? 10 : 2;
|
|
1402
|
+
const delta = { x: xPosition, y: yPosition };
|
|
1403
|
+
|
|
1404
|
+
const isAllowedToSwipe = isDeltaInDirection(delta, direction, swipeStartThreshold);
|
|
1405
|
+
if (isAllowedToSwipe) onDrag(event);
|
|
1406
|
+
else if (
|
|
1407
|
+
Math.abs(xPosition) > swipeStartThreshold ||
|
|
1408
|
+
Math.abs(yPosition) > swipeStartThreshold
|
|
1409
|
+
) {
|
|
1410
|
+
pointerStartRef.current = null;
|
|
1411
|
+
}
|
|
1412
|
+
}}
|
|
1413
|
+
onPointerUp={(event) => {
|
|
1414
|
+
rest.onPointerUp?.(event);
|
|
1384
1415
|
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) {
|
|
1416
|
+
wasBeyondThePointRef.current = false;
|
|
1417
|
+
onRelease(event);
|
|
1418
|
+
}}
|
|
1419
|
+
onPointerOut={(event) => {
|
|
1420
|
+
rest.onPointerOut?.(event);
|
|
1400
1421
|
handleOnPointerUp(lastKnownPointerEventRef.current);
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1422
|
+
}}
|
|
1423
|
+
onContextMenu={(event) => {
|
|
1424
|
+
rest.onContextMenu?.(event);
|
|
1425
|
+
if (lastKnownPointerEventRef.current) {
|
|
1426
|
+
handleOnPointerUp(lastKnownPointerEventRef.current);
|
|
1427
|
+
}
|
|
1428
|
+
}}
|
|
1429
|
+
>
|
|
1430
|
+
{hasAutoSnapPoint ? (
|
|
1431
|
+
<div ref={setAutoSizeNode} data-vaul-auto-size-wrapper="" style={autoSizeWrapperStyle}>
|
|
1432
|
+
{children}
|
|
1433
|
+
</div>
|
|
1434
|
+
) : (
|
|
1435
|
+
children
|
|
1436
|
+
)}
|
|
1437
|
+
</DialogPrimitive.Content>
|
|
1438
|
+
);
|
|
1413
1439
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
}
|
|
1440
|
+
return (
|
|
1441
|
+
<div data-vaul-detached-wrapper="" style={wrapperStyle}>
|
|
1442
|
+
{contentNode}
|
|
1443
|
+
{detachedSiblings}
|
|
1444
|
+
</div>
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
);
|
|
1421
1448
|
|
|
1422
1449
|
Content.displayName = 'Drawer.Content';
|
|
1423
1450
|
|
|
@@ -1434,106 +1461,105 @@ export type HandleProps = React.ComponentPropsWithoutRef<'div'> & {
|
|
|
1434
1461
|
const LONG_HANDLE_PRESS_TIMEOUT = 250;
|
|
1435
1462
|
const DOUBLE_TAP_TIMEOUT = 120;
|
|
1436
1463
|
|
|
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
|
-
|
|
1464
|
+
export const Handle = React.forwardRef<HTMLDivElement, HandleProps>(
|
|
1465
|
+
({ preventCycle = false, children, ...rest }, ref) => {
|
|
1466
|
+
const {
|
|
1467
|
+
closeDrawer,
|
|
1468
|
+
isDragging,
|
|
1469
|
+
snapPoints,
|
|
1470
|
+
activeSnapPoint,
|
|
1471
|
+
setActiveSnapPoint,
|
|
1472
|
+
dismissible,
|
|
1473
|
+
handleOnly,
|
|
1474
|
+
isOpen,
|
|
1475
|
+
onPress,
|
|
1476
|
+
onDrag,
|
|
1477
|
+
} = useDrawerContext();
|
|
1478
|
+
|
|
1479
|
+
const closeTimeoutIdRef = React.useRef<number | null>(null);
|
|
1480
|
+
const shouldCancelInteractionRef = React.useRef(false);
|
|
1481
|
+
|
|
1482
|
+
function handleStartCycle() {
|
|
1483
|
+
// Stop if this is the second click of a double click
|
|
1484
|
+
if (shouldCancelInteractionRef.current) {
|
|
1485
|
+
handleCancelInteraction();
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
window.setTimeout(() => {
|
|
1489
|
+
handleCycleSnapPoints();
|
|
1490
|
+
}, DOUBLE_TAP_TIMEOUT);
|
|
1462
1491
|
}
|
|
1463
|
-
window.setTimeout(() => {
|
|
1464
|
-
handleCycleSnapPoints();
|
|
1465
|
-
}, DOUBLE_TAP_TIMEOUT);
|
|
1466
|
-
}
|
|
1467
1492
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1493
|
+
function handleCycleSnapPoints() {
|
|
1494
|
+
// Prevent accidental taps while resizing drawer
|
|
1495
|
+
if (isDragging || preventCycle || shouldCancelInteractionRef.current) {
|
|
1496
|
+
handleCancelInteraction();
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
// Make sure to clear the timeout id if the user releases the handle before the cancel timeout
|
|
1471
1500
|
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
1501
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1502
|
+
if (!snapPoints || snapPoints.length === 0) {
|
|
1503
|
+
if (!dismissible) {
|
|
1504
|
+
closeDrawer();
|
|
1505
|
+
}
|
|
1506
|
+
return;
|
|
1480
1507
|
}
|
|
1481
|
-
return;
|
|
1482
|
-
}
|
|
1483
1508
|
|
|
1484
|
-
|
|
1509
|
+
const isLastSnapPoint = activeSnapPoint === snapPoints[snapPoints.length - 1];
|
|
1485
1510
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1511
|
+
if (isLastSnapPoint && dismissible) {
|
|
1512
|
+
closeDrawer();
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1490
1515
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1516
|
+
const currentSnapIndex = snapPoints.findIndex((point) => point === activeSnapPoint);
|
|
1517
|
+
if (currentSnapIndex === -1) return; // activeSnapPoint not found in snapPoints
|
|
1518
|
+
const nextSnapPoint = snapPoints[currentSnapIndex + 1];
|
|
1519
|
+
if (nextSnapPoint === undefined) return;
|
|
1520
|
+
setActiveSnapPoint(nextSnapPoint);
|
|
1521
|
+
}
|
|
1497
1522
|
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1523
|
+
function handleStartInteraction() {
|
|
1524
|
+
closeTimeoutIdRef.current = window.setTimeout(() => {
|
|
1525
|
+
// Cancel click interaction on a long press
|
|
1526
|
+
shouldCancelInteractionRef.current = true;
|
|
1527
|
+
}, LONG_HANDLE_PRESS_TIMEOUT);
|
|
1528
|
+
}
|
|
1504
1529
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1530
|
+
function handleCancelInteraction() {
|
|
1531
|
+
if (closeTimeoutIdRef.current) {
|
|
1532
|
+
window.clearTimeout(closeTimeoutIdRef.current);
|
|
1533
|
+
}
|
|
1534
|
+
shouldCancelInteractionRef.current = false;
|
|
1508
1535
|
}
|
|
1509
|
-
shouldCancelInteractionRef.current = false;
|
|
1510
|
-
}
|
|
1511
1536
|
|
|
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
|
-
}
|
|
1537
|
+
return (
|
|
1538
|
+
<div
|
|
1539
|
+
onClick={handleStartCycle}
|
|
1540
|
+
onPointerCancel={handleCancelInteraction}
|
|
1541
|
+
onPointerDown={(e) => {
|
|
1542
|
+
if (handleOnly) onPress(e);
|
|
1543
|
+
handleStartInteraction();
|
|
1544
|
+
}}
|
|
1545
|
+
onPointerMove={(e) => {
|
|
1546
|
+
if (handleOnly) onDrag(e);
|
|
1547
|
+
}}
|
|
1548
|
+
// onPointerUp is already handled by the content component
|
|
1549
|
+
ref={ref}
|
|
1550
|
+
data-vaul-drawer-visible={isOpen ? 'true' : 'false'}
|
|
1551
|
+
data-vaul-handle=""
|
|
1552
|
+
aria-hidden="true"
|
|
1553
|
+
{...rest}
|
|
1554
|
+
>
|
|
1555
|
+
{/* Expand handle's hit area beyond what's visible to ensure a 44x44 tap target for touch devices */}
|
|
1556
|
+
<span data-vaul-handle-hitarea="" aria-hidden="true">
|
|
1557
|
+
{children}
|
|
1558
|
+
</span>
|
|
1559
|
+
</div>
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
);
|
|
1537
1563
|
|
|
1538
1564
|
Handle.displayName = 'Drawer.Handle';
|
|
1539
1565
|
|