@lodev09/react-native-true-sheet 3.10.1 → 3.11.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/module/TrueSheet.js +3 -0
- package/lib/module/TrueSheet.js.map +1 -1
- package/lib/module/TrueSheet.web.js +604 -376
- package/lib/module/TrueSheet.web.js.map +1 -1
- package/lib/module/TrueSheetProvider.js.map +1 -1
- package/lib/module/TrueSheetProvider.web.js +141 -100
- package/lib/module/TrueSheetProvider.web.js.map +1 -1
- package/lib/module/mocks/index.js.map +1 -1
- package/lib/module/reanimated/ReanimatedTrueSheet.web.js.map +1 -1
- package/lib/module/reanimated/useReanimatedPositionChangeHandler.web.js +2 -3
- package/lib/module/reanimated/useReanimatedPositionChangeHandler.web.js.map +1 -1
- package/lib/module/web/constants.js +16 -0
- package/lib/module/web/constants.js.map +1 -0
- package/lib/module/web/vaul/README.md +9 -0
- package/lib/module/web/vaul/browser.js +30 -0
- package/lib/module/web/vaul/browser.js.map +1 -0
- package/lib/module/web/vaul/constants.js +14 -0
- package/lib/module/web/vaul/constants.js.map +1 -0
- package/lib/module/web/vaul/context.js +57 -0
- package/lib/module/web/vaul/context.js.map +1 -0
- package/lib/module/web/vaul/helpers.js +93 -0
- package/lib/module/web/vaul/helpers.js.map +1 -0
- package/lib/module/web/vaul/index.js +1206 -0
- package/lib/module/web/vaul/index.js.map +1 -0
- package/lib/module/web/vaul/style.css +264 -0
- package/lib/module/web/vaul/types.js +2 -0
- package/lib/module/web/vaul/types.js.map +1 -0
- package/lib/module/web/vaul/use-composed-refs.js +34 -0
- package/lib/module/web/vaul/use-composed-refs.js.map +1 -0
- package/lib/module/web/vaul/use-controllable-state.js +54 -0
- package/lib/module/web/vaul/use-controllable-state.js.map +1 -0
- package/lib/module/web/vaul/use-position-fixed.js +123 -0
- package/lib/module/web/vaul/use-position-fixed.js.map +1 -0
- package/lib/module/web/vaul/use-prevent-scroll.js +258 -0
- package/lib/module/web/vaul/use-prevent-scroll.js.map +1 -0
- package/lib/module/web/vaul/use-scale-background.js +57 -0
- package/lib/module/web/vaul/use-scale-background.js.map +1 -0
- package/lib/module/web/vaul/use-snap-points.js +294 -0
- package/lib/module/web/vaul/use-snap-points.js.map +1 -0
- package/lib/typescript/src/TrueSheet.d.ts +2 -2
- package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.types.d.ts +79 -36
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts +3 -11
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheetProvider.d.ts +2 -2
- package/lib/typescript/src/TrueSheetProvider.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheetProvider.web.d.ts +17 -14
- package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
- package/lib/typescript/src/mocks/index.d.ts +1 -2
- package/lib/typescript/src/mocks/index.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/reanimated/ReanimatedTrueSheet.web.d.ts +0 -1
- package/lib/typescript/src/reanimated/ReanimatedTrueSheet.web.d.ts.map +1 -1
- package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.web.d.ts +2 -3
- package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.web.d.ts.map +1 -1
- package/lib/typescript/src/web/constants.d.ts +14 -0
- package/lib/typescript/src/web/constants.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/browser.d.ts +8 -0
- package/lib/typescript/src/web/vaul/browser.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/constants.d.ts +12 -0
- package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/context.d.ts +45 -0
- package/lib/typescript/src/web/vaul/context.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/helpers.d.ts +17 -0
- package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/index.d.ts +210 -0
- package/lib/typescript/src/web/vaul/index.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/types.d.ts +7 -0
- package/lib/typescript/src/web/vaul/types.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-composed-refs.d.ts +14 -0
- package/lib/typescript/src/web/vaul/use-composed-refs.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-controllable-state.d.ts +9 -0
- package/lib/typescript/src/web/vaul/use-controllable-state.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-position-fixed.d.ts +19 -0
- package/lib/typescript/src/web/vaul/use-position-fixed.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-prevent-scroll.d.ts +18 -0
- package/lib/typescript/src/web/vaul/use-prevent-scroll.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-scale-background.d.ts +2 -0
- package/lib/typescript/src/web/vaul/use-scale-background.d.ts.map +1 -0
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts +37 -0
- package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/TrueSheet.tsx +9 -1
- package/src/TrueSheet.types.ts +88 -38
- package/src/TrueSheet.web.tsx +675 -450
- package/src/TrueSheetProvider.tsx +2 -4
- package/src/TrueSheetProvider.web.tsx +193 -125
- package/src/mocks/index.ts +1 -2
- package/src/navigation/types.ts +0 -1
- package/src/reanimated/ReanimatedTrueSheet.web.tsx +0 -1
- package/src/reanimated/useReanimatedPositionChangeHandler.web.ts +2 -3
- package/src/web/constants.ts +13 -0
- package/src/web/vaul/README.md +9 -0
- package/src/web/vaul/browser.ts +38 -0
- package/src/web/vaul/constants.ts +18 -0
- package/src/web/vaul/context.ts +87 -0
- package/src/web/vaul/helpers.ts +124 -0
- package/src/web/vaul/index.tsx +1590 -0
- package/src/web/vaul/style.css +264 -0
- package/src/web/vaul/types.ts +7 -0
- package/src/web/vaul/use-composed-refs.ts +35 -0
- package/src/web/vaul/use-controllable-state.ts +66 -0
- package/src/web/vaul/use-position-fixed.ts +147 -0
- package/src/web/vaul/use-prevent-scroll.ts +309 -0
- package/src/web/vaul/use-scale-background.ts +64 -0
- package/src/web/vaul/use-snap-points.ts +433 -0
|
@@ -1,456 +1,684 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
/// <reference lib="dom" />
|
|
4
|
+
import { createElement, forwardRef, isValidElement, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { View, useColorScheme, useWindowDimensions } from 'react-native';
|
|
6
|
+
import { Drawer } from "./web/vaul/index.js";
|
|
7
|
+
import { TRANSITIONS } from "./web/vaul/constants.js";
|
|
8
|
+
import { usePortalContainer, useRegisterSheet, useSheetStack } from "./TrueSheetProvider.web.js";
|
|
9
|
+
import { COLOR_SURFACE_CONTAINER_LOW_DARK, COLOR_SURFACE_CONTAINER_LOW_LIGHT, DEFAULT_ANCHOR_OFFSET, DEFAULT_CORNER_RADIUS, DEFAULT_DETACHED_OFFSET, DEFAULT_GRABBER_COLOR_DARK, DEFAULT_GRABBER_COLOR_LIGHT, DEFAULT_GRABBER_HEIGHT, DEFAULT_GRABBER_TOP_MARGIN, DEFAULT_GRABBER_WIDTH, DEFAULT_FORM_SHEET_HEIGHT_RATIO, DEFAULT_FORM_SHEET_WIDTH, DEFAULT_MAX_WIDTH } from "./web/constants.js";
|
|
8
10
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
9
|
-
const DEFAULT_CORNER_RADIUS = 16;
|
|
10
|
-
const DEFAULT_ELEVATION = 4;
|
|
11
|
-
const DEFAULT_MAX_WIDTH = 640;
|
|
12
|
-
// M3 baseline surfaceContainerLow
|
|
13
|
-
const COLOR_SURFACE_CONTAINER_LOW_LIGHT = '#F7F2FA';
|
|
14
|
-
const COLOR_SURFACE_CONTAINER_LOW_DARK = '#1D1B20';
|
|
15
|
-
const DEFAULT_ANCHOR_OFFSET = 16;
|
|
16
|
-
const DEFAULT_DETACHED_OFFSET = 16;
|
|
17
|
-
const DEFAULT_GRABBER_COLOR_LIGHT = 'rgba(0, 0, 0, 0.3)';
|
|
18
|
-
const DEFAULT_GRABBER_COLOR_DARK = 'rgba(255, 255, 255, 0.3)';
|
|
19
|
-
const DEFAULT_GRABBER_WIDTH = 32;
|
|
20
|
-
const DEFAULT_GRABBER_HEIGHT = 4;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Converts elevation to CSS box-shadow based on Material Design 3 elevation system.
|
|
24
|
-
* Uses a combination of ambient and key shadows for realistic depth.
|
|
25
|
-
*/
|
|
26
|
-
const getElevationShadow = elevation => {
|
|
27
|
-
if (elevation <= 0) return 'none';
|
|
28
|
-
const ambientY = elevation * 0.5;
|
|
29
|
-
const ambientBlur = elevation * 1.5;
|
|
30
|
-
const ambientOpacity = 0.08 + elevation * 0.01;
|
|
31
|
-
const keyY = elevation;
|
|
32
|
-
const keyBlur = elevation * 2;
|
|
33
|
-
const keyOpacity = 0.12 + elevation * 0.02;
|
|
34
|
-
return `0px ${ambientY}px ${ambientBlur}px rgba(0, 0, 0, ${ambientOpacity}), 0px ${keyY}px ${keyBlur}px rgba(0, 0, 0, ${keyOpacity})`;
|
|
35
|
-
};
|
|
36
|
-
const renderSlot = slot => {
|
|
37
|
-
if (!slot) return null;
|
|
38
|
-
if (/*#__PURE__*/isValidElement(slot)) return slot;
|
|
39
|
-
return /*#__PURE__*/createElement(slot);
|
|
40
|
-
};
|
|
41
11
|
const TrueSheetComponent = /*#__PURE__*/forwardRef((props, ref) => {
|
|
42
12
|
const {
|
|
13
|
+
children,
|
|
43
14
|
name,
|
|
44
|
-
detents = [0.5, 1],
|
|
45
15
|
dismissible = true,
|
|
46
16
|
draggable = true,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
children,
|
|
50
|
-
scrollable = false,
|
|
51
|
-
initialDetentIndex = -1,
|
|
17
|
+
cornerRadius,
|
|
18
|
+
style,
|
|
52
19
|
backgroundColor: backgroundColorProp,
|
|
53
|
-
cornerRadius = DEFAULT_CORNER_RADIUS,
|
|
54
|
-
elevation = DEFAULT_ELEVATION,
|
|
55
|
-
grabber = true,
|
|
56
|
-
grabberOptions,
|
|
57
20
|
maxContentHeight,
|
|
58
21
|
maxContentWidth,
|
|
59
22
|
anchor = 'center',
|
|
60
23
|
anchorOffset = DEFAULT_ANCHOR_OFFSET,
|
|
24
|
+
grabber = true,
|
|
25
|
+
grabberOptions,
|
|
26
|
+
detents = [0.5, 1],
|
|
27
|
+
dimmed = true,
|
|
28
|
+
dimmedDetentIndex = 0,
|
|
29
|
+
initialDetentIndex = -1,
|
|
61
30
|
header,
|
|
62
31
|
headerStyle,
|
|
63
32
|
footer,
|
|
64
33
|
footerStyle,
|
|
65
|
-
|
|
34
|
+
scrollable = false,
|
|
35
|
+
scrollableOptions,
|
|
36
|
+
pageSizing = true,
|
|
37
|
+
detached = false,
|
|
38
|
+
detachedOffset = DEFAULT_DETACHED_OFFSET,
|
|
39
|
+
elevation = 4,
|
|
40
|
+
insetAdjustment = 'automatic',
|
|
41
|
+
initialDetentAnimated = true,
|
|
42
|
+
onPositionChange,
|
|
66
43
|
onWillPresent,
|
|
67
44
|
onDidPresent,
|
|
68
45
|
onWillDismiss,
|
|
69
46
|
onDidDismiss,
|
|
70
47
|
onDetentChange,
|
|
71
|
-
onPositionChange,
|
|
72
48
|
onDragBegin,
|
|
73
49
|
onDragChange,
|
|
74
50
|
onDragEnd,
|
|
51
|
+
onMount,
|
|
75
52
|
onWillFocus,
|
|
76
53
|
onDidFocus,
|
|
77
54
|
onWillBlur,
|
|
78
|
-
onDidBlur
|
|
79
|
-
detached,
|
|
80
|
-
detachedOffset = DEFAULT_DETACHED_OFFSET,
|
|
81
|
-
stackBehavior = 'switch',
|
|
82
|
-
style
|
|
55
|
+
onDidBlur
|
|
83
56
|
} = props;
|
|
84
|
-
const
|
|
85
|
-
const
|
|
57
|
+
const validDetents = useMemo(() => detents.filter(d => typeof d === 'number' || d === 'auto'), [detents]);
|
|
58
|
+
const snapPointsProps = useMemo(() => {
|
|
59
|
+
if (validDetents.length === 0) return {};
|
|
60
|
+
return {
|
|
61
|
+
snapPoints: validDetents,
|
|
62
|
+
fadeFromIndex: Math.min(dimmedDetentIndex, validDetents.length - 1)
|
|
63
|
+
};
|
|
64
|
+
}, [validDetents, dimmedDetentIndex]);
|
|
86
65
|
const {
|
|
87
66
|
width: windowWidth,
|
|
88
67
|
height: windowHeight
|
|
89
68
|
} = useWindowDimensions();
|
|
90
69
|
const isLandscapeOrTablet = windowWidth >= 600 || windowWidth > windowHeight;
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
const initialDetentIndexRef = useRef(initialDetentIndex);
|
|
97
|
-
const currentIndexRef = useRef(0);
|
|
98
|
-
const isPresenting = useRef(false);
|
|
99
|
-
const isDismissing = useRef(false);
|
|
100
|
-
const isMinimized = useRef(false);
|
|
101
|
-
const isDragging = useRef(false);
|
|
102
|
-
const presentResolver = useRef(null);
|
|
103
|
-
const dismissResolver = useRef(null);
|
|
104
|
-
const animatedPosition = useSharedValue(windowHeight);
|
|
105
|
-
const animatedIndex = useSharedValue(0);
|
|
106
|
-
const [snapIndex, setSnapIndex] = useState(initialDetentIndex);
|
|
107
|
-
const [isMounted, setIsMounted] = useState(false);
|
|
108
|
-
const isNonModal = stackBehavior === 'none';
|
|
109
|
-
useDerivedValue(() => {
|
|
110
|
-
onPositionChange?.({
|
|
111
|
-
nativeEvent: {
|
|
112
|
-
position: animatedPosition.value,
|
|
113
|
-
index: animatedIndex.value,
|
|
114
|
-
detent: detents[animatedIndex.value] ?? 0,
|
|
115
|
-
realtime: true
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
const hasAutoDetent = detents.includes('auto');
|
|
120
|
-
const containerHeight = maxContentHeight ?? windowHeight;
|
|
121
|
-
const snapPoints = useMemo(() => detents.filter(detent => detent !== 'auto' && typeof detent === 'number').map(detent => Math.min(1, Math.max(0.1, detent)) * containerHeight), [detents, containerHeight]);
|
|
122
|
-
const handleChange = useCallback((index, _position, _type) => {
|
|
123
|
-
const previousIndex = currentIndexRef.current;
|
|
124
|
-
currentIndexRef.current = index;
|
|
70
|
+
const colorScheme = useColorScheme();
|
|
71
|
+
const backgroundColor = backgroundColorProp ?? (colorScheme === 'dark' ? COLOR_SURFACE_CONTAINER_LOW_DARK : COLOR_SURFACE_CONTAINER_LOW_LIGHT);
|
|
72
|
+
const shouldAutoPresent = initialDetentIndex >= 0 && initialDetentIndex < validDetents.length;
|
|
73
|
+
const [isOpen, setIsOpen] = useState(shouldAutoPresent);
|
|
74
|
+
const [activeSnapPoint, setActiveSnapPoint] = useState(() => validDetents[shouldAutoPresent ? initialDetentIndex : 0] ?? null);
|
|
125
75
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
76
|
+
// Keep activeSnapPoint valid if detents change (e.g., prop updates).
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (validDetents.length === 0) return;
|
|
79
|
+
setActiveSnapPoint(current => current != null && validDetents.includes(current) ? current : validDetents[0]);
|
|
80
|
+
}, [validDetents]);
|
|
81
|
+
const validDetentsRef = useRef(validDetents);
|
|
82
|
+
validDetentsRef.current = validDetents;
|
|
83
|
+
const handleSetActiveSnapPoint = useCallback(snapPoint => {
|
|
84
|
+
setActiveSnapPoint(snapPoint == null ? null : typeof snapPoint === 'number' || snapPoint === 'auto' ? snapPoint : null);
|
|
85
|
+
}, []);
|
|
86
|
+
const handleOpenChange = useCallback(open => {
|
|
87
|
+
if (!open && isOpen) {
|
|
88
|
+
setIsOpen(false);
|
|
136
89
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
90
|
+
}, [isOpen]);
|
|
91
|
+
const portalContainer = usePortalContainer();
|
|
92
|
+
const handlePointerDownOutside = e => {
|
|
93
|
+
const target = e.target;
|
|
94
|
+
if (!(target instanceof Node)) return;
|
|
95
|
+
// Pointer down that landed outside this sheet's portal container (e.g.,
|
|
96
|
+
// in another screen's tree when navigating) should not close the drawer.
|
|
97
|
+
if (portalContainer && !portalContainer.contains(target)) {
|
|
98
|
+
e.preventDefault();
|
|
145
99
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
100
|
+
};
|
|
101
|
+
const dismissAboveRef = useRef(async () => {});
|
|
102
|
+
const methods = useMemo(() => ({
|
|
103
|
+
present: async (index = 0) => {
|
|
104
|
+
const detent = validDetentsRef.current[index];
|
|
105
|
+
if (detent === undefined) {
|
|
106
|
+
throw new Error(`TrueSheet: present index (${index}) is out of bounds. detents array has ${validDetentsRef.current.length} item(s)`);
|
|
153
107
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
108
|
+
setActiveSnapPoint(detent);
|
|
109
|
+
setIsOpen(true);
|
|
110
|
+
},
|
|
111
|
+
dismiss: async () => {
|
|
112
|
+
setIsOpen(false);
|
|
113
|
+
},
|
|
114
|
+
resize: async index => {
|
|
115
|
+
const detent = validDetentsRef.current[index];
|
|
116
|
+
if (detent === undefined) {
|
|
117
|
+
throw new Error(`TrueSheet: resize index (${index}) is out of bounds. detents array has ${validDetentsRef.current.length} item(s)`);
|
|
118
|
+
}
|
|
119
|
+
setActiveSnapPoint(detent);
|
|
120
|
+
},
|
|
121
|
+
dismissStack: async animated => {
|
|
122
|
+
await dismissAboveRef.current(animated);
|
|
164
123
|
}
|
|
124
|
+
}), []);
|
|
125
|
+
useImperativeHandle(ref, () => methods, [methods]);
|
|
126
|
+
const methodsRef = useRef(methods);
|
|
127
|
+
useRegisterSheet(name, methodsRef);
|
|
128
|
+
const drawerContentRef = useRef(null);
|
|
165
129
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
130
|
+
// Present/dismiss events. The sheet settles via a CSS `transform` transition
|
|
131
|
+
// on either the drawer (snap-points on autopresent) or the wrapper (whole-
|
|
132
|
+
// card slide on reopen/dismiss). `Animation.finished` from the Web Animations
|
|
133
|
+
// API tracks whichever is actually running — reflects what the browser is
|
|
134
|
+
// doing, doesn't miss when no transition runs (same-value change), and
|
|
135
|
+
// handles interruptions correctly (a drag/resnap mid-present resolves only
|
|
136
|
+
// once all transform animations drain).
|
|
137
|
+
const onWillPresentRef = useRef(onWillPresent);
|
|
138
|
+
const onDidPresentRef = useRef(onDidPresent);
|
|
139
|
+
const onWillDismissRef = useRef(onWillDismiss);
|
|
140
|
+
const onDidDismissRef = useRef(onDidDismiss);
|
|
141
|
+
const onDetentChangeRef = useRef(onDetentChange);
|
|
142
|
+
const onDragBeginRef = useRef(onDragBegin);
|
|
143
|
+
const onDragChangeRef = useRef(onDragChange);
|
|
144
|
+
const onDragEndRef = useRef(onDragEnd);
|
|
145
|
+
const onPositionChangeRef = useRef(onPositionChange);
|
|
146
|
+
const activeSnapPointRef = useRef(activeSnapPoint);
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
onWillPresentRef.current = onWillPresent;
|
|
149
|
+
onDidPresentRef.current = onDidPresent;
|
|
150
|
+
onWillDismissRef.current = onWillDismiss;
|
|
151
|
+
onDidDismissRef.current = onDidDismiss;
|
|
152
|
+
onDetentChangeRef.current = onDetentChange;
|
|
153
|
+
onDragBeginRef.current = onDragBegin;
|
|
154
|
+
onDragChangeRef.current = onDragChange;
|
|
155
|
+
onDragEndRef.current = onDragEnd;
|
|
156
|
+
onPositionChangeRef.current = onPositionChange;
|
|
157
|
+
activeSnapPointRef.current = activeSnapPoint;
|
|
158
|
+
});
|
|
159
|
+
const computeDetentInfo = useCallback(() => {
|
|
160
|
+
const snap = activeSnapPointRef.current;
|
|
161
|
+
const index = snap != null ? validDetentsRef.current.indexOf(snap) : -1;
|
|
162
|
+
const position = drawerContentRef.current?.getBoundingClientRect().top ?? 0;
|
|
163
|
+
const detent = typeof snap === 'number' ? snap : 0;
|
|
164
|
+
return {
|
|
165
|
+
index,
|
|
166
|
+
position,
|
|
167
|
+
detent
|
|
168
|
+
};
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
171
|
+
// Mirror Android: interpolate fractional index and detent from the drawer's
|
|
172
|
+
// top-Y so continuous position updates (drag, animation) carry smooth values
|
|
173
|
+
// between detent boundaries. Numeric detent d → top-Y = (1 - d) * effectiveH.
|
|
174
|
+
// 'auto' resolves to the [data-vaul-auto-size-wrapper] element's measured
|
|
175
|
+
// offsetHeight — same signal vaul uses to compute its snap offset.
|
|
176
|
+
const interpolateFromPosition = useCallback(position => {
|
|
177
|
+
const snaps = validDetentsRef.current;
|
|
178
|
+
const count = snaps.length;
|
|
179
|
+
if (count === 0) return {
|
|
180
|
+
index: -1,
|
|
181
|
+
detent: 0
|
|
182
|
+
};
|
|
183
|
+
const windowH = window.innerHeight;
|
|
184
|
+
const effectiveH = detached ? windowH - detachedOffset : windowH;
|
|
185
|
+
// Matches vaul's height ceiling: min(effectiveH, maxContentHeight).
|
|
186
|
+
const ceiling = maxContentHeight !== undefined ? Math.min(effectiveH, maxContentHeight) : effectiveH;
|
|
187
|
+
const autoWrapper = drawerContentRef.current?.querySelector('[data-vaul-auto-size-wrapper]');
|
|
188
|
+
const autoHeight = Math.min(autoWrapper?.offsetHeight ?? ceiling / 2, ceiling);
|
|
189
|
+
const positions = [];
|
|
190
|
+
const values = [];
|
|
191
|
+
for (let i = 0; i < count; i++) {
|
|
192
|
+
const d = snaps[i];
|
|
193
|
+
if (typeof d === 'number') {
|
|
194
|
+
const h = Math.min(d * effectiveH, ceiling);
|
|
195
|
+
positions.push(effectiveH - h);
|
|
196
|
+
values.push(effectiveH > 0 ? h / effectiveH : 0);
|
|
197
|
+
} else {
|
|
198
|
+
positions.push(effectiveH - autoHeight);
|
|
199
|
+
values.push(effectiveH > 0 ? autoHeight / effectiveH : 0);
|
|
200
|
+
}
|
|
171
201
|
}
|
|
172
202
|
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
203
|
+
// Absorb subpixel drift from getBoundingClientRect so at-rest positions
|
|
204
|
+
// don't sneak into the below-first branch and emit near-zero negatives
|
|
205
|
+
// like `-1e-8` (which render as "-1" via JS scientific-notation toString).
|
|
206
|
+
const epsilon = 0.5;
|
|
207
|
+
const firstPos = positions[0];
|
|
208
|
+
const lastPos = positions[count - 1];
|
|
209
|
+
if (position > firstPos + epsilon) {
|
|
210
|
+
// Two ranges: index spans the full animation (windowH of wrapper
|
|
211
|
+
// travel) so it's smooth for driving dependent animations end-to-end;
|
|
212
|
+
// detent tracks the sheet's visible-height ratio (windowH - firstPos)
|
|
213
|
+
// so its 0–values[0] fade has fine resolution while the sheet is still
|
|
214
|
+
// in view. Both clamp to keep outputs in [-1, 0].
|
|
215
|
+
const indexRaw = (position - firstPos) / windowH;
|
|
216
|
+
const detentRaw = (position - firstPos) / Math.max(1, windowH - firstPos);
|
|
217
|
+
const indexProgress = Math.max(0, Math.min(1, indexRaw));
|
|
218
|
+
const detentProgress = Math.max(0, Math.min(1, detentRaw));
|
|
219
|
+
return {
|
|
220
|
+
index: -indexProgress,
|
|
221
|
+
detent: Math.max(0, values[0] * (1 - detentProgress))
|
|
222
|
+
};
|
|
179
223
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
224
|
+
if (count === 1) return {
|
|
225
|
+
index: 0,
|
|
226
|
+
detent: values[0]
|
|
227
|
+
};
|
|
184
228
|
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
229
|
+
// Clamp into the segment range so subpixel drift at the boundaries
|
|
230
|
+
// resolves cleanly to the nearest segment edge (index 0 or count-1).
|
|
231
|
+
const clamped = Math.max(lastPos, Math.min(firstPos, position));
|
|
232
|
+
for (let i = 0; i < count - 1; i++) {
|
|
233
|
+
const pos = positions[i];
|
|
234
|
+
const nextPos = positions[i + 1];
|
|
235
|
+
if (clamped >= nextPos && clamped <= pos) {
|
|
236
|
+
const range = pos - nextPos;
|
|
237
|
+
const progress = range > 0 ? (pos - clamped) / range : 0;
|
|
238
|
+
const clampedProgress = Math.max(0, Math.min(1, progress));
|
|
239
|
+
return {
|
|
240
|
+
index: i + clampedProgress,
|
|
241
|
+
detent: values[i] + clampedProgress * (values[i + 1] - values[i])
|
|
242
|
+
};
|
|
243
|
+
}
|
|
189
244
|
}
|
|
190
|
-
|
|
245
|
+
return {
|
|
246
|
+
index: count - 1,
|
|
247
|
+
detent: values[count - 1]
|
|
248
|
+
};
|
|
249
|
+
}, [detached, detachedOffset, maxContentHeight]);
|
|
250
|
+
const handlePositionChange = useCallback(position => {
|
|
251
|
+
const {
|
|
252
|
+
index,
|
|
253
|
+
detent
|
|
254
|
+
} = interpolateFromPosition(position);
|
|
255
|
+
onPositionChangeRef.current?.({
|
|
256
|
+
nativeEvent: {
|
|
257
|
+
index,
|
|
258
|
+
position,
|
|
259
|
+
detent,
|
|
260
|
+
realtime: true
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}, [interpolateFromPosition]);
|
|
264
|
+
|
|
265
|
+
// Fire onMount once after first render. React-mount is the earliest point
|
|
266
|
+
// the component is ready for imperative calls, matching the native
|
|
267
|
+
// "ready for present" contract. Declared before the present/dismiss effect
|
|
268
|
+
// so it fires first during autopresent (onMount → onWillPresent).
|
|
269
|
+
const onMountRef = useRef(onMount);
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
onMountRef.current = onMount;
|
|
272
|
+
});
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
onMountRef.current?.({
|
|
191
275
|
nativeEvent: null
|
|
192
276
|
});
|
|
277
|
+
}, []);
|
|
193
278
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (!
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
position: animatedPosition.value,
|
|
207
|
-
detent: detents[currentIndexRef.current] ?? 0
|
|
208
|
-
}
|
|
279
|
+
// Start at `false` so a mount with `isOpen=true` (autopresent via
|
|
280
|
+
// `initialDetentIndex`) is detected as a false→true transition and fires
|
|
281
|
+
// `onWillPresent`.
|
|
282
|
+
const wasOpenRef = useRef(false);
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
const wasOpen = wasOpenRef.current;
|
|
285
|
+
wasOpenRef.current = isOpen;
|
|
286
|
+
if (!isOpen && !wasOpen) return undefined;
|
|
287
|
+
const present = !wasOpen && isOpen;
|
|
288
|
+
if (present) {
|
|
289
|
+
onWillPresentRef.current?.({
|
|
290
|
+
nativeEvent: computeDetentInfo()
|
|
209
291
|
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (isDragging.current && toIndex >= 0) {
|
|
214
|
-
onDragChange?.({
|
|
215
|
-
nativeEvent: {
|
|
216
|
-
index: toIndex,
|
|
217
|
-
position: animatedPosition.value,
|
|
218
|
-
detent: detents[toIndex] ?? 0
|
|
219
|
-
}
|
|
292
|
+
} else if (wasOpen && !isOpen) {
|
|
293
|
+
onWillDismissRef.current?.({
|
|
294
|
+
nativeEvent: null
|
|
220
295
|
});
|
|
296
|
+
} else {
|
|
297
|
+
return undefined;
|
|
221
298
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
299
|
+
const fireDone = () => {
|
|
300
|
+
if (present) {
|
|
301
|
+
onDidPresentRef.current?.({
|
|
302
|
+
nativeEvent: computeDetentInfo()
|
|
303
|
+
});
|
|
304
|
+
} else {
|
|
305
|
+
onDidDismissRef.current?.({
|
|
227
306
|
nativeEvent: null
|
|
228
307
|
});
|
|
229
308
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
309
|
+
};
|
|
310
|
+
let canceled = false;
|
|
311
|
+
let rafId = 0;
|
|
312
|
+
const start = () => {
|
|
313
|
+
if (canceled) return;
|
|
314
|
+
const drawer = drawerContentRef.current;
|
|
315
|
+
if (!drawer) {
|
|
316
|
+
// Drawer hasn't mounted yet (Radix Presence defers the portal mount
|
|
317
|
+
// past the first effect pass). Poll until the ref is populated.
|
|
318
|
+
rafId = window.requestAnimationFrame(start);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const wrapper = drawer.closest('[data-vaul-detached-wrapper]') ?? null;
|
|
322
|
+
const targets = wrapper ? [drawer, wrapper] : [drawer];
|
|
323
|
+
const waitForSettle = () => {
|
|
324
|
+
if (canceled) return;
|
|
325
|
+
// Force style recalc so transitions queued by vaul's effects this
|
|
326
|
+
// commit are registered in `getAnimations()`. RAF callbacks run BEFORE
|
|
327
|
+
// the frame's style recalc, and ignoring this returns a stale empty
|
|
328
|
+
// list — we'd fire `did` immediately with nothing queued.
|
|
329
|
+
|
|
330
|
+
drawer.offsetHeight;
|
|
331
|
+
const pending = targets.flatMap(el => el.getAnimations().filter(a => a.playState !== 'finished'));
|
|
332
|
+
if (pending.length === 0) {
|
|
333
|
+
fireDone();
|
|
334
|
+
return;
|
|
235
335
|
}
|
|
236
|
-
|
|
336
|
+
// allSettled: resolve even when a transition is canceled (drag /
|
|
337
|
+
// resnap), then re-check — a replacement transition may have started.
|
|
338
|
+
Promise.allSettled(pending.map(a => a.finished)).then(() => {
|
|
339
|
+
if (!canceled) waitForSettle();
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
waitForSettle();
|
|
343
|
+
};
|
|
344
|
+
rafId = window.requestAnimationFrame(start);
|
|
345
|
+
return () => {
|
|
346
|
+
canceled = true;
|
|
347
|
+
window.cancelAnimationFrame(rafId);
|
|
348
|
+
};
|
|
349
|
+
}, [isOpen, computeDetentInfo]);
|
|
237
350
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
351
|
+
// Fire onDetentChange only while open→open. Present/dismiss have their own
|
|
352
|
+
// events and carry detent info via onDidPresent, so we skip those edges.
|
|
353
|
+
const detentChangeStateRef = useRef({
|
|
354
|
+
isOpen,
|
|
355
|
+
activeSnapPoint
|
|
356
|
+
});
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
const prev = detentChangeStateRef.current;
|
|
359
|
+
detentChangeStateRef.current = {
|
|
360
|
+
isOpen,
|
|
361
|
+
activeSnapPoint
|
|
362
|
+
};
|
|
363
|
+
if (!prev.isOpen || !isOpen) return;
|
|
364
|
+
if (prev.activeSnapPoint === activeSnapPoint) return;
|
|
365
|
+
onDetentChangeRef.current?.({
|
|
366
|
+
nativeEvent: computeDetentInfo()
|
|
367
|
+
});
|
|
368
|
+
}, [isOpen, activeSnapPoint, computeDetentInfo]);
|
|
369
|
+
|
|
370
|
+
// Vaul's `onDrag` fires once per pointermove while dragging; the first tick
|
|
371
|
+
// after an idle gap marks the drag boundary, so track it via a ref.
|
|
372
|
+
const isDraggingRef = useRef(false);
|
|
373
|
+
const handleDrag = useCallback(() => {
|
|
374
|
+
if (!isDraggingRef.current) {
|
|
375
|
+
isDraggingRef.current = true;
|
|
376
|
+
onDragBeginRef.current?.({
|
|
377
|
+
nativeEvent: computeDetentInfo()
|
|
241
378
|
});
|
|
242
379
|
}
|
|
380
|
+
onDragChangeRef.current?.({
|
|
381
|
+
nativeEvent: computeDetentInfo()
|
|
382
|
+
});
|
|
383
|
+
}, [computeDetentInfo]);
|
|
384
|
+
const handleRelease = useCallback(() => {
|
|
385
|
+
if (!isDraggingRef.current) return;
|
|
386
|
+
isDraggingRef.current = false;
|
|
387
|
+
onDragEndRef.current?.({
|
|
388
|
+
nativeEvent: computeDetentInfo()
|
|
389
|
+
});
|
|
390
|
+
}, [computeDetentInfo]);
|
|
391
|
+
const {
|
|
392
|
+
isNested,
|
|
393
|
+
dismissAbove,
|
|
394
|
+
descendants
|
|
395
|
+
} = useSheetStack(methodsRef, drawerContentRef, isOpen);
|
|
396
|
+
dismissAboveRef.current = dismissAbove;
|
|
243
397
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
398
|
+
// Mirror Android: translate this sheet down to match the deepest descendant's
|
|
399
|
+
// top so the whole stack visually aligns. Cascades because every ancestor
|
|
400
|
+
// re-runs whenever the stack (and thus its descendants) changes.
|
|
401
|
+
useEffect(() => {
|
|
402
|
+
const parent = drawerContentRef.current;
|
|
403
|
+
if (!parent) return;
|
|
404
|
+
const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
|
|
405
|
+
if (descendants.length === 0) {
|
|
406
|
+
parent.style.transition = transition;
|
|
407
|
+
parent.style.transform = '';
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const computeTargetY = () => {
|
|
411
|
+
const parentSnap = parseFloat(parent.style.getPropertyValue('--snap-point-height')) || 0;
|
|
412
|
+
let targetY = parentSnap;
|
|
413
|
+
for (const d of descendants) {
|
|
414
|
+
const node = d.nodeRef.current;
|
|
415
|
+
if (!node) continue;
|
|
416
|
+
const snap = parseFloat(node.style.getPropertyValue('--snap-point-height')) || 0;
|
|
417
|
+
if (snap > targetY) targetY = snap;
|
|
418
|
+
}
|
|
419
|
+
return targetY;
|
|
420
|
+
};
|
|
421
|
+
const apply = () => {
|
|
422
|
+
const targetY = computeTargetY();
|
|
423
|
+
const match = parent.style.transform.match(/translate3d\([^,]*,\s*(-?\d*\.?\d+)px/);
|
|
424
|
+
const currentY = match ? parseFloat(match[1]) : 0;
|
|
425
|
+
if (Math.abs(currentY - targetY) < 0.5) return;
|
|
426
|
+
parent.style.transition = transition;
|
|
427
|
+
parent.style.transform = `translate3d(0, ${targetY}px, 0)`;
|
|
428
|
+
};
|
|
429
|
+
const raf = requestAnimationFrame(apply);
|
|
430
|
+
// Vaul re-runs snapToPoint on window resize (e.g., mobile keyboard open)
|
|
431
|
+
// which clobbers the cascade transform. Re-apply whenever the parent's
|
|
432
|
+
// inline style changes.
|
|
433
|
+
const observer = new MutationObserver(apply);
|
|
434
|
+
observer.observe(parent, {
|
|
435
|
+
attributes: true,
|
|
436
|
+
attributeFilter: ['style']
|
|
437
|
+
});
|
|
438
|
+
return () => {
|
|
439
|
+
cancelAnimationFrame(raf);
|
|
440
|
+
observer.disconnect();
|
|
441
|
+
};
|
|
442
|
+
}, [descendants, activeSnapPoint]);
|
|
443
|
+
|
|
444
|
+
// Focus/blur events fire when a descendant sheet appears on top of this one
|
|
445
|
+
// (blur) or when all descendants are dismissed (focus). will-events fire
|
|
446
|
+
// synchronously at the transition boundary; did-events fire once the cascade
|
|
447
|
+
// transform drains. Intermediate count changes (1↔2) don't re-fire — this
|
|
448
|
+
// sheet stays blurred throughout.
|
|
449
|
+
const onWillBlurRef = useRef(onWillBlur);
|
|
450
|
+
const onDidBlurRef = useRef(onDidBlur);
|
|
451
|
+
const onWillFocusRef = useRef(onWillFocus);
|
|
452
|
+
const onDidFocusRef = useRef(onDidFocus);
|
|
453
|
+
useEffect(() => {
|
|
454
|
+
onWillBlurRef.current = onWillBlur;
|
|
455
|
+
onDidBlurRef.current = onDidBlur;
|
|
456
|
+
onWillFocusRef.current = onWillFocus;
|
|
457
|
+
onDidFocusRef.current = onDidFocus;
|
|
458
|
+
});
|
|
459
|
+
const prevDescendantCountRef = useRef(0);
|
|
460
|
+
useEffect(() => {
|
|
461
|
+
const prevCount = prevDescendantCountRef.current;
|
|
462
|
+
const count = descendants.length;
|
|
463
|
+
prevDescendantCountRef.current = count;
|
|
464
|
+
if (!isOpen) return;
|
|
465
|
+
const gained = count > 0 && prevCount === 0;
|
|
466
|
+
const lost = count === 0 && prevCount > 0;
|
|
467
|
+
if (!gained && !lost) return;
|
|
468
|
+
if (gained) {
|
|
469
|
+
onWillBlurRef.current?.({
|
|
470
|
+
nativeEvent: null
|
|
471
|
+
});
|
|
472
|
+
} else {
|
|
473
|
+
onWillFocusRef.current?.({
|
|
247
474
|
nativeEvent: null
|
|
248
475
|
});
|
|
249
476
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
477
|
+
const drawer = drawerContentRef.current;
|
|
478
|
+
if (!drawer) return;
|
|
479
|
+
let canceled = false;
|
|
480
|
+
const fireDone = () => {
|
|
481
|
+
if (canceled) return;
|
|
482
|
+
if (gained) onDidBlurRef.current?.({
|
|
483
|
+
nativeEvent: null
|
|
484
|
+
});else onDidFocusRef.current?.({
|
|
253
485
|
nativeEvent: null
|
|
254
486
|
});
|
|
487
|
+
};
|
|
488
|
+
const rafId = window.requestAnimationFrame(() => {
|
|
489
|
+
if (canceled) return;
|
|
490
|
+
// Force style recalc so the cascade effect's queued transform registers.
|
|
255
491
|
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
});
|
|
492
|
+
drawer.offsetHeight;
|
|
493
|
+
const pending = drawer.getAnimations().filter(a => a.playState !== 'finished');
|
|
494
|
+
if (pending.length === 0) {
|
|
495
|
+
fireDone();
|
|
496
|
+
return;
|
|
262
497
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (!dimmed) {
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// When not dismissible, collapse to below dimmed index instead of dismissing
|
|
271
|
-
const pressBehavior = dismissible ? 'close' : dimmedDetentIndex > 0 ? dimmedDetentIndex - 1 : 'none';
|
|
272
|
-
return /*#__PURE__*/_jsx(BottomSheetBackdrop, {
|
|
273
|
-
...backdropProps,
|
|
274
|
-
opacity: 0.5,
|
|
275
|
-
appearsOnIndex: dimmedDetentIndex,
|
|
276
|
-
disappearsOnIndex: dimmedDetentIndex - 1,
|
|
277
|
-
pressBehavior: pressBehavior
|
|
498
|
+
Promise.allSettled(pending.map(a => a.finished)).then(() => {
|
|
499
|
+
if (!canceled) fireDone();
|
|
500
|
+
});
|
|
278
501
|
});
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
502
|
+
return () => {
|
|
503
|
+
canceled = true;
|
|
504
|
+
window.cancelAnimationFrame(rafId);
|
|
505
|
+
};
|
|
506
|
+
}, [isOpen, descendants.length]);
|
|
507
|
+
const effectiveCornerRadius = cornerRadius ?? DEFAULT_CORNER_RADIUS;
|
|
508
|
+
|
|
509
|
+
// Shadow cast upward from the sheet's top edge toward the background. Matches
|
|
510
|
+
// Android's `elevation` semantics roughly — the sheet "lifts" off whatever is
|
|
511
|
+
// behind it. Scales linearly so higher elevation reads as more separation.
|
|
512
|
+
const boxShadow = elevation > 0 ? `0 ${-elevation}px ${elevation * 3}px rgba(0, 0, 0, 0.15)` : undefined;
|
|
513
|
+
const mergedContentStyle = useMemo(() => ({
|
|
514
|
+
position: 'fixed',
|
|
515
|
+
top: 0,
|
|
516
|
+
left: 0,
|
|
517
|
+
right: 0,
|
|
518
|
+
bottom: 0,
|
|
519
|
+
display: 'flex',
|
|
520
|
+
flexDirection: 'column',
|
|
521
|
+
borderTopLeftRadius: effectiveCornerRadius,
|
|
522
|
+
borderTopRightRadius: effectiveCornerRadius,
|
|
523
|
+
backgroundColor: backgroundColor,
|
|
524
|
+
boxShadow,
|
|
525
|
+
// Lift content above iOS home indicator / bottom safe area when enabled.
|
|
526
|
+
paddingBottom: insetAdjustment === 'automatic' ? 'env(safe-area-inset-bottom, 0px)' : 0
|
|
527
|
+
}), [backgroundColor, effectiveCornerRadius, boxShadow, insetAdjustment]);
|
|
286
528
|
const defaultGrabberColor = colorScheme === 'dark' ? DEFAULT_GRABBER_COLOR_DARK : DEFAULT_GRABBER_COLOR_LIGHT;
|
|
287
|
-
const
|
|
288
|
-
height: indicatorHeight,
|
|
289
|
-
borderRadius: grabberOptions?.cornerRadius ?? indicatorHeight / 2,
|
|
290
|
-
width: grabberOptions?.width ?? DEFAULT_GRABBER_WIDTH,
|
|
291
|
-
backgroundColor: grabberOptions?.color ?? defaultGrabberColor
|
|
292
|
-
}), [grabberOptions, indicatorHeight, defaultGrabberColor]);
|
|
293
|
-
const footerComponent = useMemo(() => footer ? footerProps => /*#__PURE__*/_jsx(BottomSheetFooter, {
|
|
294
|
-
style: StyleSheet.flatten([styles.footer, footerStyle]),
|
|
295
|
-
...footerProps,
|
|
296
|
-
children: renderSlot(footer)
|
|
297
|
-
}) : undefined, [footer, footerStyle]);
|
|
529
|
+
const grabberHeight = grabberOptions?.height ?? DEFAULT_GRABBER_HEIGHT;
|
|
298
530
|
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
bottomSheetModalRef.current?.present();
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
},
|
|
315
|
-
dismiss: () => {
|
|
316
|
-
return new Promise(resolve => {
|
|
317
|
-
dismissResolver.current = resolve;
|
|
318
|
-
isDismissing.current = true;
|
|
319
|
-
if (isNonModal) {
|
|
320
|
-
bottomSheetRef.current?.close();
|
|
321
|
-
} else {
|
|
322
|
-
bottomSheetModalRef.current?.dismiss();
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
},
|
|
326
|
-
dismissStack: () => {
|
|
327
|
-
return new Promise(resolve => {
|
|
328
|
-
// Dismiss only sheets above, keeping this sheet presented
|
|
329
|
-
const sheetsAbove = bottomSheetContext?.getSheetsAbove(sheetName) ?? [];
|
|
330
|
-
const immediateChild = sheetsAbove[sheetsAbove.length - 1];
|
|
331
|
-
if (immediateChild) {
|
|
332
|
-
// Dismiss the immediate child - gorhom will dismiss all sheets above it
|
|
333
|
-
bottomSheetContext?.dismiss(immediateChild).then(resolve);
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
resolve();
|
|
337
|
-
});
|
|
338
|
-
},
|
|
339
|
-
resize: async index => {
|
|
340
|
-
if (isNonModal) {
|
|
341
|
-
bottomSheetRef.current?.snapToIndex(index);
|
|
342
|
-
} else {
|
|
343
|
-
bottomSheetModalRef.current?.snapToIndex(index);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
|
-
useImperativeHandle(ref, () => sheetMethodsRef.current);
|
|
531
|
+
// Footer is rendered inside the wrapper via `detachedSiblings`, so it
|
|
532
|
+
// follows the wrapper on dismiss and drag-overshoot. Positioning is
|
|
533
|
+
// relative to the wrapper (contain: paint creates the containing block).
|
|
534
|
+
const footerFloatStyle = useMemo(() => ({
|
|
535
|
+
position: 'fixed',
|
|
536
|
+
left: 0,
|
|
537
|
+
right: 0,
|
|
538
|
+
bottom: 0,
|
|
539
|
+
// Wrapper has `pointer-events: none` to let clicks fall through; the
|
|
540
|
+
// footer must opt back in.
|
|
541
|
+
pointerEvents: 'auto'
|
|
542
|
+
}), []);
|
|
348
543
|
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
544
|
+
// Form-sheet style (iOS pageSizing=false): centered floating card with a
|
|
545
|
+
// default width and a height capped to a fraction of the window. We reuse
|
|
546
|
+
// the existing detached mechanic so drag/snap math stays correct — the
|
|
547
|
+
// wrapper is bottom-attached with a computed offset that centers it
|
|
548
|
+
// vertically.
|
|
549
|
+
const isFormSheet = isLandscapeOrTablet && maxContentWidth == null && !pageSizing;
|
|
550
|
+
const effectiveMaxContentHeight = maxContentHeight ?? (isFormSheet ? windowHeight * DEFAULT_FORM_SHEET_HEIGHT_RATIO : undefined);
|
|
551
|
+
const effectiveDetached = isFormSheet || detached;
|
|
552
|
+
const effectiveDetachedOffset = isFormSheet ? Math.max(0, (windowHeight - (effectiveMaxContentHeight ?? 0)) / 2) : detachedOffset;
|
|
356
553
|
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
554
|
+
// The wrapper holds all horizontal sizing/anchoring so its rounded-bottom
|
|
555
|
+
// clip (when detached) aligns with the drawer's horizontal bounds on
|
|
556
|
+
// desktop — otherwise its corners sit at the far viewport edges.
|
|
557
|
+
// Mirrors iOS setupSheetSizing: maxContentWidth wins (forces pageSizing
|
|
558
|
+
// off). pageSizing on → constrain to readable width (page-sheet);
|
|
559
|
+
// pageSizing off with no maxContentWidth → form-sheet width.
|
|
560
|
+
// Detached without a width constraint applies anchorOffset on both edges so
|
|
561
|
+
// the floating card breathes from the viewport sides.
|
|
562
|
+
const wrapperStyle = useMemo(() => {
|
|
563
|
+
const maxWidth = isLandscapeOrTablet ? isFormSheet ? DEFAULT_FORM_SHEET_WIDTH : maxContentWidth ?? (pageSizing ? DEFAULT_MAX_WIDTH : undefined) : undefined;
|
|
564
|
+
if (maxWidth == null && !detached) return undefined;
|
|
565
|
+
let marginLeft;
|
|
566
|
+
let marginRight;
|
|
567
|
+
if (isFormSheet) {
|
|
568
|
+
marginLeft = 'auto';
|
|
569
|
+
marginRight = 'auto';
|
|
570
|
+
} else if (maxWidth == null) {
|
|
571
|
+
marginLeft = anchorOffset;
|
|
572
|
+
marginRight = anchorOffset;
|
|
573
|
+
} else {
|
|
574
|
+
marginLeft = anchor === 'left' ? anchorOffset : 'auto';
|
|
575
|
+
marginRight = anchor === 'right' ? anchorOffset : 'auto';
|
|
361
576
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
577
|
+
return {
|
|
578
|
+
...(maxWidth != null && {
|
|
579
|
+
maxWidth
|
|
580
|
+
}),
|
|
581
|
+
marginLeft,
|
|
582
|
+
marginRight
|
|
583
|
+
};
|
|
584
|
+
}, [isLandscapeOrTablet, isFormSheet, maxContentWidth, pageSizing, anchor, anchorOffset, detached]);
|
|
585
|
+
const handleStyle = useMemo(() => ({
|
|
586
|
+
height: grabberHeight,
|
|
587
|
+
width: grabberOptions?.width ?? DEFAULT_GRABBER_WIDTH,
|
|
588
|
+
borderRadius: grabberOptions?.cornerRadius ?? grabberHeight / 2,
|
|
589
|
+
backgroundColor: grabberOptions?.color ?? defaultGrabberColor,
|
|
590
|
+
opacity: 1,
|
|
591
|
+
marginTop: grabberOptions?.topMargin ?? DEFAULT_GRABBER_TOP_MARGIN
|
|
592
|
+
}), [grabberOptions, grabberHeight, defaultGrabberColor]);
|
|
593
|
+
return /*#__PURE__*/_jsx(Drawer.Root, {
|
|
594
|
+
open: isOpen,
|
|
595
|
+
onOpenChange: handleOpenChange,
|
|
596
|
+
onPositionChange: handlePositionChange,
|
|
597
|
+
onDrag: handleDrag,
|
|
598
|
+
onRelease: handleRelease,
|
|
599
|
+
dismissible: dismissible,
|
|
600
|
+
draggable: draggable,
|
|
601
|
+
handleOnly: scrollable && scrollableOptions?.scrollingExpandsSheet === false,
|
|
602
|
+
repositionInputs: false,
|
|
603
|
+
modal: dimmed,
|
|
604
|
+
nested: isNested,
|
|
605
|
+
detached: effectiveDetached,
|
|
606
|
+
detachedOffset: effectiveDetachedOffset,
|
|
607
|
+
detachedRadius: effectiveCornerRadius,
|
|
608
|
+
maxContentHeight: effectiveMaxContentHeight,
|
|
609
|
+
initialAnimated: initialDetentAnimated,
|
|
610
|
+
detachedWrapperStyle: wrapperStyle,
|
|
611
|
+
activeSnapPoint: activeSnapPoint,
|
|
612
|
+
setActiveSnapPoint: handleSetActiveSnapPoint,
|
|
613
|
+
...snapPointsProps,
|
|
614
|
+
children: /*#__PURE__*/_jsxs(Drawer.Portal, {
|
|
615
|
+
container: portalContainer ?? undefined,
|
|
616
|
+
children: [/*#__PURE__*/_jsx(Drawer.Overlay, {
|
|
617
|
+
style: overlayStyle
|
|
618
|
+
}), /*#__PURE__*/_jsxs(Drawer.Content, {
|
|
619
|
+
ref: drawerContentRef,
|
|
620
|
+
style: mergedContentStyle,
|
|
621
|
+
onPointerDownOutside: handlePointerDownOutside,
|
|
622
|
+
detachedSiblings: footer ? /*#__PURE__*/_jsx("div", {
|
|
623
|
+
style: footerFloatStyle,
|
|
624
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
625
|
+
style: footerStyle,
|
|
626
|
+
children: /*#__PURE__*/isValidElement(footer) ? footer : /*#__PURE__*/createElement(footer)
|
|
627
|
+
})
|
|
628
|
+
}) : undefined,
|
|
629
|
+
children: [/*#__PURE__*/_jsx(Drawer.Title, {
|
|
630
|
+
style: visuallyHiddenStyle,
|
|
631
|
+
children: "Sheet"
|
|
632
|
+
}), grabber && /*#__PURE__*/_jsx(Drawer.Handle, {
|
|
633
|
+
style: handleStyle
|
|
634
|
+
}), header && /*#__PURE__*/_jsx(View, {
|
|
635
|
+
style: headerStyle,
|
|
636
|
+
children: /*#__PURE__*/isValidElement(header) ? header : /*#__PURE__*/createElement(header)
|
|
637
|
+
}), scrollable ? /*#__PURE__*/_jsx("div", {
|
|
638
|
+
style: scrollableContainerStyle,
|
|
639
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
640
|
+
style: style,
|
|
641
|
+
children: children
|
|
642
|
+
})
|
|
643
|
+
}) : /*#__PURE__*/_jsx(View, {
|
|
644
|
+
style: style,
|
|
645
|
+
children: children
|
|
646
|
+
})]
|
|
647
|
+
})]
|
|
648
|
+
})
|
|
420
649
|
});
|
|
421
650
|
});
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
651
|
+
const overlayStyle = {
|
|
652
|
+
position: 'fixed',
|
|
653
|
+
inset: 0,
|
|
654
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)'
|
|
426
655
|
};
|
|
427
|
-
|
|
428
|
-
|
|
656
|
+
const scrollableContainerStyle = {
|
|
657
|
+
flex: 1,
|
|
658
|
+
minHeight: 0,
|
|
659
|
+
overflowY: 'auto',
|
|
660
|
+
overscrollBehavior: 'contain',
|
|
661
|
+
touchAction: 'pan-y'
|
|
429
662
|
};
|
|
430
|
-
|
|
431
|
-
|
|
663
|
+
const visuallyHiddenStyle = {
|
|
664
|
+
position: 'absolute',
|
|
665
|
+
width: 1,
|
|
666
|
+
height: 1,
|
|
667
|
+
padding: 0,
|
|
668
|
+
margin: -1,
|
|
669
|
+
overflow: 'hidden',
|
|
670
|
+
clip: 'rect(0, 0, 0, 0)',
|
|
671
|
+
whiteSpace: 'nowrap',
|
|
672
|
+
border: 0
|
|
432
673
|
};
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
TrueSheet.dismissAll = async () => {
|
|
674
|
+
const STATIC_METHOD_ERROR = 'Static methods are not supported on web. Use the useTrueSheet() hook instead.';
|
|
675
|
+
export const TrueSheet = TrueSheetComponent;
|
|
676
|
+
const rejectStatic = async () => {
|
|
437
677
|
throw new Error(STATIC_METHOD_ERROR);
|
|
438
678
|
};
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
position: 'absolute',
|
|
445
|
-
top: 0,
|
|
446
|
-
left: 0,
|
|
447
|
-
right: 0,
|
|
448
|
-
zIndex: 999,
|
|
449
|
-
paddingVertical: 10,
|
|
450
|
-
pointerEvents: 'none'
|
|
451
|
-
},
|
|
452
|
-
footer: {
|
|
453
|
-
pointerEvents: 'box-none'
|
|
454
|
-
}
|
|
455
|
-
});
|
|
679
|
+
TrueSheet.present = rejectStatic;
|
|
680
|
+
TrueSheet.dismiss = rejectStatic;
|
|
681
|
+
TrueSheet.dismissStack = rejectStatic;
|
|
682
|
+
TrueSheet.resize = rejectStatic;
|
|
683
|
+
TrueSheet.dismissAll = rejectStatic;
|
|
456
684
|
//# sourceMappingURL=TrueSheet.web.js.map
|