@niibase/bottom-sheet-manager 1.2.0 → 1.3.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.
Files changed (63) hide show
  1. package/README.md +371 -37
  2. package/lib/commonjs/events.js +100 -15
  3. package/lib/commonjs/events.js.map +1 -1
  4. package/lib/commonjs/index.js +7 -0
  5. package/lib/commonjs/index.js.map +1 -1
  6. package/lib/commonjs/manager.js +107 -29
  7. package/lib/commonjs/manager.js.map +1 -1
  8. package/lib/commonjs/provider.js +69 -28
  9. package/lib/commonjs/provider.js.map +1 -1
  10. package/lib/commonjs/router/index.js +50 -21
  11. package/lib/commonjs/router/index.js.map +1 -1
  12. package/lib/commonjs/router/router.js +137 -12
  13. package/lib/commonjs/router/router.js.map +1 -1
  14. package/lib/commonjs/router/view.js +194 -84
  15. package/lib/commonjs/router/view.js.map +1 -1
  16. package/lib/commonjs/sheet.js +124 -76
  17. package/lib/commonjs/sheet.js.map +1 -1
  18. package/lib/module/events.js +100 -15
  19. package/lib/module/events.js.map +1 -1
  20. package/lib/module/index.js +1 -1
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/module/manager.js +108 -29
  23. package/lib/module/manager.js.map +1 -1
  24. package/lib/module/provider.js +65 -25
  25. package/lib/module/provider.js.map +1 -1
  26. package/lib/module/router/index.js +34 -18
  27. package/lib/module/router/index.js.map +1 -1
  28. package/lib/module/router/router.js +135 -11
  29. package/lib/module/router/router.js.map +1 -1
  30. package/lib/module/router/view.js +194 -84
  31. package/lib/module/router/view.js.map +1 -1
  32. package/lib/module/sheet.js +126 -78
  33. package/lib/module/sheet.js.map +1 -1
  34. package/lib/typescript/events.d.ts +46 -12
  35. package/lib/typescript/events.d.ts.map +1 -1
  36. package/lib/typescript/index.d.ts +1 -1
  37. package/lib/typescript/index.d.ts.map +1 -1
  38. package/lib/typescript/manager.d.ts +57 -7
  39. package/lib/typescript/manager.d.ts.map +1 -1
  40. package/lib/typescript/provider.d.ts +22 -3
  41. package/lib/typescript/provider.d.ts.map +1 -1
  42. package/lib/typescript/router/index.d.ts +33 -17
  43. package/lib/typescript/router/index.d.ts.map +1 -1
  44. package/lib/typescript/router/router.d.ts +44 -5
  45. package/lib/typescript/router/router.d.ts.map +1 -1
  46. package/lib/typescript/router/types.d.ts +113 -17
  47. package/lib/typescript/router/types.d.ts.map +1 -1
  48. package/lib/typescript/router/view.d.ts +1 -1
  49. package/lib/typescript/router/view.d.ts.map +1 -1
  50. package/lib/typescript/sheet.d.ts.map +1 -1
  51. package/lib/typescript/types.d.ts +25 -11
  52. package/lib/typescript/types.d.ts.map +1 -1
  53. package/package.json +1 -1
  54. package/src/events.ts +118 -27
  55. package/src/index.ts +6 -5
  56. package/src/manager.ts +156 -33
  57. package/src/provider.tsx +98 -44
  58. package/src/router/index.tsx +38 -31
  59. package/src/router/router.ts +184 -15
  60. package/src/router/types.ts +119 -22
  61. package/src/router/view.tsx +252 -132
  62. package/src/sheet.tsx +175 -95
  63. package/src/types.ts +144 -130
@@ -1,70 +1,182 @@
1
- import {
2
- BottomSheetBackdrop,
3
- BottomSheetModal,
4
- BottomSheetModalProps,
5
- BottomSheetModalProvider,
6
- } from "@gorhom/bottom-sheet";
7
- import { ParamListBase, useTheme } from "@react-navigation/native";
8
- import * as React from "react";
9
- import { StatusBar } from "react-native";
10
1
  import Animated, {
11
2
  Easing,
12
3
  interpolate,
13
4
  interpolateColor,
14
5
  runOnJS,
6
+ SharedValue,
15
7
  useAnimatedReaction,
16
8
  useAnimatedStyle,
17
9
  useSharedValue,
18
10
  withSpring,
11
+ withTiming,
19
12
  } from "react-native-reanimated";
13
+ import {
14
+ BottomSheetBackdrop,
15
+ BottomSheetModal,
16
+ BottomSheetModalProvider,
17
+ } from "@gorhom/bottom-sheet";
20
18
  import { useSafeAreaInsets } from "react-native-safe-area-context";
