@lodev09/react-native-true-sheet 3.2.2 → 3.3.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.js.map +1 -1
- package/lib/module/TrueSheet.web.js +348 -0
- package/lib/module/TrueSheet.web.js.map +1 -0
- package/lib/module/TrueSheetProvider.js +26 -0
- package/lib/module/TrueSheetProvider.js.map +1 -0
- package/lib/module/TrueSheetProvider.web.js +74 -0
- package/lib/module/TrueSheetProvider.web.js.map +1 -0
- package/lib/module/index.js +2 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js +1 -1
- package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js.map +1 -1
- package/lib/module/navigation/screen/TrueSheetScreen.js +1 -1
- package/lib/module/navigation/screen/TrueSheetScreen.js.map +1 -1
- package/lib/module/reanimated/ReanimatedTrueSheet.js +2 -2
- package/lib/module/reanimated/ReanimatedTrueSheet.js.map +1 -1
- package/lib/module/reanimated/ReanimatedTrueSheet.web.js +81 -0
- package/lib/module/reanimated/ReanimatedTrueSheet.web.js.map +1 -0
- package/lib/module/reanimated/index.js +2 -2
- package/lib/module/reanimated/index.js.map +1 -1
- package/lib/module/reanimated/useReanimatedPositionChangeHandler.web.js +21 -0
- package/lib/module/reanimated/useReanimatedPositionChangeHandler.web.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 +72 -0
- package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
- package/lib/typescript/src/TrueSheet.web.d.ts +3 -0
- package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetProvider.d.ts +17 -0
- package/lib/typescript/src/TrueSheetProvider.d.ts.map +1 -0
- package/lib/typescript/src/TrueSheetProvider.web.d.ts +22 -0
- package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/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 +41 -0
- package/lib/typescript/src/reanimated/ReanimatedTrueSheet.web.d.ts.map +1 -0
- package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.web.d.ts +16 -0
- package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.web.d.ts.map +1 -0
- package/package.json +26 -13
- package/src/TrueSheet.tsx +5 -1
- package/src/TrueSheet.types.ts +76 -0
- package/src/TrueSheet.web.tsx +423 -0
- package/src/TrueSheetProvider.tsx +29 -0
- package/src/TrueSheetProvider.web.tsx +81 -0
- package/src/index.ts +1 -0
- package/src/navigation/types.ts +2 -0
- package/src/reanimated/ReanimatedTrueSheet.web.tsx +78 -0
- package/src/reanimated/useReanimatedPositionChangeHandler.web.ts +32 -0
package/src/TrueSheet.types.ts
CHANGED
|
@@ -40,6 +40,52 @@ export type WillFocusEvent = NativeSyntheticEvent<null>;
|
|
|
40
40
|
export type WillBlurEvent = NativeSyntheticEvent<null>;
|
|
41
41
|
export type BackPressEvent = NativeSyntheticEvent<null>;
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Ref methods exposed by a TrueSheet instance.
|
|
45
|
+
*/
|
|
46
|
+
export interface TrueSheetRef {
|
|
47
|
+
/**
|
|
48
|
+
* Present the sheet at a given detent index.
|
|
49
|
+
* @param index - The detent index to present at (default: 0)
|
|
50
|
+
* @param animated - Whether to animate the presentation (default: true)
|
|
51
|
+
*/
|
|
52
|
+
present: (index?: number, animated?: boolean) => Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Dismiss the sheet.
|
|
55
|
+
* @param animated - Whether to animate the dismissal (default: true)
|
|
56
|
+
*/
|
|
57
|
+
dismiss: (animated?: boolean) => Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Resize the sheet to a given detent index.
|
|
60
|
+
* @param index - The detent index to resize to
|
|
61
|
+
*/
|
|
62
|
+
resize: (index: number) => Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Methods for controlling TrueSheet instances by name.
|
|
67
|
+
* Returned by the `useTrueSheet` hook.
|
|
68
|
+
*/
|
|
69
|
+
export interface TrueSheetContextMethods {
|
|
70
|
+
/**
|
|
71
|
+
* Present a sheet by name.
|
|
72
|
+
* @param name - The name of the sheet to present
|
|
73
|
+
* @param index - The detent index to present at (default: 0)
|
|
74
|
+
*/
|
|
75
|
+
present: (name: string, index?: number) => Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Dismiss a sheet by name.
|
|
78
|
+
* @param name - The name of the sheet to dismiss
|
|
79
|
+
*/
|
|
80
|
+
dismiss: (name: string) => Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* Resize a sheet by name.
|
|
83
|
+
* @param name - The name of the sheet to resize
|
|
84
|
+
* @param index - The detent index to resize to
|
|
85
|
+
*/
|
|
86
|
+
resize: (name: string, index: number) => Promise<void>;
|
|
87
|
+
}
|
|
88
|
+
|
|
43
89
|
/**
|
|
44
90
|
* Options for customizing the grabber (drag handle) appearance.
|
|
45
91
|
*/
|
|
@@ -97,6 +143,25 @@ export interface BlurOptions {
|
|
|
97
143
|
interaction?: boolean;
|
|
98
144
|
}
|
|
99
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Defines the stack behavior when a modal is presented on web.
|
|
148
|
+
*
|
|
149
|
+
* @platform web
|
|
150
|
+
*/
|
|
151
|
+
export type StackBehavior =
|
|
152
|
+
/**
|
|
153
|
+
* Mount the modal on top of the current one.
|
|
154
|
+
*/
|
|
155
|
+
| 'push'
|
|
156
|
+
/**
|
|
157
|
+
* Minimize the current modal then mount the new one.
|
|
158
|
+
*/
|
|
159
|
+
| 'switch'
|
|
160
|
+
/**
|
|
161
|
+
* Dismiss the current modal then mount the new one.
|
|
162
|
+
*/
|
|
163
|
+
| 'replace';
|
|
164
|
+
|
|
100
165
|
/**
|
|
101
166
|
* Inset adjustment behavior for the sheet content.
|
|
102
167
|
*/
|
|
@@ -355,6 +420,17 @@ export interface TrueSheetProps extends ViewProps {
|
|
|
355
420
|
*/
|
|
356
421
|
keyboardMode?: 'resize' | 'pan';
|
|
357
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Defines the stack behavior when a modal is presented.
|
|
425
|
+
* - `push`: Mount the modal on top of the current one.
|
|
426
|
+
* - `switch`: Minimize the current modal then mount the new one.
|
|
427
|
+
* - `replace`: Dismiss the current modal then mount the new one.
|
|
428
|
+
*
|
|
429
|
+
* @platform web
|
|
430
|
+
* @default 'switch'
|
|
431
|
+
*/
|
|
432
|
+
stackBehavior?: StackBehavior;
|
|
433
|
+
|
|
358
434
|
/**
|
|
359
435
|
* Called when the sheet's content is mounted and ready.
|
|
360
436
|
* The sheet automatically waits for this event before presenting.
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createElement,
|
|
3
|
+
Fragment,
|
|
4
|
+
forwardRef,
|
|
5
|
+
isValidElement,
|
|
6
|
+
useCallback,
|
|
7
|
+
useContext,
|
|
8
|
+
useEffect,
|
|
9
|
+
useId,
|
|
10
|
+
useImperativeHandle,
|
|
11
|
+
useMemo,
|
|
12
|
+
useRef,
|
|
13
|
+
useState,
|
|
14
|
+
} from 'react';
|
|
15
|
+
import { View, StyleSheet, useWindowDimensions } from 'react-native';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
BottomSheetBackdrop,
|
|
19
|
+
type BottomSheetBackdropProps,
|
|
20
|
+
BottomSheetFooter,
|
|
21
|
+
type BottomSheetFooterProps,
|
|
22
|
+
BottomSheetHandle,
|
|
23
|
+
type BottomSheetHandleProps,
|
|
24
|
+
BottomSheetModal,
|
|
25
|
+
BottomSheetView,
|
|
26
|
+
type SNAP_POINT_TYPE,
|
|
27
|
+
} from '@gorhom/bottom-sheet';
|
|
28
|
+
import { useDerivedValue, useSharedValue } from 'react-native-reanimated';
|
|
29
|
+
|
|
30
|
+
import { BottomSheetContext } from './TrueSheetProvider.web';
|
|
31
|
+
import type {
|
|
32
|
+
TrueSheetProps,
|
|
33
|
+
TrueSheetRef,
|
|
34
|
+
DetentChangeEvent,
|
|
35
|
+
DidBlurEvent,
|
|
36
|
+
DidDismissEvent,
|
|
37
|
+
DidFocusEvent,
|
|
38
|
+
DidPresentEvent,
|
|
39
|
+
MountEvent,
|
|
40
|
+
PositionChangeEvent,
|
|
41
|
+
WillBlurEvent,
|
|
42
|
+
WillDismissEvent,
|
|
43
|
+
WillFocusEvent,
|
|
44
|
+
WillPresentEvent,
|
|
45
|
+
DragBeginEvent,
|
|
46
|
+
DragChangeEvent,
|
|
47
|
+
DragEndEvent,
|
|
48
|
+
} from './TrueSheet.types';
|
|
49
|
+
|
|
50
|
+
const DEFAULT_CORNER_RADIUS = 16;
|
|
51
|
+
|
|
52
|
+
const DEFAULT_GRABBER_COLOR = 'rgba(0, 0, 0, 0.3)';
|
|
53
|
+
const DEFAULT_GRABBER_WIDTH = 32;
|
|
54
|
+
const DEFAULT_GRABBER_HEIGHT = 4;
|
|
55
|
+
|
|
56
|
+
const renderSlot = (slot: TrueSheetProps['header'] | TrueSheetProps['footer']) => {
|
|
57
|
+
if (!slot) return null;
|
|
58
|
+
if (isValidElement(slot)) return slot;
|
|
59
|
+
return createElement(slot);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const TrueSheet = forwardRef<TrueSheetRef, TrueSheetProps>((props, ref) => {
|
|
63
|
+
const {
|
|
64
|
+
name,
|
|
65
|
+
detents = [0.5, 1],
|
|
66
|
+
dismissible = true,
|
|
67
|
+
draggable = true,
|
|
68
|
+
dimmed = true,
|
|
69
|
+
dimmedDetentIndex = 0,
|
|
70
|
+
children,
|
|
71
|
+
scrollable = false,
|
|
72
|
+
initialDetentIndex = -1,
|
|
73
|
+
backgroundColor = '#ffffff',
|
|
74
|
+
cornerRadius = DEFAULT_CORNER_RADIUS,
|
|
75
|
+
grabber = true,
|
|
76
|
+
grabberOptions,
|
|
77
|
+
maxHeight,
|
|
78
|
+
header,
|
|
79
|
+
footer,
|
|
80
|
+
onMount,
|
|
81
|
+
onWillPresent,
|
|
82
|
+
onDidPresent,
|
|
83
|
+
onWillDismiss,
|
|
84
|
+
onDidDismiss,
|
|
85
|
+
onDetentChange,
|
|
86
|
+
onPositionChange,
|
|
87
|
+
onDragBegin,
|
|
88
|
+
onDragChange,
|
|
89
|
+
onDragEnd,
|
|
90
|
+
onWillFocus,
|
|
91
|
+
onDidFocus,
|
|
92
|
+
onWillBlur,
|
|
93
|
+
onDidBlur,
|
|
94
|
+
stackBehavior = 'switch',
|
|
95
|
+
style,
|
|
96
|
+
} = props;
|
|
97
|
+
|
|
98
|
+
const { height: windowHeight } = useWindowDimensions();
|
|
99
|
+
const defaultName = useId();
|
|
100
|
+
const sheetName = name ?? defaultName;
|
|
101
|
+
const bottomSheetContext = useContext(BottomSheetContext);
|
|
102
|
+
const modalRef = useRef<BottomSheetModal>(null);
|
|
103
|
+
const initialDetentIndexRef = useRef(initialDetentIndex);
|
|
104
|
+
const currentIndexRef = useRef(0);
|
|
105
|
+
const isPresenting = useRef(false);
|
|
106
|
+
const isDismissing = useRef(false);
|
|
107
|
+
const isMinimized = useRef(false);
|
|
108
|
+
const isDragging = useRef(false);
|
|
109
|
+
|
|
110
|
+
const presentResolver = useRef<(() => void) | null>(null);
|
|
111
|
+
const dismissResolver = useRef<(() => void) | null>(null);
|
|
112
|
+
|
|
113
|
+
const animatedPosition = useSharedValue(windowHeight);
|
|
114
|
+
const animatedIndex = useSharedValue(0);
|
|
115
|
+
|
|
116
|
+
const [snapIndex, setSnapIndex] = useState(initialDetentIndex);
|
|
117
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
118
|
+
|
|
119
|
+
useDerivedValue(() => {
|
|
120
|
+
onPositionChange?.({
|
|
121
|
+
nativeEvent: {
|
|
122
|
+
position: animatedPosition.value,
|
|
123
|
+
index: animatedIndex.value,
|
|
124
|
+
detent: detents[animatedIndex.value] ?? 0,
|
|
125
|
+
realtime: true,
|
|
126
|
+
},
|
|
127
|
+
} as PositionChangeEvent);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const hasAutoDetent = detents.includes('auto');
|
|
131
|
+
|
|
132
|
+
const containerHeight = maxHeight ?? windowHeight;
|
|
133
|
+
const snapPoints = useMemo(
|
|
134
|
+
() =>
|
|
135
|
+
detents
|
|
136
|
+
.filter((detent): detent is number => detent !== 'auto' && typeof detent === 'number')
|
|
137
|
+
.map((detent) => Math.min(1, Math.max(0.1, detent)) * containerHeight),
|
|
138
|
+
[detents, containerHeight]
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const handleChange = useCallback(
|
|
142
|
+
(index: number, _position: number, _type: SNAP_POINT_TYPE) => {
|
|
143
|
+
const previousIndex = currentIndexRef.current;
|
|
144
|
+
currentIndexRef.current = index;
|
|
145
|
+
|
|
146
|
+
// Handle drag end
|
|
147
|
+
if (isDragging.current && !isPresenting.current) {
|
|
148
|
+
isDragging.current = false;
|
|
149
|
+
onDragEnd?.({
|
|
150
|
+
nativeEvent: {
|
|
151
|
+
index,
|
|
152
|
+
position: animatedPosition.value,
|
|
153
|
+
detent: detents[index] ?? 0,
|
|
154
|
+
},
|
|
155
|
+
} as DragEndEvent);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!isPresenting.current && !isMinimized.current && previousIndex !== index && index >= 0) {
|
|
159
|
+
onDetentChange?.({
|
|
160
|
+
nativeEvent: {
|
|
161
|
+
index,
|
|
162
|
+
position: animatedPosition.value,
|
|
163
|
+
detent: detents[index] ?? 0,
|
|
164
|
+
},
|
|
165
|
+
} as DetentChangeEvent);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (isPresenting.current) {
|
|
169
|
+
isPresenting.current = false;
|
|
170
|
+
|
|
171
|
+
// Resolve present promise
|
|
172
|
+
if (presentResolver.current) {
|
|
173
|
+
presentResolver.current();
|
|
174
|
+
presentResolver.current = null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
onDidPresent?.({
|
|
178
|
+
nativeEvent: {
|
|
179
|
+
index,
|
|
180
|
+
position: animatedPosition.value,
|
|
181
|
+
detent: detents[index] ?? 0,
|
|
182
|
+
},
|
|
183
|
+
} as DidPresentEvent);
|
|
184
|
+
|
|
185
|
+
onDidFocus?.({ nativeEvent: null } as DidFocusEvent);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Fire onDidBlur when sheet reaches minimized state (index -1 but still mounted)
|
|
189
|
+
if (isMinimized.current && index === -1) {
|
|
190
|
+
onDidBlur?.({ nativeEvent: null } as DidBlurEvent);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Fire onDidFocus when sheet is restored from minimized state
|
|
194
|
+
if (isMinimized.current && index >= 0) {
|
|
195
|
+
isMinimized.current = false;
|
|
196
|
+
onDidFocus?.({ nativeEvent: null } as DidFocusEvent);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
[detents, animatedPosition]
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const handleDismiss = useCallback(() => {
|
|
203
|
+
// Resolve dismiss promise
|
|
204
|
+
if (dismissResolver.current) {
|
|
205
|
+
dismissResolver.current();
|
|
206
|
+
dismissResolver.current = null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
onDidDismiss?.({ nativeEvent: null } as DidDismissEvent);
|
|
210
|
+
|
|
211
|
+
// Reset states since sheet is being dismissed
|
|
212
|
+
isMinimized.current = false;
|
|
213
|
+
isDismissing.current = false;
|
|
214
|
+
isDragging.current = false;
|
|
215
|
+
}, []);
|
|
216
|
+
|
|
217
|
+
const handleAnimate = useCallback(
|
|
218
|
+
(_fromIndex: number, toIndex: number) => {
|
|
219
|
+
// Detect drag begin (when not presenting or dismissing)
|
|
220
|
+
if (!isPresenting.current && !isDismissing.current && !isDragging.current && toIndex >= 0) {
|
|
221
|
+
isDragging.current = true;
|
|
222
|
+
onDragBegin?.({
|
|
223
|
+
nativeEvent: {
|
|
224
|
+
index: currentIndexRef.current,
|
|
225
|
+
position: animatedPosition.value,
|
|
226
|
+
detent: detents[currentIndexRef.current] ?? 0,
|
|
227
|
+
},
|
|
228
|
+
} as DragBeginEvent);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Drag change during animation
|
|
232
|
+
if (isDragging.current && toIndex >= 0) {
|
|
233
|
+
onDragChange?.({
|
|
234
|
+
nativeEvent: {
|
|
235
|
+
index: toIndex,
|
|
236
|
+
position: animatedPosition.value,
|
|
237
|
+
detent: detents[toIndex] ?? 0,
|
|
238
|
+
},
|
|
239
|
+
} as DragChangeEvent);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (isPresenting.current) {
|
|
243
|
+
onWillPresent?.({
|
|
244
|
+
nativeEvent: {
|
|
245
|
+
index: toIndex,
|
|
246
|
+
position: animatedPosition.value,
|
|
247
|
+
detent: detents[toIndex] ?? 0,
|
|
248
|
+
},
|
|
249
|
+
} as WillPresentEvent);
|
|
250
|
+
|
|
251
|
+
// Focus events fire together with present events
|
|
252
|
+
onWillFocus?.({ nativeEvent: null } as WillFocusEvent);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Detect if sheet is being restored (will focus)
|
|
256
|
+
if (isMinimized.current && toIndex >= 0) {
|
|
257
|
+
onWillFocus?.({ nativeEvent: null } as WillFocusEvent);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (toIndex === -1 && !isPresenting.current) {
|
|
261
|
+
// Will be handled as blur if the sheet doesn't actually dismiss
|
|
262
|
+
isMinimized.current = true;
|
|
263
|
+
onWillBlur?.({ nativeEvent: null } as WillBlurEvent);
|
|
264
|
+
|
|
265
|
+
if (isDismissing.current) {
|
|
266
|
+
onWillDismiss?.({ nativeEvent: null } as WillDismissEvent);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
[detents, animatedPosition]
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const backdropComponent = useCallback(
|
|
274
|
+
(backdropProps: BottomSheetBackdropProps) => {
|
|
275
|
+
if (!dimmed) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
return (
|
|
279
|
+
<BottomSheetBackdrop
|
|
280
|
+
{...backdropProps}
|
|
281
|
+
opacity={0.5}
|
|
282
|
+
appearsOnIndex={dimmedDetentIndex}
|
|
283
|
+
disappearsOnIndex={dimmedDetentIndex - 1}
|
|
284
|
+
pressBehavior={dismissible ? 'close' : 'none'}
|
|
285
|
+
/>
|
|
286
|
+
);
|
|
287
|
+
},
|
|
288
|
+
[dimmed, dimmedDetentIndex, dismissible]
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const handleComponent = useCallback(
|
|
292
|
+
(handleProps: BottomSheetHandleProps) => {
|
|
293
|
+
if (!grabber) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const height = grabberOptions?.height ?? DEFAULT_GRABBER_HEIGHT;
|
|
298
|
+
const borderRadius = grabberOptions?.cornerRadius ?? height / 2;
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<BottomSheetHandle
|
|
302
|
+
{...handleProps}
|
|
303
|
+
style={[styles.handle, { paddingTop: grabberOptions?.topMargin }]}
|
|
304
|
+
indicatorStyle={{
|
|
305
|
+
height,
|
|
306
|
+
borderRadius,
|
|
307
|
+
width: grabberOptions?.width ?? DEFAULT_GRABBER_WIDTH,
|
|
308
|
+
backgroundColor: grabberOptions?.color ?? DEFAULT_GRABBER_COLOR,
|
|
309
|
+
}}
|
|
310
|
+
/>
|
|
311
|
+
);
|
|
312
|
+
},
|
|
313
|
+
[grabber, grabberOptions]
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const footerComponent = useMemo(
|
|
317
|
+
() =>
|
|
318
|
+
footer
|
|
319
|
+
? (footerProps: BottomSheetFooterProps) => (
|
|
320
|
+
<BottomSheetFooter {...footerProps}>{renderSlot(footer)}</BottomSheetFooter>
|
|
321
|
+
)
|
|
322
|
+
: undefined,
|
|
323
|
+
[footer]
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// For scrollable, we render the child directly
|
|
327
|
+
const ContainerComponent = scrollable ? Fragment : BottomSheetView;
|
|
328
|
+
|
|
329
|
+
const sheetMethodsRef = useRef<TrueSheetRef>({
|
|
330
|
+
present: (index = 0) => {
|
|
331
|
+
return new Promise<void>((resolve) => {
|
|
332
|
+
presentResolver.current = resolve;
|
|
333
|
+
setSnapIndex(index);
|
|
334
|
+
isPresenting.current = true;
|
|
335
|
+
modalRef.current?.present();
|
|
336
|
+
});
|
|
337
|
+
},
|
|
338
|
+
dismiss: () => {
|
|
339
|
+
return new Promise<void>((resolve) => {
|
|
340
|
+
dismissResolver.current = resolve;
|
|
341
|
+
isDismissing.current = true;
|
|
342
|
+
modalRef.current?.dismiss();
|
|
343
|
+
});
|
|
344
|
+
},
|
|
345
|
+
resize: async (index: number) => {
|
|
346
|
+
modalRef.current?.snapToIndex(index);
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
useImperativeHandle(ref, () => sheetMethodsRef.current);
|
|
351
|
+
|
|
352
|
+
// Register with context provider
|
|
353
|
+
useEffect(() => {
|
|
354
|
+
bottomSheetContext?.register(sheetName, sheetMethodsRef);
|
|
355
|
+
return () => {
|
|
356
|
+
bottomSheetContext?.unregister(sheetName);
|
|
357
|
+
};
|
|
358
|
+
}, [sheetName]);
|
|
359
|
+
|
|
360
|
+
// Auto-present on mount if initialDetentIndex is set
|
|
361
|
+
useEffect(() => {
|
|
362
|
+
if (initialDetentIndexRef.current >= 0) {
|
|
363
|
+
sheetMethodsRef.current.present(initialDetentIndexRef.current);
|
|
364
|
+
}
|
|
365
|
+
}, []);
|
|
366
|
+
|
|
367
|
+
// Handle mount event after first render
|
|
368
|
+
useEffect(() => {
|
|
369
|
+
if (!isMounted) {
|
|
370
|
+
setIsMounted(true);
|
|
371
|
+
onMount?.({ nativeEvent: null } as MountEvent);
|
|
372
|
+
}
|
|
373
|
+
}, [isMounted, onMount]);
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<BottomSheetModal
|
|
377
|
+
ref={modalRef}
|
|
378
|
+
name={sheetName}
|
|
379
|
+
style={[
|
|
380
|
+
styles.root,
|
|
381
|
+
{ backgroundColor, borderTopLeftRadius: cornerRadius, borderTopRightRadius: cornerRadius },
|
|
382
|
+
]}
|
|
383
|
+
index={snapIndex}
|
|
384
|
+
animateOnMount
|
|
385
|
+
enablePanDownToClose={dismissible}
|
|
386
|
+
enableContentPanningGesture={draggable}
|
|
387
|
+
enableHandlePanningGesture={draggable}
|
|
388
|
+
animatedPosition={animatedPosition}
|
|
389
|
+
animatedIndex={animatedIndex}
|
|
390
|
+
handleComponent={handleComponent}
|
|
391
|
+
onChange={handleChange}
|
|
392
|
+
onAnimate={handleAnimate}
|
|
393
|
+
enableDynamicSizing={hasAutoDetent}
|
|
394
|
+
maxDynamicContentSize={maxHeight}
|
|
395
|
+
snapPoints={snapPoints.length > 0 ? snapPoints : undefined}
|
|
396
|
+
onDismiss={handleDismiss}
|
|
397
|
+
stackBehavior={stackBehavior}
|
|
398
|
+
backdropComponent={backdropComponent}
|
|
399
|
+
backgroundComponent={null}
|
|
400
|
+
footerComponent={footerComponent}
|
|
401
|
+
>
|
|
402
|
+
<ContainerComponent>
|
|
403
|
+
{renderSlot(header)}
|
|
404
|
+
<View style={style}>{children}</View>
|
|
405
|
+
</ContainerComponent>
|
|
406
|
+
</BottomSheetModal>
|
|
407
|
+
);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const styles = StyleSheet.create({
|
|
411
|
+
root: {
|
|
412
|
+
overflow: 'hidden',
|
|
413
|
+
},
|
|
414
|
+
handle: {
|
|
415
|
+
position: 'absolute',
|
|
416
|
+
top: 0,
|
|
417
|
+
left: 0,
|
|
418
|
+
right: 0,
|
|
419
|
+
zIndex: 999,
|
|
420
|
+
paddingVertical: 10,
|
|
421
|
+
pointerEvents: 'none',
|
|
422
|
+
},
|
|
423
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import { TrueSheet } from './TrueSheet';
|
|
4
|
+
import type { TrueSheetContextMethods } from './TrueSheet.types';
|
|
5
|
+
|
|
6
|
+
export interface TrueSheetProviderProps {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Provider for TrueSheet on native platforms.
|
|
12
|
+
* This is a pass-through component - no context is needed on native
|
|
13
|
+
* since TrueSheet uses static instance methods internally.
|
|
14
|
+
*/
|
|
15
|
+
export function TrueSheetProvider({ children }: TrueSheetProviderProps) {
|
|
16
|
+
return children;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hook to control TrueSheet instances by name.
|
|
21
|
+
* On native, this maps directly to TrueSheet static methods.
|
|
22
|
+
*/
|
|
23
|
+
export function useTrueSheet(): TrueSheetContextMethods {
|
|
24
|
+
return {
|
|
25
|
+
present: TrueSheet.present,
|
|
26
|
+
dismiss: TrueSheet.dismiss,
|
|
27
|
+
resize: TrueSheet.resize,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createContext, useContext, useRef, type ReactNode, type RefObject } from 'react';
|
|
2
|
+
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
|
3
|
+
import type { TrueSheetContextMethods, TrueSheetRef } from './TrueSheet.types';
|
|
4
|
+
|
|
5
|
+
interface BottomSheetContextValue extends TrueSheetContextMethods {
|
|
6
|
+
register: (name: string, methods: RefObject<TrueSheetRef>) => void;
|
|
7
|
+
unregister: (name: string) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const BottomSheetContext = createContext<BottomSheetContextValue | null>(null);
|
|
11
|
+
|
|
12
|
+
export interface TrueSheetProviderProps {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Provider for TrueSheet on web.
|
|
18
|
+
* Required to wrap your app for sheet management via useTrueSheet hook.
|
|
19
|
+
*/
|
|
20
|
+
export function TrueSheetProvider({ children }: TrueSheetProviderProps) {
|
|
21
|
+
const sheetsRef = useRef<Map<string, RefObject<TrueSheetRef>>>(new Map());
|
|
22
|
+
|
|
23
|
+
const register = (name: string, methods: RefObject<TrueSheetRef>) => {
|
|
24
|
+
sheetsRef.current.set(name, methods);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const unregister = (name: string) => {
|
|
28
|
+
sheetsRef.current.delete(name);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const present = async (name: string, index: number = 0) => {
|
|
32
|
+
const sheet = sheetsRef.current.get(name);
|
|
33
|
+
if (!sheet?.current) {
|
|
34
|
+
console.warn(`TrueSheet: Could not find sheet with name "${name}"`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
return sheet.current.present(index);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const dismiss = async (name: string) => {
|
|
41
|
+
const sheet = sheetsRef.current.get(name);
|
|
42
|
+
if (!sheet?.current) {
|
|
43
|
+
console.warn(`TrueSheet: Could not find sheet with name "${name}"`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
return sheet.current.dismiss();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const resize = async (name: string, index: number) => {
|
|
50
|
+
const sheet = sheetsRef.current.get(name);
|
|
51
|
+
if (!sheet?.current) {
|
|
52
|
+
console.warn(`TrueSheet: Could not find sheet with name "${name}"`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
return sheet.current.resize(index);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<BottomSheetContext.Provider value={{ register, unregister, present, dismiss, resize }}>
|
|
60
|
+
<BottomSheetModalProvider>{children}</BottomSheetModalProvider>
|
|
61
|
+
</BottomSheetContext.Provider>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Hook to control TrueSheet instances by name.
|
|
67
|
+
* On web, this uses the TrueSheetContext from TrueSheetProvider.
|
|
68
|
+
*/
|
|
69
|
+
export function useTrueSheet(): TrueSheetContextMethods {
|
|
70
|
+
const context = useContext(BottomSheetContext);
|
|
71
|
+
|
|
72
|
+
if (!context) {
|
|
73
|
+
throw new Error('useTrueSheet must be used within a TrueSheetProvider');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
present: context.present,
|
|
78
|
+
dismiss: context.dismiss,
|
|
79
|
+
resize: context.resize,
|
|
80
|
+
};
|
|
81
|
+
}
|
package/src/index.ts
CHANGED