react-native-bottom-sheet-stack 1.0.2 → 1.1.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 (86) hide show
  1. package/README.md +262 -50
  2. package/lib/commonjs/BottomSheet.context.js.map +1 -1
  3. package/lib/commonjs/BottomSheetBackdrop.js +76 -0
  4. package/lib/commonjs/BottomSheetBackdrop.js.map +1 -0
  5. package/lib/commonjs/BottomSheetHost.js +280 -38
  6. package/lib/commonjs/BottomSheetHost.js.map +1 -1
  7. package/lib/commonjs/BottomSheetManaged.js +128 -38
  8. package/lib/commonjs/BottomSheetManaged.js.map +1 -1
  9. package/lib/commonjs/BottomSheetManager.context.js.map +1 -1
  10. package/lib/commonjs/BottomSheetManager.provider.js +41 -13
  11. package/lib/commonjs/BottomSheetManager.provider.js.map +1 -1
  12. package/lib/commonjs/BottomSheetPortal.js +46 -0
  13. package/lib/commonjs/BottomSheetPortal.js.map +1 -0
  14. package/lib/commonjs/BottomSheetScaleView.js +67 -0
  15. package/lib/commonjs/BottomSheetScaleView.js.map +1 -0
  16. package/lib/commonjs/animatedRegistry.js +25 -0
  17. package/lib/commonjs/animatedRegistry.js.map +1 -0
  18. package/lib/commonjs/bottomSheet.store.js +19 -0
  19. package/lib/commonjs/bottomSheet.store.js.map +1 -1
  20. package/lib/commonjs/bottomSheetCoordinator.js +5 -6
  21. package/lib/commonjs/bottomSheetCoordinator.js.map +1 -1
  22. package/lib/commonjs/index.js +17 -3
  23. package/lib/commonjs/index.js.map +1 -1
  24. package/lib/commonjs/portal.types.js +2 -0
  25. package/lib/commonjs/portal.types.js.map +1 -0
  26. package/lib/commonjs/useBottomSheetControl.js +81 -0
  27. package/lib/commonjs/useBottomSheetControl.js.map +1 -0
  28. package/lib/commonjs/useBottomSheetManager.js +88 -44
  29. package/lib/commonjs/useBottomSheetManager.js.map +1 -1
  30. package/lib/commonjs/useBottomSheetState.js +40 -10
  31. package/lib/commonjs/useBottomSheetState.js.map +1 -1
  32. package/lib/commonjs/useScaleAnimation.js +108 -0
  33. package/lib/commonjs/useScaleAnimation.js.map +1 -0
  34. package/lib/typescript/example/src/App.d.ts +0 -2
  35. package/lib/typescript/example/src/App.d.ts.map +1 -1
  36. package/lib/typescript/src/BottomSheetBackdrop.d.ts +12 -0
  37. package/lib/typescript/src/BottomSheetBackdrop.d.ts.map +1 -0
  38. package/lib/typescript/src/BottomSheetHost.d.ts +1 -2
  39. package/lib/typescript/src/BottomSheetHost.d.ts.map +1 -1
  40. package/lib/typescript/src/BottomSheetManaged.d.ts.map +1 -1
  41. package/lib/typescript/src/BottomSheetManager.context.d.ts +2 -0
  42. package/lib/typescript/src/BottomSheetManager.context.d.ts.map +1 -1
  43. package/lib/typescript/src/BottomSheetManager.provider.d.ts +4 -3
  44. package/lib/typescript/src/BottomSheetManager.provider.d.ts.map +1 -1
  45. package/lib/typescript/src/BottomSheetPortal.d.ts +9 -0
  46. package/lib/typescript/src/BottomSheetPortal.d.ts.map +1 -0
  47. package/lib/typescript/src/BottomSheetScaleView.d.ts +18 -0
  48. package/lib/typescript/src/BottomSheetScaleView.d.ts.map +1 -0
  49. package/lib/typescript/src/animatedRegistry.d.ts +4 -0
  50. package/lib/typescript/src/animatedRegistry.d.ts.map +1 -0
  51. package/lib/typescript/src/bottomSheet.store.d.ts +9 -3
  52. package/lib/typescript/src/bottomSheet.store.d.ts.map +1 -1
  53. package/lib/typescript/src/bottomSheetCoordinator.d.ts.map +1 -1
  54. package/lib/typescript/src/index.d.ts +5 -1
  55. package/lib/typescript/src/index.d.ts.map +1 -1
  56. package/lib/typescript/src/portal.types.d.ts +24 -0
  57. package/lib/typescript/src/portal.types.d.ts.map +1 -0
  58. package/lib/typescript/src/useBottomSheetControl.d.ts +10 -0
  59. package/lib/typescript/src/useBottomSheetControl.d.ts.map +1 -0
  60. package/lib/typescript/src/useBottomSheetManager.d.ts +2 -0
  61. package/lib/typescript/src/useBottomSheetManager.d.ts.map +1 -1
  62. package/lib/typescript/src/useBottomSheetState.d.ts.map +1 -1
  63. package/lib/typescript/src/useScaleAnimation.d.ts +43 -0
  64. package/lib/typescript/src/useScaleAnimation.d.ts.map +1 -0
  65. package/package.json +11 -2
  66. package/src/BottomSheetBackdrop.tsx +61 -0
  67. package/src/BottomSheetHost.tsx +74 -15
  68. package/src/BottomSheetManaged.tsx +26 -33
  69. package/src/BottomSheetManager.context.tsx +2 -0
  70. package/src/BottomSheetManager.provider.tsx +15 -8
  71. package/src/BottomSheetPortal.tsx +39 -0
  72. package/src/BottomSheetScaleView.tsx +41 -0
  73. package/src/animatedRegistry.ts +22 -0
  74. package/src/bottomSheet.store.ts +150 -123
  75. package/src/bottomSheetCoordinator.ts +5 -6
  76. package/src/index.tsx +8 -4
  77. package/src/portal.types.ts +25 -0
  78. package/src/useBottomSheetControl.ts +52 -0
  79. package/src/useBottomSheetManager.tsx +37 -48
  80. package/src/useBottomSheetState.ts +2 -6
  81. package/src/useScaleAnimation.ts +114 -0
  82. package/lib/commonjs/ScaleBackgroundWrapper.js +0 -71
  83. package/lib/commonjs/ScaleBackgroundWrapper.js.map +0 -1
  84. package/lib/typescript/src/ScaleBackgroundWrapper.d.ts +0 -32
  85. package/lib/typescript/src/ScaleBackgroundWrapper.d.ts.map +0 -1
  86. package/src/ScaleBackgroundWrapper.tsx +0 -97
