@lodev09/react-native-true-sheet 3.2.2 → 3.3.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.map +1 -1
- package/lib/module/TrueSheet.web.js +333 -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 +44 -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/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 +11 -4
- package/src/TrueSheet.tsx +5 -1
- package/src/TrueSheet.types.ts +46 -0
- package/src/TrueSheet.web.tsx +407 -0
- package/src/TrueSheetProvider.tsx +29 -0
- package/src/TrueSheetProvider.web.tsx +81 -0
- package/src/index.ts +1 -0
- package/src/reanimated/ReanimatedTrueSheet.web.tsx +78 -0
- package/src/reanimated/useReanimatedPositionChangeHandler.web.ts +32 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createElement,
|
|
3
|
+
Fragment,
|
|
4
|
+
forwardRef,
|
|
5
|
+
isValidElement,
|
|
6
|
+
useCallback,
|
|
7
|
+
useContext,
|
|
8
|
+
useEffect,
|
|
9
|
+
useImperativeHandle,
|
|
10
|
+
useMemo,
|
|
11
|
+
useRef,
|
|
12
|
+
useState,
|
|
13
|
+
} from 'react';
|
|
14
|
+
import { View, StyleSheet, useWindowDimensions } from 'react-native';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
BottomSheetBackdrop,
|
|
18
|
+
type BottomSheetBackdropProps,
|
|
19
|
+
BottomSheetFooter,
|
|
20
|
+
type BottomSheetFooterProps,
|
|
21
|
+
BottomSheetHandle,
|
|
22
|
+
type BottomSheetHandleProps,
|
|
23
|
+
BottomSheetModal,
|
|
24
|
+
BottomSheetView,
|
|
25
|
+
type SNAP_POINT_TYPE,
|
|
26
|
+
} from '@gorhom/bottom-sheet';
|
|
27
|
+
import { useDerivedValue, useSharedValue } from 'react-native-reanimated';
|
|
28
|
+
|
|
29
|
+
import { BottomSheetContext } from './TrueSheetProvider.web';
|
|
30
|
+
import type {
|
|
31
|
+
TrueSheetProps,
|
|
32
|
+
TrueSheetRef,
|
|
33
|
+
DetentChangeEvent,
|
|
34
|
+
DidBlurEvent,
|
|
35
|
+
DidDismissEvent,
|
|
36
|
+
DidFocusEvent,
|
|
37
|
+
DidPresentEvent,
|
|
38
|
+
MountEvent,
|
|
39
|
+
PositionChangeEvent,
|
|
40
|
+
WillBlurEvent,
|
|
41
|
+
WillDismissEvent,
|
|
42
|
+
WillFocusEvent,
|
|
43
|
+
WillPresentEvent,
|
|
44
|
+
DragBeginEvent,
|
|
45
|
+
DragChangeEvent,
|
|
46
|
+
DragEndEvent,
|
|
47
|
+
} from './TrueSheet.types';
|
|
48
|
+
|
|
49
|
+
const DEFAULT_CORNER_RADIUS = 16;
|
|
50
|
+
const DEFAULT_GRABBER_COLOR = 'rgba(0, 0, 0, 0.3)';
|
|
51
|
+
|
|
52
|
+
const renderSlot = (slot: TrueSheetProps['header'] | TrueSheetProps['footer']) => {
|
|
53
|
+
if (!slot) return null;
|
|
54
|
+
if (isValidElement(slot)) return slot;
|
|
55
|
+
return createElement(slot);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const TrueSheet = forwardRef<TrueSheetRef, TrueSheetProps>((props, ref) => {
|
|
59
|
+
const {
|
|
60
|
+
name,
|
|
61
|
+
detents = [0.5, 1],
|
|
62
|
+
dismissible = true,
|
|
63
|
+
draggable = true,
|
|
64
|
+
dimmed = true,
|
|
65
|
+
dimmedDetentIndex = 0,
|
|
66
|
+
children,
|
|
67
|
+
scrollable = false,
|
|
68
|
+
initialDetentIndex = -1,
|
|
69
|
+
backgroundColor = '#ffffff',
|
|
70
|
+
cornerRadius = DEFAULT_CORNER_RADIUS,
|
|
71
|
+
grabber = true,
|
|
72
|
+
grabberOptions,
|
|
73
|
+
maxHeight,
|
|
74
|
+
header,
|
|
75
|
+
footer,
|
|
76
|
+
onMount,
|
|
77
|
+
onWillPresent,
|
|
78
|
+
onDidPresent,
|
|
79
|
+
onWillDismiss,
|
|
80
|
+
onDidDismiss,
|
|
81
|
+
onDetentChange,
|
|
82
|
+
onPositionChange,
|
|
83
|
+
onDragBegin,
|
|
84
|
+
onDragChange,
|
|
85
|
+
onDragEnd,
|
|
86
|
+
onWillFocus,
|
|
87
|
+
onDidFocus,
|
|
88
|
+
onWillBlur,
|
|
89
|
+
onDidBlur,
|
|
90
|
+
style,
|
|
91
|
+
} = props;
|
|
92
|
+
|
|
93
|
+
const { height: windowHeight } = useWindowDimensions();
|
|
94
|
+
const bottomSheetContext = useContext(BottomSheetContext);
|
|
95
|
+
const modalRef = useRef<BottomSheetModal>(null);
|
|
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
|
+
|
|
103
|
+
const animatedPosition = useSharedValue(windowHeight);
|
|
104
|
+
const animatedIndex = useSharedValue(0);
|
|
105
|
+
|
|
106
|
+
const [snapIndex, setSnapIndex] = useState(initialDetentIndex);
|
|
107
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
108
|
+
|
|
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
|
+
} as PositionChangeEvent);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const hasAutoDetent = detents.includes('auto');
|
|
121
|
+
|
|
122
|
+
const containerHeight = maxHeight ?? windowHeight;
|
|
123
|
+
const snapPoints = useMemo(
|
|
124
|
+
() =>
|
|
125
|
+
detents
|
|
126
|
+
.filter((detent): detent is number => detent !== 'auto' && typeof detent === 'number')
|
|
127
|
+
.map((detent) => Math.min(1, Math.max(0.1, detent)) * containerHeight),
|
|
128
|
+
[detents, containerHeight]
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const handleChange = useCallback(
|
|
132
|
+
(index: number, _position: number, _type: SNAP_POINT_TYPE) => {
|
|
133
|
+
const previousIndex = currentIndexRef.current;
|
|
134
|
+
currentIndexRef.current = index;
|
|
135
|
+
|
|
136
|
+
// Handle drag end
|
|
137
|
+
if (isDragging.current && !isPresenting.current) {
|
|
138
|
+
isDragging.current = false;
|
|
139
|
+
onDragEnd?.({
|
|
140
|
+
nativeEvent: {
|
|
141
|
+
index,
|
|
142
|
+
position: animatedPosition.value,
|
|
143
|
+
detent: detents[index] ?? 0,
|
|
144
|
+
},
|
|
145
|
+
} as DragEndEvent);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!isPresenting.current && !isMinimized.current && previousIndex !== index && index >= 0) {
|
|
149
|
+
onDetentChange?.({
|
|
150
|
+
nativeEvent: {
|
|
151
|
+
index,
|
|
152
|
+
position: animatedPosition.value,
|
|
153
|
+
detent: detents[index] ?? 0,
|
|
154
|
+
},
|
|
155
|
+
} as DetentChangeEvent);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (isPresenting.current) {
|
|
159
|
+
isPresenting.current = false;
|
|
160
|
+
|
|
161
|
+
onDidPresent?.({
|
|
162
|
+
nativeEvent: {
|
|
163
|
+
index,
|
|
164
|
+
position: animatedPosition.value,
|
|
165
|
+
detent: detents[index] ?? 0,
|
|
166
|
+
},
|
|
167
|
+
} as DidPresentEvent);
|
|
168
|
+
|
|
169
|
+
onDidFocus?.({ nativeEvent: null } as DidFocusEvent);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Fire onDidBlur when sheet reaches minimized state (index -1 but still mounted)
|
|
173
|
+
if (isMinimized.current && index === -1) {
|
|
174
|
+
onDidBlur?.({ nativeEvent: null } as DidBlurEvent);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Fire onDidFocus when sheet is restored from minimized state
|
|
178
|
+
if (isMinimized.current && index >= 0) {
|
|
179
|
+
isMinimized.current = false;
|
|
180
|
+
onDidFocus?.({ nativeEvent: null } as DidFocusEvent);
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
[detents, animatedPosition]
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const handleDismiss = useCallback(() => {
|
|
187
|
+
onDidDismiss?.({ nativeEvent: null } as DidDismissEvent);
|
|
188
|
+
|
|
189
|
+
// Reset states since sheet is being dismissed
|
|
190
|
+
isMinimized.current = false;
|
|
191
|
+
isDismissing.current = false;
|
|
192
|
+
isDragging.current = false;
|
|
193
|
+
}, []);
|
|
194
|
+
|
|
195
|
+
const handleAnimate = useCallback(
|
|
196
|
+
(_fromIndex: number, toIndex: number) => {
|
|
197
|
+
// Detect drag begin (when not presenting or dismissing)
|
|
198
|
+
if (!isPresenting.current && !isDismissing.current && !isDragging.current && toIndex >= 0) {
|
|
199
|
+
isDragging.current = true;
|
|
200
|
+
onDragBegin?.({
|
|
201
|
+
nativeEvent: {
|
|
202
|
+
index: currentIndexRef.current,
|
|
203
|
+
position: animatedPosition.value,
|
|
204
|
+
detent: detents[currentIndexRef.current] ?? 0,
|
|
205
|
+
},
|
|
206
|
+
} as DragBeginEvent);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Drag change during animation
|
|
210
|
+
if (isDragging.current && toIndex >= 0) {
|
|
211
|
+
onDragChange?.({
|
|
212
|
+
nativeEvent: {
|
|
213
|
+
index: toIndex,
|
|
214
|
+
position: animatedPosition.value,
|
|
215
|
+
detent: detents[toIndex] ?? 0,
|
|
216
|
+
},
|
|
217
|
+
} as DragChangeEvent);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (isPresenting.current) {
|
|
221
|
+
onWillPresent?.({
|
|
222
|
+
nativeEvent: {
|
|
223
|
+
index: toIndex,
|
|
224
|
+
position: animatedPosition.value,
|
|
225
|
+
detent: detents[toIndex] ?? 0,
|
|
226
|
+
},
|
|
227
|
+
} as WillPresentEvent);
|
|
228
|
+
|
|
229
|
+
// Focus events fire together with present events
|
|
230
|
+
onWillFocus?.({ nativeEvent: null } as WillFocusEvent);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Detect if sheet is being restored (will focus)
|
|
234
|
+
if (isMinimized.current && toIndex >= 0) {
|
|
235
|
+
onWillFocus?.({ nativeEvent: null } as WillFocusEvent);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (toIndex === -1 && !isPresenting.current) {
|
|
239
|
+
// Will be handled as blur if the sheet doesn't actually dismiss
|
|
240
|
+
isMinimized.current = true;
|
|
241
|
+
onWillBlur?.({ nativeEvent: null } as WillBlurEvent);
|
|
242
|
+
|
|
243
|
+
if (isDismissing.current) {
|
|
244
|
+
onWillDismiss?.({ nativeEvent: null } as WillDismissEvent);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
[detents, animatedPosition]
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const backdropComponent = useCallback(
|
|
252
|
+
(backdropProps: BottomSheetBackdropProps) => {
|
|
253
|
+
if (!dimmed) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
return (
|
|
257
|
+
<BottomSheetBackdrop
|
|
258
|
+
{...backdropProps}
|
|
259
|
+
opacity={0.5}
|
|
260
|
+
appearsOnIndex={dimmedDetentIndex}
|
|
261
|
+
disappearsOnIndex={dimmedDetentIndex - 1}
|
|
262
|
+
pressBehavior={dismissible ? 'close' : 'none'}
|
|
263
|
+
/>
|
|
264
|
+
);
|
|
265
|
+
},
|
|
266
|
+
[dimmed, dimmedDetentIndex, dismissible]
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const handleComponent = useCallback(
|
|
270
|
+
(handleProps: BottomSheetHandleProps) => {
|
|
271
|
+
if (!grabber) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
return (
|
|
275
|
+
<BottomSheetHandle
|
|
276
|
+
{...handleProps}
|
|
277
|
+
style={[
|
|
278
|
+
styles.handle,
|
|
279
|
+
grabberOptions?.topMargin !== undefined && { paddingTop: grabberOptions.topMargin },
|
|
280
|
+
]}
|
|
281
|
+
indicatorStyle={[
|
|
282
|
+
styles.handleIndicator,
|
|
283
|
+
grabberOptions?.width !== undefined && { width: grabberOptions.width },
|
|
284
|
+
grabberOptions?.height !== undefined && { height: grabberOptions.height },
|
|
285
|
+
grabberOptions?.cornerRadius !== undefined && {
|
|
286
|
+
borderRadius: grabberOptions.cornerRadius,
|
|
287
|
+
},
|
|
288
|
+
{ backgroundColor: grabberOptions?.color ?? DEFAULT_GRABBER_COLOR },
|
|
289
|
+
]}
|
|
290
|
+
/>
|
|
291
|
+
);
|
|
292
|
+
},
|
|
293
|
+
[grabber, grabberOptions]
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const footerComponent = useMemo(
|
|
297
|
+
() =>
|
|
298
|
+
footer
|
|
299
|
+
? (footerProps: BottomSheetFooterProps) => (
|
|
300
|
+
<BottomSheetFooter {...footerProps}>{renderSlot(footer)}</BottomSheetFooter>
|
|
301
|
+
)
|
|
302
|
+
: undefined,
|
|
303
|
+
[footer]
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
// For scrollable, we render the child directly
|
|
307
|
+
const ContainerComponent = scrollable ? Fragment : BottomSheetView;
|
|
308
|
+
|
|
309
|
+
const sheetMethodsRef = useRef<TrueSheetRef>({
|
|
310
|
+
present: async (index = 0) => {
|
|
311
|
+
setSnapIndex(index);
|
|
312
|
+
isPresenting.current = true;
|
|
313
|
+
modalRef.current?.present();
|
|
314
|
+
},
|
|
315
|
+
dismiss: async () => {
|
|
316
|
+
isDismissing.current = true;
|
|
317
|
+
modalRef.current?.dismiss();
|
|
318
|
+
},
|
|
319
|
+
resize: async (index: number) => {
|
|
320
|
+
modalRef.current?.snapToIndex(index);
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
useImperativeHandle(ref, () => sheetMethodsRef.current);
|
|
325
|
+
|
|
326
|
+
// Register with context provider
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
if (name) {
|
|
329
|
+
bottomSheetContext?.register(name, sheetMethodsRef);
|
|
330
|
+
}
|
|
331
|
+
return () => {
|
|
332
|
+
if (name) {
|
|
333
|
+
bottomSheetContext?.unregister(name);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}, [name]);
|
|
337
|
+
|
|
338
|
+
// Auto-present on mount if initialDetentIndex is set
|
|
339
|
+
useEffect(() => {
|
|
340
|
+
if (initialDetentIndexRef.current >= 0) {
|
|
341
|
+
sheetMethodsRef.current.present(initialDetentIndexRef.current);
|
|
342
|
+
}
|
|
343
|
+
}, []);
|
|
344
|
+
|
|
345
|
+
// Handle mount event after first render
|
|
346
|
+
useEffect(() => {
|
|
347
|
+
if (!isMounted) {
|
|
348
|
+
setIsMounted(true);
|
|
349
|
+
onMount?.({ nativeEvent: null } as MountEvent);
|
|
350
|
+
}
|
|
351
|
+
}, [isMounted, onMount]);
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<BottomSheetModal
|
|
355
|
+
ref={modalRef}
|
|
356
|
+
name={name}
|
|
357
|
+
style={[
|
|
358
|
+
styles.root,
|
|
359
|
+
{ backgroundColor, borderTopLeftRadius: cornerRadius, borderTopRightRadius: cornerRadius },
|
|
360
|
+
]}
|
|
361
|
+
index={snapIndex}
|
|
362
|
+
animateOnMount
|
|
363
|
+
enablePanDownToClose={dismissible}
|
|
364
|
+
enableContentPanningGesture={draggable}
|
|
365
|
+
enableHandlePanningGesture={draggable}
|
|
366
|
+
animatedPosition={animatedPosition}
|
|
367
|
+
animatedIndex={animatedIndex}
|
|
368
|
+
handleComponent={handleComponent}
|
|
369
|
+
onChange={handleChange}
|
|
370
|
+
onAnimate={handleAnimate}
|
|
371
|
+
enableDynamicSizing={hasAutoDetent}
|
|
372
|
+
maxDynamicContentSize={maxHeight}
|
|
373
|
+
snapPoints={snapPoints.length > 0 ? snapPoints : undefined}
|
|
374
|
+
onDismiss={handleDismiss}
|
|
375
|
+
stackBehavior="switch"
|
|
376
|
+
backdropComponent={backdropComponent}
|
|
377
|
+
footerComponent={footerComponent}
|
|
378
|
+
>
|
|
379
|
+
<ContainerComponent>
|
|
380
|
+
<View style={[styles.container, style]}>
|
|
381
|
+
{renderSlot(header)}
|
|
382
|
+
{children}
|
|
383
|
+
</View>
|
|
384
|
+
</ContainerComponent>
|
|
385
|
+
</BottomSheetModal>
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const styles = StyleSheet.create({
|
|
390
|
+
root: {
|
|
391
|
+
overflow: 'hidden',
|
|
392
|
+
},
|
|
393
|
+
container: {},
|
|
394
|
+
handle: {
|
|
395
|
+
position: 'absolute',
|
|
396
|
+
top: 0,
|
|
397
|
+
left: 0,
|
|
398
|
+
right: 0,
|
|
399
|
+
zIndex: 1,
|
|
400
|
+
paddingVertical: 10,
|
|
401
|
+
pointerEvents: 'none',
|
|
402
|
+
},
|
|
403
|
+
handleIndicator: {
|
|
404
|
+
width: 36,
|
|
405
|
+
height: 5,
|
|
406
|
+
},
|
|
407
|
+
});
|
|
@@ -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
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { forwardRef, useEffect } from 'react';
|
|
2
|
+
import { useWindowDimensions } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { TrueSheet } from '../TrueSheet.web';
|
|
5
|
+
import type { TrueSheetProps, TrueSheetRef, PositionChangeEvent } from '../TrueSheet.types';
|
|
6
|
+
import { useReanimatedTrueSheet } from './ReanimatedTrueSheetProvider';
|
|
7
|
+
|
|
8
|
+
interface ReanimatedTrueSheetProps extends TrueSheetProps {
|
|
9
|
+
/**
|
|
10
|
+
* Callback for position changes.
|
|
11
|
+
* On web, this is called with the position data from @gorhom/bottom-sheet.
|
|
12
|
+
*
|
|
13
|
+
* @see {@link TrueSheetProps.onPositionChange}
|
|
14
|
+
*/
|
|
15
|
+
onPositionChange?: TrueSheetProps['onPositionChange'];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Reanimated-enabled version of TrueSheet for web that automatically syncs
|
|
20
|
+
* position with the provider's shared value.
|
|
21
|
+
* Must be used within a ReanimatedTrueSheetProvider.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* import { ReanimatedTrueSheet, ReanimatedTrueSheetProvider } from '@lodev09/react-native-true-sheet/reanimated'
|
|
26
|
+
*
|
|
27
|
+
* function MyScreen() {
|
|
28
|
+
* const sheetRef = useRef<TrueSheetRef>(null)
|
|
29
|
+
*
|
|
30
|
+
* return (
|
|
31
|
+
* <ReanimatedTrueSheetProvider>
|
|
32
|
+
* <View>
|
|
33
|
+
* <ReanimatedTrueSheet
|
|
34
|
+
* ref={sheetRef}
|
|
35
|
+
* detents={[0.25, 0.5, 1]}
|
|
36
|
+
* initialDetentIndex={1}
|
|
37
|
+
* >
|
|
38
|
+
* <Text>Sheet Content</Text>
|
|
39
|
+
* </ReanimatedTrueSheet>
|
|
40
|
+
* </View>
|
|
41
|
+
* </ReanimatedTrueSheetProvider>
|
|
42
|
+
* )
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export const ReanimatedTrueSheet = forwardRef<TrueSheetRef, ReanimatedTrueSheetProps>(
|
|
47
|
+
(props, ref) => {
|
|
48
|
+
const { onPositionChange, detents = [0.5, 1], ...rest } = props;
|
|
49
|
+
const { height: windowHeight } = useWindowDimensions();
|
|
50
|
+
|
|
51
|
+
const { animatedPosition, animatedIndex, animatedDetent } = useReanimatedTrueSheet();
|
|
52
|
+
|
|
53
|
+
// Reset animated values when component unmounts
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
return () => {
|
|
56
|
+
animatedPosition.value = windowHeight;
|
|
57
|
+
animatedIndex.value = -1;
|
|
58
|
+
animatedDetent.value = 0;
|
|
59
|
+
};
|
|
60
|
+
}, [windowHeight]);
|
|
61
|
+
|
|
62
|
+
const handlePositionChange = (event: PositionChangeEvent) => {
|
|
63
|
+
const { position, index, detent } = event.nativeEvent;
|
|
64
|
+
|
|
65
|
+
// Sync with provider's shared values
|
|
66
|
+
animatedPosition.value = position;
|
|
67
|
+
animatedIndex.value = index;
|
|
68
|
+
animatedDetent.value = detent;
|
|
69
|
+
|
|
70
|
+
// Call user's callback
|
|
71
|
+
onPositionChange?.(event);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<TrueSheet ref={ref} detents={detents} onPositionChange={handlePositionChange} {...rest} />
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import type { PositionChangeEvent, PositionChangeEventPayload } from '../TrueSheet.types';
|
|
3
|
+
|
|
4
|
+
type PositionChangeHandler = (
|
|
5
|
+
payload: PositionChangeEventPayload,
|
|
6
|
+
context: Record<string, unknown>
|
|
7
|
+
) => void;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Web implementation of useReanimatedPositionChangeHandler.
|
|
11
|
+
*
|
|
12
|
+
* On web, this returns a simple callback wrapper since @gorhom/bottom-sheet
|
|
13
|
+
* already provides animated position values. The worklet directive is ignored
|
|
14
|
+
* on web as there's no native UI thread.
|
|
15
|
+
*
|
|
16
|
+
* @param handler - The position change handler function
|
|
17
|
+
* @param _dependencies - Unused on web, kept for API compatibility
|
|
18
|
+
* @returns An event handler compatible with onPositionChange prop
|
|
19
|
+
*/
|
|
20
|
+
export const useReanimatedPositionChangeHandler = (
|
|
21
|
+
handler: PositionChangeHandler,
|
|
22
|
+
_dependencies: unknown[] = []
|
|
23
|
+
) => {
|
|
24
|
+
const context: Record<string, unknown> = {};
|
|
25
|
+
|
|
26
|
+
return useCallback(
|
|
27
|
+
(event: PositionChangeEvent) => {
|
|
28
|
+
handler(event.nativeEvent, context);
|
|
29
|
+
},
|
|
30
|
+
[handler]
|
|
31
|
+
);
|
|
32
|
+
};
|