@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
package/src/sheet.tsx CHANGED
@@ -19,20 +19,18 @@ import {
19
19
  type NativeEventSubscription,
20
20
  } from "react-native";
21
21
  import {
22
- Easing,
22
+ interpolate,
23
23
  useAnimatedReaction,
24
24
  useSharedValue,
25
- withTiming,
26
25
  } from "react-native-reanimated";
27
26
  import { useSafeAreaInsets } from "react-native-safe-area-context";
28
- import { useTheme } from "@react-navigation/native";
29
27
  import React from "react";
30
28
 
31
29
  import {
32
30
  useProviderContext,
33
- useSheetAnimationContext,
34
31
  useSheetIDContext,
35
32
  useSheetRef,
33
+ useSheetSharedContext,
36
34
  useStackBehaviorContext,
37
35
  } from "./provider";
38
36
  import { BottomSheetInstance, BottomSheetProps, SheetIds, StackBehavior } from "./types";
@@ -42,7 +40,7 @@ import { eventManager } from "./events";
42
40
  interface BottomSheetFC
43
41
  extends React.MemoExoticComponent<React.ForwardRefExoticComponent<BottomSheetProps>> {
44
42
  <Id extends SheetIds>(
45
- props: BottomSheetProps & React.RefAttributes<BottomSheetInstance<Id>>,
43
+ props: BottomSheetProps<Id> & React.RefAttributes<BottomSheetInstance<Id>>,
46
44
  ): React.JSX.Element;
47
45
 
48
46
  // Components
@@ -58,6 +56,9 @@ interface BottomSheetFC
58
56
  TextInput: typeof BottomSheetTextInput;
59
57
  }
60
58
 