@@ -0,0 +1,61 @@
1
+ import { Pressable, StyleSheet } from 'react-native';
2
+ import Animated, {
3
+ Extrapolation,
4
+ interpolate,
5
+ useAnimatedStyle,
6
+ } from 'react-native-reanimated';
7
+ import { getAnimatedIndex } from './animatedRegistry';
8
+ import { useBottomSheetStore } from './bottomSheet.store';
9
+
10
+ interface BottomSheetBackdropProps {
11
+ sheetId: string;
12
+ onPress?: () => void;
13
+ }
14
+
15
+ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
16
+
17
+ /**
18
+ * Custom backdrop component rendered separately from the scaled sheet content.
19
+ * This ensures the backdrop doesn't scale with the sheet.
20
+ * Opacity is interpolated from the bottom sheet's animatedIndex for smooth animation.
21
+ */
22
+ export function BottomSheetBackdrop({
23
+ sheetId,
24
+ onPress,
25
+ }: BottomSheetBackdropProps) {
26
+ const status = useBottomSheetStore(
27
+ (state) => state.sheetsById[sheetId]?.status
28
+ );
29
+
30
+ const animatedIndex = getAnimatedIndex(sheetId);
31
+
32
+ const isVisible = status === 'opening' || status === 'open';
33
+
34
+ const animatedStyle = useAnimatedStyle(() => {
35
+ // Interpolate opacity based on animatedIndex
36
+ // -1 = closed, 0+ = open at snap point
37
+ const opacity = interpolate(
38
+ animatedIndex.value,
39
+ [-1, 0],
40
+ [0, 1],
41
+ Extrapolation.CLAMP
42
+ );
43
+
44
+ return { opacity };
45
+ });
46
+
47
+ return (
48
+ <AnimatedPressable
49
+ style={[styles.backdrop, animatedStyle]}
50
+ onPress={onPress}
51
+ pointerEvents={isVisible ? 'auto' : 'none'}
52
+ />
53
+ );
54
+ }
55
+
56
+ const styles = StyleSheet.create({
57
+ backdrop: {
58
+ ...StyleSheet.absoluteFillObject,
59
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
60
+ },
61
+ });
@@ -1,18 +1,43 @@
1
- import React, { useEffect, useMemo } from 'react';
1
+ import { useEffect } from 'react';
2
2
  import { StyleSheet, View } from 'react-native';
