@niibase/bottom-sheet-manager 1.3.0 → 1.4.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.
Files changed (55) hide show
  1. package/README.md +204 -193
  2. package/lib/commonjs/index.js +9 -2
  3. package/lib/commonjs/index.js.map +1 -1
  4. package/lib/commonjs/manager.js +56 -16
  5. package/lib/commonjs/manager.js.map +1 -1
  6. package/lib/commonjs/provider.js +41 -44
  7. package/lib/commonjs/provider.js.map +1 -1
  8. package/lib/commonjs/router/index.js +37 -7
  9. package/lib/commonjs/router/index.js.map +1 -1
  10. package/lib/commonjs/router/router.js.map +1 -1
  11. package/lib/commonjs/router/view.js +77 -220
  12. package/lib/commonjs/router/view.js.map +1 -1
  13. package/lib/commonjs/sheet.js +61 -85
  14. package/lib/commonjs/sheet.js.map +1 -1
  15. package/lib/module/index.js +2 -2
  16. package/lib/module/index.js.map +1 -1
  17. package/lib/module/manager.js +56 -16
  18. package/lib/module/manager.js.map +1 -1
  19. package/lib/module/provider.js +39 -42
  20. package/lib/module/provider.js.map +1 -1
  21. package/lib/module/router/index.js +39 -8
  22. package/lib/module/router/index.js.map +1 -1
  23. package/lib/module/router/router.js.map +1 -1
  24. package/lib/module/router/view.js +76 -220
  25. package/lib/module/router/view.js.map +1 -1
  26. package/lib/module/sheet.js +63 -87
  27. package/lib/module/sheet.js.map +1 -1
  28. package/lib/typescript/index.d.ts +2 -2
  29. package/lib/typescript/index.d.ts.map +1 -1
  30. package/lib/typescript/manager.d.ts +16 -0
  31. package/lib/typescript/manager.d.ts.map +1 -1
  32. package/lib/typescript/provider.d.ts +10 -23
  33. package/lib/typescript/provider.d.ts.map +1 -1
  34. package/lib/typescript/router/index.d.ts +21 -7
  35. package/lib/typescript/router/index.d.ts.map +1 -1
  36. package/lib/typescript/router/router.d.ts.map +1 -1
  37. package/lib/typescript/router/types.d.ts +75 -61
  38. package/lib/typescript/router/types.d.ts.map +1 -1
  39. package/lib/typescript/router/view.d.ts +3 -3
  40. package/lib/typescript/router/view.d.ts.map +1 -1
  41. package/lib/typescript/sheet.d.ts +1 -1
  42. package/lib/typescript/sheet.d.ts.map +1 -1
  43. package/lib/typescript/types.d.ts +32 -15
  44. package/lib/typescript/types.d.ts.map +1 -1
  45. package/package.json +15 -15
  46. package/scripts/postinstall.mjs +36 -0
  47. package/src/index.ts +7 -7
  48. package/src/manager.ts +66 -22
  49. package/src/provider.tsx +72 -53
  50. package/src/router/index.tsx +46 -9
  51. package/src/router/router.ts +6 -2
  52. package/src/router/types.ts +109 -91
  53. package/src/router/view.tsx +86 -308
  54. package/src/sheet.tsx +111 -123
  55. package/src/types.ts +146 -133
@@ -1,193 +1,40 @@
1
- import Animated, {
2
- Easing,
3
- interpolate,
4
- interpolateColor,
5
- runOnJS,
6
- SharedValue,
7
- useAnimatedReaction,
8
- useAnimatedStyle,
9
- useSharedValue,
10
- withSpring,
11
- withTiming,
12
- } from "react-native-reanimated";
13
- import {
14
- BottomSheetBackdrop,
15
- BottomSheetModal,
16
- BottomSheetModalProvider,
17
- } from "@gorhom/bottom-sheet";
18
1
  import { useSafeAreaInsets } from "react-native-safe-area-context";
19
2
  import { ParamListBase, useTheme } from "@react-navigation/native";