19
+ import { ParamListBase, useTheme } from "@react-navigation/native";
20
+ import { StatusBar, ViewStyle } from "react-native";
21
+ import * as React from "react";
21
22
 
22
23
  import type {
24
+ BottomSheetDescriptor,
23
25
  BottomSheetDescriptorMap,
26
+ BottomSheetModalScreenProps,
24
27
  BottomSheetNavigationConfig,
25
28
  BottomSheetNavigationHelpers,
26
- BottomSheetNavigationProp,
27
29
  BottomSheetNavigationState,
30
+ BottomSheetRoute,
28
31
  } from "./types";
32
+ import { BottomSheetActions } from "./router";
29
33
 
30
- type BottomSheetModalScreenProps = BottomSheetModalProps & {
31
- navigation: BottomSheetNavigationProp<ParamListBase>;
32
- /**
33
- * When `true`, tapping on the backdrop will not dismiss the modal.
34
- * @default false
35
- */
36
- clickThrough?: boolean;
37
-
38
- /**
39
- * Opacity of the sheet's overlay.
40
- * @default 0.45
41
- */
42
- opacity?: number;
43
-
44
- /**
45
- * IOS modal sheet type of animation
46
- * @default false
47
- */
48
- iosModalSheetTypeOfAnimation?: boolean;
49
- };
34
+ const DEFAULT_SNAP_POINTS = ["66%"];
35
+
36
+ function AnimatedSheetWrapper({
37
+ route,
38
+ navigation,
39
+ descriptor,
40
+ isFullScreen,
41
+ previousIndex,
42
+ defaultStyle,
43
+ themeBackgroundStyle,
44
+ themeHandleIndicatorStyle,
45
+ }: {
46
+ route: BottomSheetRoute<ParamListBase>;
47
+ navigation: BottomSheetNavigationHelpers;
48
+ descriptor: BottomSheetDescriptor;
49
+ isFullScreen: SharedValue<number>;
50
+ previousIndex: SharedValue<number>;
51
+ defaultStyle: ViewStyle;
52
+ themeBackgroundStyle: ViewStyle;
53
+ themeHandleIndicatorStyle: ViewStyle;
54
+ }) {
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
+ }
50
160
 