3
+ import Animated from 'react-native-reanimated';
3
4
  import { useSafeAreaFrame } from 'react-native-safe-area-context';
5
+ import { PortalHost } from 'react-native-teleport';
4
6
 
5
7
  import { shallow } from 'zustand/shallow';
8
+ import { cleanupAnimatedIndex } from './animatedRegistry';
9
+ import { BottomSheetBackdrop } from './BottomSheetBackdrop';
6
10
  import { BottomSheetContext } from './BottomSheet.context';
7
11
  import { useBottomSheetStore } from './bottomSheet.store';
8
12
  import { useBottomSheetManagerContext } from './BottomSheetManager.provider';
9
13
  import { initBottomSheetCoordinator } from './bottomSheetCoordinator';
14
+ import {
15
+ useScaleAnimatedStyle,
16
+ useScaleDepth,
17
+ type ScaleConfig,
18
+ } from './useScaleAnimation';
19
+
20
+ function PortalHostWrapper({
21
+ id,
22
+ width,
23
+ height,
24
+ }: {
25
+ id: string;
26
+ width: number;
27
+ height: number;
28
+ }) {
29
+ return (
30
+ <View style={{ flex: 1, width, height }}>
31
+ <PortalHost name={`bottomsheet-${id}`} style={{ width, height }} />
32
+ </View>
33
+ );
34
+ }
10
35
 