20
- import { StatusBar, ViewStyle } from "react-native";
3
+ import { SNAP_POINT_TYPE } from "@gorhom/bottom-sheet";
21
4
  import * as React from "react";
22
5
 
23
6
  import type {
24
- BottomSheetDescriptor,
25
7
  BottomSheetDescriptorMap,
26
- BottomSheetModalScreenProps,
27
- BottomSheetNavigationConfig,
28
8
  BottomSheetNavigationHelpers,
29
9
  BottomSheetNavigationState,
30
10
  BottomSheetRoute,
31
11
  } from "./types";
12
+ import { BottomSheetInstance, BottomSheetProps } from "../types";
32
13
  import { BottomSheetActions } from "./router";
14
+ import BottomSheet from "../sheet";
33
15
 
34
- const DEFAULT_SNAP_POINTS = ["66%"];
16
+ type Props = {
17
+ state: BottomSheetNavigationState<ParamListBase>;
18
+ navigation: BottomSheetNavigationHelpers;
19
+ descriptors: BottomSheetDescriptorMap;
20
+ };
35
21
 
36
- function AnimatedSheetWrapper({
37
- route,
22
+ function BottomSheetScreen({
23
+ children,
38
24
  navigation,
39
- descriptor,
40
- isFullScreen,
41
- previousIndex,
42
- defaultStyle,
43
- themeBackgroundStyle,
44
- themeHandleIndicatorStyle,
45
- }: {
25
+ route,
26
+ ...props
27
+ }: BottomSheetProps & {
46
28
  route: BottomSheetRoute<ParamListBase>;
47
29
  navigation: BottomSheetNavigationHelpers;
48
- descriptor: BottomSheetDescriptor;
49
- isFullScreen: SharedValue<number>;
50
- previousIndex: SharedValue<number>;
51
- defaultStyle: ViewStyle;
52
- themeBackgroundStyle: ViewStyle;
53
- themeHandleIndicatorStyle: ViewStyle;
54
30
  }) {
55
- const { options, render } = descriptor;
56
- const {
57
- index = 0,
58
- snapPoints = DEFAULT_SNAP_POINTS,
59
- animatedIndex: defaultAnimatedIndex,
60
- onAnimate,
61
- handleStyle,
62
- backgroundStyle,
63
- handleIndicatorStyle,
64
- enableDynamicSizing,
65
- iosModalSheetTypeOfAnimation,
66
- clickThrough,
67
- style,
68
- ...sheetProps
69
- } = options;
70
-
71
- // Calculate safe index
72
- const safeIndex = Math.min(route.snapToIndex ?? index, snapPoints.length - 1);
73
-
74
- // Create animatedIndex for this sheet
75
- const animatedIndex = useSharedValue(0);
76
-
77
- // Use animated reaction to watch animatedIndex and update isFullScreen reactively
78
- useAnimatedReaction(
79
- () => animatedIndex.value,
80
- (index) => {
81
- "worklet";
82
- if (defaultAnimatedIndex) {
83
- defaultAnimatedIndex.set(index);
84
- }
85
-
86
- if (!iosModalSheetTypeOfAnimation) {
87
- if (isFullScreen.value > 0) isFullScreen.set(0);
88
- previousIndex.set(index);
89
- return;
90
- }
91
-
92
- if (isFullScreen.value < 0) {
93
- isFullScreen.set(0);
94
- }
95
-
96
- const isClosing =
97
- index < 0 || (previousIndex.value >= 0 && index < previousIndex.value - 0.05);
98
- previousIndex.set(index);
99
-
100
- if (isClosing) {
101
- if (isFullScreen.value > 0.01) {
102
- isFullScreen.set(
103
- withTiming(0, {
104
- duration: 150 * 0.85,
105
- easing: Easing.bezier(0.25, 0.1, 0.25, 1),
106
- }),
107
- );
108
- }
109
- return;
110
- }
111
-
112
- const points: (string | number)[] = ["%90", "90%"];
113
- const fullScreenIndex = snapPoints.findIndex((p: string | number) =>
114
- points.includes(p),
115
- );
116
-
117
- if (index >= fullScreenIndex - 0.5 && index <= fullScreenIndex + 0.5) {
118
- isFullScreen.set(1);
119
- } else if (index >= 0) {
120
- isFullScreen.set(0);
121
- }
122
- },
123
- [snapPoints, iosModalSheetTypeOfAnimation],
124
- );
125
-
126
- return (
127
- <BottomSheetModalScreen
128
- route={route}
129
- navigation={navigation}
130
- index={safeIndex}
131
- snapPoints={enableDynamicSizing ? undefined : snapPoints}
132
- enableDynamicSizing={enableDynamicSizing}
133
- animatedIndex={animatedIndex as any}
134
- clickThrough={clickThrough}
135
- animationConfigs={{
136
- duration: 300,
137
- easing: Easing.bezier(0.25, 0.1, 0.25, 1),
138
- }}
139
- onAnimate={(from, to, ...args) => {
140
- if (to >= isFullScreen.value && to > snapPoints.length - 1) {
141
- isFullScreen.set(0);
142
- } else if (to > 0 && to === previousIndex.value && isFullScreen.value === 0) {
143
- isFullScreen.set(1);
144
- }
145
-
146
- onAnimate?.(from, to, ...args);
147
- }}
148
- topInset={0}
149
- bottomInset={0}
150
- style={[defaultStyle, style]}
151
- backgroundStyle={[themeBackgroundStyle, backgroundStyle]}
152
- handleIndicatorStyle={[themeHandleIndicatorStyle, handleIndicatorStyle]}
153
- handleStyle={[themeBackgroundStyle, { borderRadius: 24 }, handleStyle]}
154
- {...sheetProps}
155
- >
156
- {render?.()}
157
- </BottomSheetModalScreen>
158
- );
159
- }
160
-
161
- function BottomSheetModalScreen({
162
- route,
163
- navigation,
164
- clickThrough,
165
- opacity,
166
- animatedIndex,
167
- onChange,
168
- children,
169
- ...props
170
- }: BottomSheetModalScreenProps & { animatedIndex?: ReturnType<typeof useSharedValue> }) {
171
- const ref = React.useRef<BottomSheetModal>(null);
31
+ const ref = React.useRef<BottomSheetInstance>(null);
172
32
  const lastSnapIndexRef = React.useRef(route.snapToIndex ?? props.index ?? 0);
173
33
 
174
- // Present on mount.
175
- React.useEffect(() => {
176
- ref.current?.present();
177
- }, []);
178
-
179
- // Track mount state to avoid dismissing on unmount
180
- const isMounted = React.useRef(true);
181
- React.useEffect(() => {
182
- return () => {
183
- isMounted.current = false;
184
- };
185
- }, []);
186
-
187
34
  // Handle route closing state
188
35
  React.useEffect(() => {
189
36
  if (route.closing) {
190
- ref.current?.dismiss();
37
+ ref.current?.close();
191
38
  }
192
39
  }, [route.closing]);
193
40
 
@@ -200,7 +47,13 @@ function BottomSheetModalScreen({
200
47
  }, [route.snapToIndex, route.snapToKey]);
201
48
 
202
49
  const handleChange = React.useCallback(
203
- (newIndex: number) => {
50
+ (newIndex: number, position: number, type: SNAP_POINT_TYPE) => {
51
+ navigation.emit({
52
+ type: "sheetOnChange",
53
+ target: route.key,
54
+ data: { index: newIndex, position, type },
55
+ });
56
+
204
57
  const currentIndex = lastSnapIndexRef.current;
205
58
  lastSnapIndexRef.current = newIndex;
206
59
 
@@ -211,175 +64,100 @@ function BottomSheetModalScreen({
211
64
  [navigation],
212
65
  );
213
66
 
214
- const handleDismiss = React.useCallback(() => {
215
- // BottomSheetModal will call onDismiss on unmount, but we don't want that
216
- // since we handle navigation state separately
217
- if (isMounted.current) {
218
- navigation.dispatch({
219
- ...BottomSheetActions.remove(),
220
- source: route.key,
221
- });
222
- }
223
- }, [navigation, route.key]);
224
-
225
67
  return (
226
- <BottomSheetModal
227
- {...props}
228
- ref={ref}
229
- onDismiss={handleDismiss}
230
- onChange={handleChange}
231
- animatedIndex={animatedIndex}
232
- index={props.index}
233
- backdropComponent={(backdropProps) => (
234
- <BottomSheetBackdrop
235
- {...backdropProps}
236
- appearsOnIndex={0}
237
- disappearsOnIndex={-1}
238
- enableTouchThrough={!!clickThrough}
239
- opacity={opacity ?? 0.45}
240
- />
241
- )}
242
- >
68
+ <BottomSheet ref={ref} onChange={handleChange} {...props}>
243
69
  {children}
244
- </BottomSheetModal>
70
+ </BottomSheet>
245
71
  );
246
72
  }
247
73
 
248
- type Props = BottomSheetNavigationConfig & {
249
- state: BottomSheetNavigationState<ParamListBase>;
250
- navigation: BottomSheetNavigationHelpers;
251
- descriptors: BottomSheetDescriptorMap;
252
- };
253
-
254
74
  export function BottomSheetView({ state, navigation, descriptors }: Props) {
255
75
  const { colors } = useTheme();
256
- const { top, bottom, left, right } = useSafeAreaInsets();
76
+ const { bottom, left, right } = useSafeAreaInsets();
257
77
 
258
78
  const themeBackgroundStyle = React.useMemo(
259
79
  () => ({
260
80
  borderCurve: "continuous" as unknown as undefined,
261
81
  backgroundColor: colors.card,
262
- borderTopLeftRadius: 20,
263
- borderTopRightRadius: 20,
264
82
  }),
265
83
  [colors.card],
266
84
  );
267
85
 
268
86
  const themeHandleIndicatorStyle = React.useMemo(
269
87
  () => ({
88
+ borderCurve: "continuous" as unknown as undefined,
270
89
  backgroundColor: colors.border,
271
- height: 5,
272
- width: 50,
273
90
  }),
274
91
  [colors.border],
275
92
  );
276
93
 
277
94
  const defaultStyle = React.useMemo(
278
- () => ({
279
- paddingBottom: bottom,
280
- paddingLeft: left,
281
- paddingRight: right,
282
- }),
95
+ () => ({ paddingBottom: bottom, paddingLeft: left, paddingRight: right }),
283
96
  [bottom, left, right],
284
97
  );
285
98
 
286
- // IOS modal sheet type of animation
287
- const isFullScreen = useSharedValue(-1);
288
- const previousIndex = useSharedValue(-1);
289
-
290
- const colorStyle = useAnimatedStyle(() => ({
291
- flex: 1,
292
- backgroundColor: withSpring(
293
- interpolateColor(isFullScreen.value, [0, 1], ["transparent", "#000"]),
294
- { duration: 150 },
295
- ),
296
- }));
297
- const animatedStyle = useAnimatedStyle(
298
- () => ({
299
- flex: 1,
300
- overflow: "hidden",
301
- borderRadius: interpolate(isFullScreen.value, [0, 0.8, 1], [0, 20, 24], "clamp"),
302
- transform: [
303
- {
304
- scaleX: withSpring(
305
- interpolate(isFullScreen.value, [0, 0.8], [1, 0.92], "clamp"),
306
- { duration: 150 },
307
- ),
308
- },
309
- {
310
- translateY: withSpring(
311
- interpolate(isFullScreen.value, [0, 0.8, 1], [0, top, top + 5], "clamp"),
312
- { duration: 150, dampingRatio: 1.5 },
313
- ),
314
- },
315
- ],
316
- }),
317
- [top],
318
- );
319
-
320
- // Since background color is white, we need to set status bar to light
321
- const setStatusBar = StatusBar.setBarStyle;
322
- useAnimatedReaction(
323
- () => isFullScreen.value,
324
- (currentValue) => {
325
- "worklet";
326
- if (currentValue > -1) {
327
- runOnJS(setStatusBar)(currentValue >= 0.5 ? "light-content" : "default");
328
- }
329
- },
330
- [],
331
- );
332
-
333
- // Get the base (first) route - this is the main content
334
- const baseRoute = state.routes[0];
335
- if (!baseRoute) {
336
- return null;
337
- }
338
-
339
- const baseDescriptor = descriptors[baseRoute.key];
340
- if (!baseDescriptor) {
341
- return null;
342
- }
343
-
344
- // Sheet routes are all routes after the base route
345
- const sheetRoutes = state.routes.slice(1);
346
- const hasSheets = sheetRoutes.length > 0;
99
+ const [baseRoute, ...sheetRoutes] = state.routes;
100
+ const baseDescriptor = baseRoute ? descriptors[baseRoute.key] : null;
347
101
 
348
102
  return (
349
103
  <>
350
- {/* Base content with iOS modal animation */}
351
- <Animated.View style={colorStyle}>
352
- <Animated.View style={animatedStyle}>{baseDescriptor.render?.()}</Animated.View>
353
- </Animated.View>
354
-
355
- {/* Bottom sheet modals */}
356
- {hasSheets && (
357
- <BottomSheetModalProvider>
358
- {sheetRoutes.map((route) => {
359
- // Skip routes that are being removed
360
- if (route.closing && !descriptors[route.key]) {
361
- return null;
362
- }
363
-
364
- const descriptor = descriptors[route.key];
365
- if (!descriptor) return null;
366
-
367
- return (
368
- <AnimatedSheetWrapper
369
- key={route.key}
370
- route={route}
371
- navigation={navigation}
372
- descriptor={descriptor}
373
- isFullScreen={isFullScreen}
374
- previousIndex={previousIndex}
375
- defaultStyle={defaultStyle}
376
- themeBackgroundStyle={themeBackgroundStyle}
377
- themeHandleIndicatorStyle={themeHandleIndicatorStyle}
378
- />
379
- );
380
- })}
381
- </BottomSheetModalProvider>
382
- )}
104
+ {baseDescriptor?.render()}
105
+ {sheetRoutes.map((route) => {
106
+ const descriptor = descriptors[route.key];
107
+ if (!descriptor) return null;
108
+
109
+ const { options, render } = descriptor;
110
+ const {
111
+ index = 0,
112
+ style,
113
+ backgroundStyle,
114
+ handleIndicatorStyle,
115
+ handleStyle,
116
+ ...props
117
+ } = options;
118
+
119
+ return (
120
+ <BottomSheetScreen
121
+ key={route.key}
122
+ id={route.key}
123
+ route={route}
124
+ index={index}
125
+ navigation={navigation}
126
+ style={[defaultStyle, style]}
127
+ backgroundStyle={[themeBackgroundStyle, backgroundStyle]}
128
+ handleIndicatorStyle={[themeHandleIndicatorStyle, handleIndicatorStyle]}
129
+ handleStyle={[themeBackgroundStyle, { borderRadius: 24 }, handleStyle]}
130
+ onClose={(data) => {
131
+ navigation.dispatch({
132
+ ...BottomSheetActions.remove(),
133
+ source: route.key,
134
+ });
135
+ navigation.emit({
136
+ type: "sheetDismiss",
137
+ target: route.key,
138
+ data,
139
+ });
140
+ }}
141
+ onBeforeShow={(data) => {
142
+ navigation.emit({
143
+ type: "sheetPresent",
144
+ target: route.key,
145
+ data,
146
+ });
147
+ }}
148
+ onAnimate={(fromIndex, toIndex, fromPosition, toPosition) => {
149
+ navigation.emit({
150
+ type: "sheetOnAnimate",
151
+ target: route.key,
152
+ data: { fromIndex, toIndex, fromPosition, toPosition },
153
+ });
154
+ }}
155
+ {...props}
156
+ >
157
+ {render()}
158
+ </BottomSheetScreen>
159
+ );
160
+ })}
383
161
  </>
384
162
  );
385
163
  }