59
+ const FULL_SCREEN_POINTS: (string | number)[] =
60
+ Platform.OS === "ios" ? ["%90", "90%"] : ["%93", "93%"];
61
+
61
62
  const useSheetManager = ({
62
63
  id,
63
64
  onHide,
@@ -96,6 +97,7 @@ const useSheetManager = ({
96
97
  },
97
98
  ),
98
99
  ];
100
+
99
101
  return () => {
100
102
  hasShownRef.current = false;
101
103
  subscriptions.forEach((s) => s?.unsubscribe?.());
@@ -107,22 +109,21 @@ const BottomSheetComponent = React.forwardRef<BottomSheetInstance, BottomSheetPr
107
109
  (
108
110
  {
109
111
  children,
110
- snapPoints,
111
112
  onClose,
112
113
  onBeforeShow,
113
114
  stackBehavior = "switch",
114
115
  hardwareBackPressToClose = true,
115
116
  enableDynamicSizing = false,
116
117
  handleIndicatorStyle,
117
- iosModalSheetTypeOfAnimation: pageAnimation,
118
+ iosModalSheetTypeOfAnimation,
119
+ snapPoints: defaultSnapPoints,
118
120
  animatedIndex: defaultAnimatedIndex,
119
121
  backgroundStyle,
120
122
  onAnimate,
121
123
  handleStyle,
122
124
  style,
123
- clickThrough,
125
+ passThrough,
124
126
  opacity,
125
-
126
127
  ...props
127
128
  },
128
129
  ref,
@@ -131,45 +132,39 @@ const BottomSheetComponent = React.forwardRef<BottomSheetInstance, BottomSheetPr
131
132
  const currentCtx = useProviderContext();
132
133
  const stackContext = useStackBehaviorContext();
133
134
 
134
- const { isFullScreen, iosModalSheetTypeOfAnimation, duration } =
135
- useSheetAnimationContext();
136
135
  const animatedIndex = useSharedValue(0);
137
- const previousIndex = useSharedValue(-1);
138
136
 
139
137
  const [currentStackBehavior, setCurrentStackBehavior] =
140
138
  React.useState<StackBehavior>(stackBehavior);
141
139
  const isPushed = currentStackBehavior === "push";
142
140
 
143
- const { colors } = useTheme();
144
141
  const { bottom, left, right } = useSafeAreaInsets();
145
142
 
146
- const themeBackgroundStyle = React.useMemo(
147
- () => ({
148
- borderCurve: "continuous" as unknown as undefined,
149
- backgroundColor: colors.card,
150
- borderTopLeftRadius: 20,
151
- borderTopRightRadius: 20,
152
- }),
153
- [colors.card],
154
- );
155
- const themeHandleIndicatorStyle = React.useMemo(
156
- () => ({
157
- backgroundColor: colors.border,
158
- height: 5,
159
- width: 50,
160
- }),
161
- [colors.border],
162
- );
163
-
164
143
  const defaultStyle = React.useMemo(
165
- () => ({
166
- paddingBottom: bottom,
167
- paddingLeft: left,
168
- paddingRight: right,
169
- }),
144
+ () => ({ paddingBottom: bottom, paddingLeft: left, paddingRight: right }),
170
145
  [bottom, left, right],
171
146
  );
172
147
 
148
+ const { isFullScreen } = useSheetSharedContext();
149
+ const [snapPoints, fullScreenIndex] = React.useMemo(() => {
150
+ let resolved = defaultSnapPoints;
151
+
152
+ if (
153
+ Platform.OS === "android" &&
154
+ iosModalSheetTypeOfAnimation &&
155
+ Array.isArray(resolved)
156
+ ) {
157
+ resolved = resolved.map((p) => (p === "90%" || p === "%90" ? "93%" : p));
158
+ }
159
+
160
+ const fullScreenIndex =
161
+ resolved instanceof Array
162
+ ? resolved.findIndex((p) => FULL_SCREEN_POINTS.includes(p))
163
+ : resolved?.value?.findIndex((p) => FULL_SCREEN_POINTS.includes(p)) || -1;
164
+
165
+ return [resolved, fullScreenIndex] as const;
166
+ }, [defaultSnapPoints, iosModalSheetTypeOfAnimation]);
167
+
173
168
  const valueRef = React.useRef<unknown>(null);
174
169
  const bottomSheetRef = React.useRef<BottomSheetModal>(null);
175
170
  const hardwareBackPressEvent = React.useRef<NativeEventSubscription>(
@@ -179,15 +174,27 @@ const BottomSheetComponent = React.forwardRef<BottomSheetInstance, BottomSheetPr
179
174
  const id = useSheetIDContext();
180
175
  const sheetId = props.id || id;
181
176
 
177
+ const hideSheetRef = React.useRef<
178
+ (
179
+ data?: unknown,
180
+ fromManager?: boolean,
181
+ dismiss?: boolean,
182
+ incomingBehavior?: StackBehavior,
183
+ ) => void
184
+ >(null!);
185
+
182
186
  useSheetManager({
183
187
  id: sheetId,
184
188
  onHide: (data, dismiss, behavior) => {
189
+ // Update state for future renders, but also pass behavior directly
190
+ // so hideSheet doesn't read a stale closure value (React state update
191
+ // is async — hideSheet runs before the re-render).
185
192
  if (behavior) setCurrentStackBehavior(behavior);
186
- hideSheet(data, true, dismiss);
193
+ hideSheetRef.current(data, true, dismiss, behavior);
187
194
  },
188
195
  onBeforeShow: (data, behavior) => {
189
196
  if (behavior) setCurrentStackBehavior(behavior);
190
- onBeforeShow?.(data);
197
+ onBeforeShow?.(data as never);
191
198
  valueRef.current = undefined;
192
199
  currentSheetRef.current = getInstance();
193
200
  },
@@ -204,98 +211,90 @@ const BottomSheetComponent = React.forwardRef<BottomSheetInstance, BottomSheetPr
204
211
  useAnimatedReaction(
205
212
  () => animatedIndex.value,
206
213
  (index) => {
207
- ("worklet");
214
+ "worklet";
208
215
  if (defaultAnimatedIndex) {
209
216
  defaultAnimatedIndex.set(index);
210
217
  }
211
218
 
212
- if (!iosModalSheetTypeOfAnimation && !pageAnimation) {
213
- if (isFullScreen.value > 0) isFullScreen.set(0);
214
- previousIndex.set(index);
215
- return;
216
- }
217
-
218
- if (isFullScreen.value < 0) {
219
- isFullScreen.set(0);
220
- }
221
-
222
- const isClosing =
223
- index < 0 || (previousIndex.value >= 0 && index < previousIndex.value - 0.05);
224
- previousIndex.set(index);
225
-
226
- if (isClosing) {
227
- if (isFullScreen.value > 0.01) {
228
- isFullScreen.set(
229
- withTiming(0, {
230
- duration: duration * 0.85,
231
- easing: Easing.bezier(0.25, 0.1, 0.25, 1),
232
- }),
233
- );
234
- }
235
- return;
236
- }
237
-
238
- const points: (string | number)[] = ["%90", "90%"];
239
- const fullScreenIndex =
240
- snapPoints instanceof Array
241
- ? snapPoints.findIndex((p) => points.includes(p))
242
- : snapPoints?.value?.findIndex((p) => points.includes(p)) || -1;
243
-
244
- if (index >= fullScreenIndex - 0.5 && index <= fullScreenIndex + 0.5) {
245
- isFullScreen.set(1);
246
- } else if (index >= 0) {
247
- isFullScreen.set(0);
219
+ if (iosModalSheetTypeOfAnimation) {
220
+ isFullScreen.set(
221
+ interpolate(
222
+ index,
223
+ [fullScreenIndex - 1, fullScreenIndex, fullScreenIndex + 1],
224
+ [0, 1, 0],
225
+ ),
226
+ );
248
227
  }
249
228
  },
250
- [snapPoints, iosModalSheetTypeOfAnimation, pageAnimation, duration],
229
+ [iosModalSheetTypeOfAnimation],
251
230
  );
252
231
 
253
232
  const hideSheet = React.useCallback(
254
- (data?: unknown, fromManager?: boolean, dismiss?: boolean) => {
233
+ (
234
+ data?: unknown,
235
+ fromManager?: boolean,
236
+ dismiss?: boolean,
237
+ incomingBehavior?: StackBehavior,
238
+ ) => {
239
+ // Use the freshly-delivered behavior from the event when available.
240
+ // currentStackBehavior comes from React state which may not have flushed
241
+ // yet when this callback fires synchronously from the manager.
242
+ const activeBehavior = incomingBehavior ?? currentStackBehavior;
243
+
255
244
  let value = data ?? valueRef.current;
256
245
 
257
246
  hardwareBackPressEvent.current?.remove();
258
247
 
259
- const closeValue = onClose?.(value);
248
+ const closeValue = onClose?.(value as never);
260
249
  if (closeValue !== undefined) value = closeValue;
261
250
 
262
- if (dismiss && currentStackBehavior === "push") {
251
+ if (dismiss && activeBehavior === "push") {
252
+ // For push behavior, a "dismiss" event means another sheet wants to
253
+ // appear on top — do not close this sheet.
263
254
  if (fromManager) valueRef.current = data;
264
255
  return;
265
256
  }
266
257
 
267
- if (currentStackBehavior !== "replace" || !dismiss) {
258
+ if (activeBehavior !== "replace" || !dismiss) {
268
259
  bottomSheetRef.current?.close();
269
260
  }
270
261
 
271
262
  if (sheetId) {
272
263
  const hasHistory = PrivateManager.history.length > 0;
273
- const shouldRestorePrevious = currentStackBehavior !== "replace";
264
+ const shouldRestorePrevious = activeBehavior !== "replace";
274
265
 
275
266
  eventManager.publish(
276
267
  `onclose_${sheetId}`,
277
268
  value,
278
269
  currentCtx,
279
270
  hasHistory || !!dismiss,
280
- currentStackBehavior,
271
+ activeBehavior,
281
272
  );
282
273
 
283
274
  if (shouldRestorePrevious) {
284
275
  if (dismiss) {
285
- PrivateManager.history.push({
286
- id: sheetId,
287
- context: currentCtx,
288
- behavior: currentStackBehavior,
289
- });
276
+ // it will surface naturally when the push sheet is closed.
277
+ if (activeBehavior !== "push") {
278
+ PrivateManager.history.push({
279
+ id: sheetId,
280
+ context: currentCtx,
281
+ behavior: activeBehavior,
282
+ });
283
+ }
290
284
  } else if (hasHistory) {
291
- const prev = PrivateManager.history.pop()!;
292
- eventManager.publish(
293
- `show_wrap_${prev.id}`,
294
- undefined,
295
- prev.context,
296
- true,
297
- prev.behavior,
285
+ const otherSheetsStillOpen = PrivateManager.stack().some(
286
+ (s) => !(s.id === sheetId && s.context === currentCtx),
298
287
  );
288
+ if (!otherSheetsStillOpen) {
289
+ const prev = PrivateManager.history.pop()!;
290
+ eventManager.publish(
291
+ `show_wrap_${prev.id}`,
292
+ undefined,
293
+ prev.context,
294
+ true,
295
+ prev.behavior,
296
+ );
297
+ }
299
298
  }
300
299
  }
301
300
 
@@ -307,6 +306,10 @@ const BottomSheetComponent = React.forwardRef<BottomSheetInstance, BottomSheetPr
307
306
  [sheetId, currentCtx, onClose, currentStackBehavior],
308
307
  );
309
308
 
309
+ React.useEffect(() => {
310
+ hideSheetRef.current = hideSheet;
311
+ }, [hideSheet]);
312
+
310
313
  const getInstance = React.useCallback(
311
314
  (): BottomSheetInstance => ({
312
315
  close(options = {}): void {
@@ -331,9 +334,7 @@ const BottomSheetComponent = React.forwardRef<BottomSheetInstance, BottomSheetPr
331
334
 
332
335
  React.useEffect(() => {
333
336
  if (sheetId) {
334
- PrivateManager.registerRef(sheetId, currentCtx, {
335
- current: getInstance(),
336
- } as React.RefObject<BottomSheetInstance>);
337
+ PrivateManager.registerRef(sheetId, currentCtx, { current: getInstance() });
337
338
  }
338
339
  currentSheetRef.current = getInstance();
339
340
  }, [currentCtx, getInstance, sheetId, currentSheetRef]);
@@ -343,7 +344,9 @@ const BottomSheetComponent = React.forwardRef<BottomSheetInstance, BottomSheetPr
343
344
  hardwareBackPressEvent.current = BackHandler.addEventListener(
344
345
  "hardwareBackPress",
345
346
  () => {
346
- bottomSheetRef.current?.close();
347
+ // Go through hideSheet so internal state (PrivateManager, events,
348
+ // history) is updated correctly — not just the visual sheet.
349
+ hideSheetRef.current(undefined, true, false);
347
350
  return true;
348
351
  },
349
352
  );
@@ -354,13 +357,10 @@ const BottomSheetComponent = React.forwardRef<BottomSheetInstance, BottomSheetPr
354
357
 
355
358
  React.useImperativeHandle(ref, getInstance, [getInstance]);
356
359
 
357
- const zIndex = React.useMemo(() => {
358
- if (!sheetId) return 0;
359
- if (isPushed) {
360
- return PrivateManager.zIndex(sheetId, currentCtx);
361
- }
362
- return 0;
363
- }, [sheetId, isPushed, currentCtx]);
360
+ const zIndex = React.useMemo(
361
+ () => (isPushed && sheetId ? PrivateManager.zIndex(sheetId, currentCtx) : 0),
362
+ [sheetId, isPushed, currentCtx],
363
+ );
364
364
 
365
365
  const backdropOpacity = React.useMemo(() => {
366
366
  if (isPushed && stackContext.previousSheetId) {
@@ -375,37 +375,25 @@ const BottomSheetComponent = React.forwardRef<BottomSheetInstance, BottomSheetPr
375
375
  enableDynamicSizing={enableDynamicSizing}
376
376
  backdropComponent={(backdropProps) => (
377
377
  <BottomSheetBackdrop
378
- enableTouchThrough={!!clickThrough}
378
+ enableTouchThrough={!!passThrough}
379
379
  opacity={backdropOpacity}
380
380
  disappearsOnIndex={-1}
381
381
  appearsOnIndex={0}
382
382
  {...backdropProps}
383
383
  />
384
384
  )}
385
- onAnimate={(from, to, ...args) => {
386
- const snapPointLen = Array.isArray(snapPoints)
387
- ? snapPoints.length
388
- : (snapPoints?.value?.length ?? 0);
389
-
390
- if (to >= isFullScreen.value && to > snapPointLen - 1) {
391
- isFullScreen.set(0);
392
- } else if (to > 0 && to === previousIndex.value && isFullScreen.value === 0) {
393
- isFullScreen.set(1);
394
- }
395
-
396
- onAnimate?.(from, to, ...args);
397
- }}
398
385
  topInset={0}
399
386
  bottomInset={0}
400
387
  {...props}
401
388
  ref={bottomSheetRef}
402
389
  onClose={hideSheet}
390
+ onAnimate={onAnimate}
403
391
  animatedIndex={animatedIndex}
404
392
  style={[defaultStyle, style]}
405
- snapPoints={enableDynamicSizing ? undefined : (snapPoints ?? ["66%"])}
406
- handleIndicatorStyle={[themeHandleIndicatorStyle, handleIndicatorStyle]}
407
- backgroundStyle={[themeBackgroundStyle, backgroundStyle]}
408
- handleStyle={[themeBackgroundStyle, handleStyle]}
393
+ snapPoints={enableDynamicSizing ? undefined : snapPoints}
394
+ handleIndicatorStyle={handleIndicatorStyle}
395
+ backgroundStyle={backgroundStyle}
396
+ handleStyle={handleStyle}
409
397
  >
410
398
  {children}
411
399
  </RNBottomSheet>