51
161
  function BottomSheetModalScreen({
52
- index,
162
+ route,
53
163
  navigation,
54
164
  clickThrough,
55
- iosModalSheetTypeOfAnimation,
56
165
  opacity,
166
+ animatedIndex,
167
+ onChange,
57
168
  children,
58
169
  ...props
59
- }: BottomSheetModalScreenProps) {
170
+ }: BottomSheetModalScreenProps & { animatedIndex?: ReturnType<typeof useSharedValue> }) {
60
171
  const ref = React.useRef<BottomSheetModal>(null);
61
- const lastIndexRef = React.useRef(index);
172
+ const lastSnapIndexRef = React.useRef(route.snapToIndex ?? props.index ?? 0);
62
173
 
63
174
  // Present on mount.
64
175
  React.useEffect(() => {
65
176
  ref.current?.present();
66
177
  }, []);
67
178
 
179
+ // Track mount state to avoid dismissing on unmount
68
180
  const isMounted = React.useRef(true);
69
181
  React.useEffect(() => {
70
182
  return () => {
@@ -72,70 +184,87 @@ function BottomSheetModalScreen({
72
184
  };
73
185
  }, []);
74
186
 
187
+ // Handle route closing state
75
188
  React.useEffect(() => {
76
- if (index != null && lastIndexRef.current !== index) {
77
- ref.current?.snapToIndex(index);
189
+ if (route.closing) {
190
+ ref.current?.dismiss();
78
191
  }
79
- }, [index]);
192
+ }, [route.closing]);
80
193
 
81
- const onChange = React.useCallback(
194
+ // Handle snap point changes from navigation actions
195
+ React.useEffect(() => {
196
+ if (route.snapToIndex != null && route.snapToIndex !== lastSnapIndexRef.current) {
197
+ ref.current?.snapToIndex(route.snapToIndex);
198
+ lastSnapIndexRef.current = route.snapToIndex;
199
+ }
200
+ }, [route.snapToIndex, route.snapToKey]);
201
+
202
+ const handleChange = React.useCallback(
82
203
  (newIndex: number) => {
83
- const currentIndex = lastIndexRef.current;
84
- lastIndexRef.current = newIndex;
204
+ const currentIndex = lastSnapIndexRef.current;
205
+ lastSnapIndexRef.current = newIndex;
206
+
85
207
  if (newIndex >= 0 && newIndex !== currentIndex) {
86
- navigation.snapTo(newIndex);
208
+ navigation.dispatch(BottomSheetActions.snapTo(newIndex));
87
209
  }
88
210
  },
89
211
  [navigation],
90
212
  );
91
213
 
92
- const onDismiss = React.useCallback(() => {
93
- // BottomSheetModal will call onDismiss on unmount, be we do not want that since
94
- // we already popped the screen.
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
95
217
  if (isMounted.current) {
96
- navigation.goBack();
218
+ navigation.dispatch({
219
+ ...BottomSheetActions.remove(),
220
+ source: route.key,
221
+ });
97
222
  }
98
- }, [navigation]);
223
+ }, [navigation, route.key]);
99
224
 
100
225
  return (
101
226
  <BottomSheetModal
227
+ {...props}
102
228
  ref={ref}
103
- onDismiss={onDismiss}
104
- onChange={onChange}
105
- index={index}
106
- backdropComponent={(props) => (
229
+ onDismiss={handleDismiss}
230
+ onChange={handleChange}
231
+ animatedIndex={animatedIndex}
232
+ index={props.index}
233
+ backdropComponent={(backdropProps) => (
107
234
  <BottomSheetBackdrop
108
- {...props}
235
+ {...backdropProps}
109
236
  appearsOnIndex={0}
110
237
  disappearsOnIndex={-1}
111
238
  enableTouchThrough={!!clickThrough}
112
- opacity={opacity || 0.45}
239
+ opacity={opacity ?? 0.45}
113
240
  />
114
241
  )}
115
- {...props}
116
242
  >
117
243
  {children}
118
244
  </BottomSheetModal>
119
245
  );
120
246
  }
121
247
 
122
- const DEFAULT_SNAP_POINTS = ["66%"];
123
-
124
248
  type Props = BottomSheetNavigationConfig & {
125
249
  state: BottomSheetNavigationState<ParamListBase>;
126
250
  navigation: BottomSheetNavigationHelpers;
127
251
  descriptors: BottomSheetDescriptorMap;
128
252
  };
129
253
 
130
- export function BottomSheetView({ state, descriptors }: Props) {
254
+ export function BottomSheetView({ state, navigation, descriptors }: Props) {
131
255
  const { colors } = useTheme();
132
- const { top } = useSafeAreaInsets();
256
+ const { top, bottom, left, right } = useSafeAreaInsets();
257
+
133
258
  const themeBackgroundStyle = React.useMemo(
134
259
  () => ({
260
+ borderCurve: "continuous" as unknown as undefined,
135
261
  backgroundColor: colors.card,
262
+ borderTopLeftRadius: 20,
263
+ borderTopRightRadius: 20,
136
264
  }),
137
265
  [colors.card],
138
266
  );
267
+
139
268
  const themeHandleIndicatorStyle = React.useMemo(
140
269
  () => ({
141
270
  backgroundColor: colors.border,
@@ -145,33 +274,48 @@ export function BottomSheetView({ state, descriptors }: Props) {
145
274
  [colors.border],
146
275
  );
147
276
 
277
+ const defaultStyle = React.useMemo(
278
+ () => ({
279
+ paddingBottom: bottom,
280
+ paddingLeft: left,
281
+ paddingRight: right,
282
+ }),
283
+ [bottom, left, right],
284
+ );
285
+
148
286
  // IOS modal sheet type of animation
149
- const isFullScreen = useSharedValue(0);
287
+ const isFullScreen = useSharedValue(-1);
288
+ const previousIndex = useSharedValue(-1);
289
+
150
290
  const colorStyle = useAnimatedStyle(() => ({
151
291
  flex: 1,
152
- backgroundColor: interpolateColor(
153
- isFullScreen.value,
154
- [0, 1],
155
- ["transparent", "#000"],
292
+ backgroundColor: withSpring(
293
+ interpolateColor(isFullScreen.value, [0, 1], ["transparent", "#000"]),
294
+ { duration: 150 },
156
295
  ),
157
296
  }));
158
- const animatedStyle = useAnimatedStyle(() => ({
159
- flex: 1,
160
- transform: [
161
- {
162
- scaleX: withSpring(interpolate(isFullScreen.value, [0, 1], [1, 0.92]), {
163
- damping: 15,
164
- stiffness: 100,
165
- }),
166
- },
167
- {
168
- translateY: withSpring(interpolate(isFullScreen.value, [0, 1], [0, top + 5]), {
169
- damping: 15,
170
- stiffness: 100,
171
- }),
172
- },
173
- ],
174
- }));
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
+ );
175
319
 
176
320
  // Since background color is white, we need to set status bar to light
177
321
  const setStatusBar = StatusBar.setBarStyle;
@@ -186,76 +330,52 @@ export function BottomSheetView({ state, descriptors }: Props) {
186
330
  [],
187
331
  );
188
332
 
189
- // Avoid rendering provider if we only have one screen.
190
- const shouldRenderProvider = React.useRef(false);
191
- shouldRenderProvider.current = shouldRenderProvider.current || state.routes.length > 1;
192
-
193
- const firstRoute = state.routes[0];
194
- if (!firstRoute) {
195
- // no routes at all, probably shouldn't happen, but let's be defensive
333
+ // Get the base (first) route - this is the main content
334
+ const baseRoute = state.routes[0];
335
+ if (!baseRoute) {
196
336
  return null;
197
337
  }
198
338
 
199
- const firstDescriptor = descriptors[firstRoute.key];
200
- if (!firstDescriptor) {
201
- // if we don't have a descriptor for the first route, bail out
339
+ const baseDescriptor = descriptors[baseRoute.key];
340
+ if (!baseDescriptor) {
202
341
  return null;
203
342
  }
204
343
 
344
+ // Sheet routes are all routes after the base route
345
+ const sheetRoutes = state.routes.slice(1);
346
+ const hasSheets = sheetRoutes.length > 0;
347
+
205
348
  return (
206
349
  <>
350
+ {/* Base content with iOS modal animation */}
207
351
  <Animated.View style={colorStyle}>
208
- <Animated.View style={animatedStyle}>{firstDescriptor.render?.()}</Animated.View>
352
+ <Animated.View style={animatedStyle}>{baseDescriptor.render?.()}</Animated.View>
209
353
  </Animated.View>
210
- {shouldRenderProvider.current && (
354
+
355
+ {/* Bottom sheet modals */}
356
+ {hasSheets && (
211
357
  <BottomSheetModalProvider>
212
- {state.routes.slice(1).map((route) => {
358
+ {sheetRoutes.map((route) => {
359
+ // Skip routes that are being removed
360
+ if (route.closing && !descriptors[route.key]) {
361
+ return null;
362
+ }
363
+
213
364
  const descriptor = descriptors[route.key];
214
365
  if (!descriptor) return null;
215
366
 
216
- const { options, navigation, render } = descriptor;
217
- const {
218
- index,
219
- snapPoints,
220
- handleStyle,
221
- backgroundStyle,
222
- handleIndicatorStyle,
223
- enableDynamicSizing,
224
- ...sheetProps
225
- } = options;
226
-
227
367
  return (
228
- <BottomSheetModalScreen
368
+ <AnimatedSheetWrapper
229
369
  key={route.key}
230
- // Make sure index is in range, it could be out if snapToIndex is persisted
231
- // and snapPoints is changed.
232
- index={Math.min(
233
- route.snapToIndex ?? index ?? 0,
234
- !!snapPoints ? snapPoints.length - 1 : 0,
235
- )}
236
- snapPoints={
237
- !snapPoints && !enableDynamicSizing ? DEFAULT_SNAP_POINTS : snapPoints
238
- }
239
- onAnimate={(_, to) => {
240
- // @ts-ignore TODO: Fix types
241
- isFullScreen.value = ["%100", "100%"].includes(snapPoints?.[to])
242
- ? 1
243
- : 0;
244
- }}
245
- animationConfigs={{
246
- duration: 300,
247
- easing: Easing.bezier(0.25, 0.1, 0.25, 1),
248
- }}
249
- topInset={top + 18}
370
+ route={route}
250
371
  navigation={navigation}
251
- enableDynamicSizing={enableDynamicSizing}
252
- backgroundStyle={[themeBackgroundStyle, backgroundStyle]}
253
- handleIndicatorStyle={[themeHandleIndicatorStyle, handleIndicatorStyle]}
254
- handleStyle={[themeBackgroundStyle, { borderRadius: 20 }, handleStyle]}
255
- {...sheetProps}
256
- >
257
- {render?.()}
258
- </BottomSheetModalScreen>
372
+ descriptor={descriptor}
373
+ isFullScreen={isFullScreen}
374
+ previousIndex={previousIndex}
375
+ defaultStyle={defaultStyle}
376
+ themeBackgroundStyle={themeBackgroundStyle}
377
+ themeHandleIndicatorStyle={themeHandleIndicatorStyle}
378
+ />
259
379
  );
260
380
  })}
261
381
  </BottomSheetModalProvider>