11
36
  function BottomSheetHostComp() {
12
37
  const queueIds = useQueueIds();
13
38
  const clearAll = useBottomSheetStore((store) => store.clearAll);
14
39
 
15
- const { groupId } = useBottomSheetManagerContext();
40
+ const { groupId, scaleConfig } = useBottomSheetManagerContext();
16
41
 
17
42
  useEffect(() => {
18
43
  const unsubscribe = initBottomSheetCoordinator(groupId);
@@ -30,35 +55,66 @@ function BottomSheetHostComp() {
30
55
  return (
31
56
  <>
32
57
  {queueIds.map((id) => (
33
- <QueueItem key={id} id={id} />
58
+ <QueueItem
59
+ key={id}
60
+ id={id}
61
+ groupId={groupId}
62
+ scaleConfig={scaleConfig}
63
+ />
34
64
  ))}
35
65
  </>
36
66
  );
37
67
  }
38
68
 
39
- const QueueItem = React.memo(({ id }: { id: string }) => {
40
- const content = useBottomSheetStore((state) => state.sheetsById[id]?.content);
69
+ function QueueItem({
70
+ id,
71
+ groupId,
72
+ scaleConfig,
73
+ }: {
74
+ id: string;
75
+ groupId: string;
76
+ scaleConfig?: ScaleConfig;
77
+ }) {
78
+ const sheet = useBottomSheetStore((state) => state.sheetsById[id]);
41
79
 
42
80
  const { width, height } = useSafeAreaFrame();
43
- const value = useMemo(() => ({ id }), [id]);
81
+ const value = { id };
82
+
83
+ const scaleDepth = useScaleDepth(groupId, id);
84
+ const scaleStyle = useScaleAnimatedStyle(scaleDepth, scaleConfig);
85
+
86
+ // Cleanup animated index when sheet is unmounted
87
+ useEffect(() => {
88
+ return () => {
89
+ cleanupAnimatedIndex(id);
90
+ };
91
+ }, [id]);
44
92
 
45
93
  return (
46
94
  <BottomSheetContext.Provider value={value}>
47
- <View
95
+ {/* Backdrop - rendered without scaling */}
96
+ <View style={[StyleSheet.absoluteFillObject, styles.backdropContainer]}>
97
+ <BottomSheetBackdrop sheetId={id} />
98
+ </View>
99
+
100
+ {/* Sheet content - rendered with scaling */}
101
+ <Animated.View
48
102
  style={[
49
103
  StyleSheet.absoluteFillObject,
50
104
  styles.container,
51
- {
52
- width,
53
- height,
54
- },
105
+ { width, height },
106
+ scaleStyle,
55
107
  ]}
56
108
  >
57
- {content}
58
- </View>
109
+ {sheet?.usePortal ? (
110
+ <PortalHostWrapper id={id} width={width} height={height} />
111
+ ) : (
112
+ sheet?.content
113
+ )}
114
+ </Animated.View>
59
115
  </BottomSheetContext.Provider>
60
116
  );
61
- });
117
+ }
62
118
 
63
119
  const useQueueIds = () => {
64
120
  const { groupId } = useBottomSheetManagerContext();
@@ -72,9 +128,12 @@ const useQueueIds = () => {
72
128
  );
73
129
  };
74
130
 
75
- export const BottomSheetHost = React.memo(BottomSheetHostComp);
131
+ export const BottomSheetHost = BottomSheetHostComp;
76
132
 
77
133
  const styles = StyleSheet.create({
134
+ backdropContainer: {
135
+ zIndex: 99_999_999,
136
+ },
78
137
  container: {
79
138
  zIndex: 100_000_000,
80
139
  pointerEvents: 'box-none',
@@ -1,12 +1,11 @@
1
1
  import BottomSheetOriginal, {
2
- BottomSheetBackdrop,
3
2
  useBottomSheetSpringConfigs,
4
- type BottomSheetBackdropProps,
5
3
  type BottomSheetProps,
6
4
  } from '@gorhom/bottom-sheet';
7
5
  import { type BottomSheetMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
8
- import React, { useCallback, useMemo } from 'react';
6
+ import React from 'react';
9
7
 
8
+ import { getAnimatedIndex } from './animatedRegistry';
10
9
  import { createSheetEventHandlers } from './bottomSheetCoordinator';
11
10
  import { useBottomSheetState } from './useBottomSheetState';
12
11
 
@@ -14,6 +13,9 @@ export interface BottomSheetRef extends BottomSheetMethods {}
14
13
 
15
14
  interface BottomSheetManagedProps extends BottomSheetProps {}
16
15
 
16
+ // Null backdrop - we render our own backdrop separately in BottomSheetHost
17
+ const nullBackdrop = () => null;
18
+
17
19
  export const BottomSheetManaged = React.forwardRef<
18
20
  BottomSheetRef,
19
21
  BottomSheetManagedProps
@@ -24,35 +26,36 @@ export const BottomSheetManaged = React.forwardRef<
24
26
  onAnimate,
25
27
  onClose,
26
28
  enablePanDownToClose = true,
27
- backdropComponent,
29
+ backdropComponent = nullBackdrop,
30
+ animatedIndex: externalAnimatedIndex,
28
31
  ...props
29
32
  },
30
33
  ref
31
34
  ) => {
32
35
  const { bottomSheetState } = useBottomSheetState();
33
36
 
34
- const { handleAnimate, handleClose } = useMemo(
35
- () => createSheetEventHandlers(bottomSheetState.id),
36
- [bottomSheetState.id]
37
- );
37
+ // Get or create shared animated index for this sheet
38
+ const animatedIndex =
39
+ externalAnimatedIndex ?? getAnimatedIndex(bottomSheetState.id);
38
40
 
39
- const wrappedOnAnimate: BottomSheetProps['onAnimate'] = useCallback(
40
- (
41
- fromIndex: number,
42
- toIndex: number,
43
- fromPosition: number,
44
- toPosition: number
45
- ) => {
46
- handleAnimate(fromIndex, toIndex);
47
- onAnimate?.(fromIndex, toIndex, fromPosition, toPosition);
48
- },
49
- [handleAnimate, onAnimate]
41
+ const { handleAnimate, handleClose } = createSheetEventHandlers(
42
+ bottomSheetState.id
50
43
  );
51
44
 
52
- const wrappedOnClose = useCallback(() => {
45
+ const wrappedOnAnimate: BottomSheetProps['onAnimate'] = (
46
+ fromIndex: number,
47
+ toIndex: number,
48
+ fromPosition: number,
49
+ toPosition: number
50
+ ) => {
51
+ handleAnimate(fromIndex, toIndex);
52
+ onAnimate?.(fromIndex, toIndex, fromPosition, toPosition);
53
+ };
54
+
55
+ const wrappedOnClose = () => {
53
56
  onClose?.();
54
57
  handleClose();
55
- }, [handleClose, onClose]);
58
+ };
56
59
 
57
60
  const config = useBottomSheetSpringConfigs({
58
61
  stiffness: 400,
@@ -60,25 +63,15 @@ export const BottomSheetManaged = React.forwardRef<
60
63
  mass: 0.7,
61
64
  });
62
65
 
63
- const renderBackdropComponent = useCallback(
64
- (backdropProps: BottomSheetBackdropProps) => (
65
- <BottomSheetBackdrop
66
- {...backdropProps}
67
- disappearsOnIndex={-1}
68
- appearsOnIndex={0}
69
- />
70
- ),
71
- []
72
- );
73
-
74
66
  return (
75
67
  <BottomSheetOriginal
76
68
  animationConfigs={config}
77
69
  ref={ref}
78
70
  {...props}
71
+ animatedIndex={animatedIndex}
79
72
  onClose={wrappedOnClose}
80
73
  onAnimate={wrappedOnAnimate}
81
- backdropComponent={backdropComponent || renderBackdropComponent}
74
+ backdropComponent={backdropComponent}
82
75
  enablePanDownToClose={enablePanDownToClose}
83
76
  >
84
77
  {children}
@@ -1,7 +1,9 @@
1
1
  import React from 'react';
2
+ import type { ScaleConfig } from './useScaleAnimation';
2
3
 
3
4
  export interface BottomSheetManagerContextValue {
4
5
  groupId: string;
6
+ scaleConfig?: ScaleConfig;
5
7
  }
6
8
 
7
9
  export const BottomSheetManagerContext =
@@ -1,26 +1,33 @@
1
1
  import React, { type PropsWithChildren } from 'react';
2
+ import { PortalProvider } from 'react-native-teleport';
2
3
 
3
4
  import {
4
5
  BottomSheetManagerContext,
5
6
  type BottomSheetManagerContextValue,
6
7
  } from './BottomSheetManager.context';
8
+ import type { ScaleConfig } from './useScaleAnimation';
7
9
 
8
10
  interface ProviderProps extends PropsWithChildren {
9
11
  id: string;
12
+ scaleConfig?: ScaleConfig;
10
13
  }
11
14
 
12
- function BottomSheetManagerProviderComp({ id, children }: ProviderProps) {
15
+ export function BottomSheetManagerProvider({
16
+ id,
17
+ scaleConfig,
18
+ children,
19
+ }: ProviderProps) {
20
+ const value = { groupId: id, scaleConfig };
21
+
13
22
  return (
14
- <BottomSheetManagerContext.Provider key={id} value={{ groupId: id }}>
15
- {children}
16
- </BottomSheetManagerContext.Provider>
23
+ <PortalProvider>
24
+ <BottomSheetManagerContext.Provider key={id} value={value}>
25
+ {children}
26
+ </BottomSheetManagerContext.Provider>
27
+ </PortalProvider>
17
28
  );
18
29
  }
19
30
 
20
- export const BottomSheetManagerProvider = React.memo(
21
- BottomSheetManagerProviderComp
22
- );
23
-
24
31
  export const useBottomSheetManagerContext =
25
32
  (): BottomSheetManagerContextValue => {
26
33
  const context = React.useContext(BottomSheetManagerContext);
@@ -0,0 +1,39 @@
1
+ 'use no memo';
2
+
3
+ import React from 'react';
4
+ import { Portal } from 'react-native-teleport';
5
+
6
+ import { BottomSheetContext } from './BottomSheet.context';
7
+ import { useBottomSheetStore } from './bottomSheet.store';
8
+ import type { BottomSheetPortalId } from './portal.types';
9
+ import { sheetRefs } from './refsMap';
10
+
11
+ interface BottomSheetPortalProps {
12
+ id: BottomSheetPortalId;
13
+ children: React.ReactElement;
14
+ }
15
+
16
+ export function BottomSheetPortal({ id, children }: BottomSheetPortalProps) {
17
+ const sheetState = useBottomSheetStore((state) => state.sheetsById[id]);
18
+
19
+ // Only render when the sheet is active and using portal
20
+ if (!sheetState?.usePortal) {
21
+ return null;
22
+ }
23
+
24
+ // Get the ref that was created in useBottomSheetControl.open()
25
+ const ref = sheetRefs[id];
26
+
27
+ // Clone the child element to add the ref
28
+ // @ts-ignore - same pattern as useBottomSheetManager
29
+ const childWithRef = React.cloneElement(children, { ref });
30
+
31
+ // Wrap with BottomSheetContext so useBottomSheetState() works inside portal content
32
+ return (
33
+ <Portal hostName={`bottomsheet-${id}`}>
34
+ <BottomSheetContext.Provider value={{ id }}>
35
+ {childWithRef}
36
+ </BottomSheetContext.Provider>
37
+ </Portal>
38
+ );
39
+ }
@@ -0,0 +1,41 @@
1
+ import { useContext, type PropsWithChildren } from 'react';
2
+ import { StyleSheet } from 'react-native';
3
+ import Animated from 'react-native-reanimated';
4
+ import { BottomSheetManagerContext } from './BottomSheetManager.context';
5
+ import { useScaleAnimatedStyle, useScaleDepth } from './useScaleAnimation';
6
+
7
+ /**
8
+ * Wraps your app content with iOS-style scale animation when a bottom sheet
9
+ * with scaleBackground: true is open. Place your main content inside this
10
+ * component, but keep BottomSheetHost outside of it.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * <BottomSheetManagerProvider id="default" scaleConfig={{ scale: 0.92 }}>
15
+ * <BottomSheetScaleView>
16
+ * <MainContent />
17
+ * </BottomSheetScaleView>
18
+ * <BottomSheetHost />
19
+ * </BottomSheetManagerProvider>
20
+ * ```
21
+ */
22
+ export function BottomSheetScaleView({ children }: PropsWithChildren) {
23
+ const context = useContext(BottomSheetManagerContext);
24
+ const groupId = context?.groupId ?? 'default';
25
+ const scaleConfig = context?.scaleConfig;
26
+
27
+ const scaleDepth = useScaleDepth(groupId);
28
+ const animatedStyle = useScaleAnimatedStyle(scaleDepth, scaleConfig);
29
+
30
+ return (
31
+ <Animated.View style={[styles.container, animatedStyle]}>
32
+ {children}
33
+ </Animated.View>
34
+ );
35
+ }
36
+
37
+ const styles = StyleSheet.create({
38
+ container: {
39
+ flex: 1,
40
+ },
41
+ });
@@ -0,0 +1,22 @@
1
+ import { makeMutable, type SharedValue } from 'react-native-reanimated';
2
+
3
+ /**
4
+ * Registry for shared animated values per sheet.
5
+ * This allows backdrop to access the animatedIndex from the bottom sheet.
6
+ */
7
+ const animatedIndexRegistry = new Map<string, SharedValue<number>>();
8
+
9
+ export function getAnimatedIndex(sheetId: string): SharedValue<number> {
10
+ let animatedIndex = animatedIndexRegistry.get(sheetId);
11
+
12
+ if (!animatedIndex) {
13
+ animatedIndex = makeMutable(-1);
14
+ animatedIndexRegistry.set(sheetId, animatedIndex);
15
+ }
16
+
17
+ return animatedIndex;
18
+ }
19
+
20
+ export function cleanupAnimatedIndex(sheetId: string): void {
21
+ animatedIndexRegistry.delete(sheetId);
22
+ }