@niibase/bottom-sheet-manager 1.2.0 → 1.4.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 (64) hide show
  1. package/README.md +414 -69
  2. package/lib/commonjs/events.js +100 -15
  3. package/lib/commonjs/events.js.map +1 -1
  4. package/lib/commonjs/index.js +14 -0
  5. package/lib/commonjs/index.js.map +1 -1
  6. package/lib/commonjs/manager.js +153 -35
  7. package/lib/commonjs/manager.js.map +1 -1
  8. package/lib/commonjs/provider.js +92 -54
  9. package/lib/commonjs/provider.js.map +1 -1
  10. package/lib/commonjs/router/index.js +80 -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 +93 -126
  15. package/lib/commonjs/router/view.js.map +1 -1
  16. package/lib/commonjs/sheet.js +122 -98
  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 +2 -2
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/module/manager.js +154 -35
  23. package/lib/module/manager.js.map +1 -1
  24. package/lib/module/provider.js +87 -50
  25. package/lib/module/provider.js.map +1 -1
  26. package/lib/module/router/index.js +66 -19
  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 +92 -126
  31. package/lib/module/router/view.js.map +1 -1
  32. package/lib/module/sheet.js +124 -100
  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 +2 -2
  37. package/lib/typescript/index.d.ts.map +1 -1
  38. package/lib/typescript/manager.d.ts +73 -7
  39. package/lib/typescript/manager.d.ts.map +1 -1
  40. package/lib/typescript/provider.d.ts +22 -16
  41. package/lib/typescript/provider.d.ts.map +1 -1
  42. package/lib/typescript/router/index.d.ts +47 -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 +142 -32
  47. package/lib/typescript/router/types.d.ts.map +1 -1
  48. package/lib/typescript/router/view.d.ts +3 -3
  49. package/lib/typescript/router/view.d.ts.map +1 -1
  50. package/lib/typescript/sheet.d.ts +1 -1
  51. package/lib/typescript/sheet.d.ts.map +1 -1
  52. package/lib/typescript/types.d.ts +52 -21
  53. package/lib/typescript/types.d.ts.map +1 -1
  54. package/package.json +14 -15
  55. package/src/events.ts +118 -27
  56. package/src/index.ts +2 -1
  57. package/src/manager.ts +209 -42
  58. package/src/provider.tsx +144 -71
  59. package/src/router/index.tsx +77 -33
  60. package/src/router/router.ts +188 -15
  61. package/src/router/types.ts +172 -57
  62. package/src/router/view.tsx +111 -213
  63. package/src/sheet.tsx +192 -124
  64. package/src/types.ts +51 -24
@@ -1,57 +1,124 @@
1
1
  import {
2
- ParamListBase,
3
- Router,
4
2
  StackActions,
5
- StackActionType,
6
3
  StackRouter,
7
- StackRouterOptions,
4
+ useNavigation,
5
+ type CommonActions,
6
+ type ParamListBase,
7
+ type Router,
8
+ type StackActionType,
9
+ type StackRouterOptions,
8
10
  } from "@react-navigation/native";
9
11
  import { nanoid } from "nanoid/non-secure";
10
12
 
11
- import type { BottomSheetNavigationState } from "./types";
13
+ import type { BottomSheetNavigationProp, BottomSheetNavigationState } from "./types";
12
14
 
13
15
  export type BottomSheetRouterOptions = StackRouterOptions;
14
16
 
15
17
  export type BottomSheetActionType =
16
18
  | StackActionType
19
+ | ReturnType<typeof CommonActions.goBack>
17
20
  | {
18
21
  type: "SNAP_TO";
19
22
  index: number;
20
23
  source?: string;
21
24
  target?: string;
25
+ }
26
+ | {
27
+ type: "DISMISS";
28
+ source?: string;
29
+ target?: string;
30
+ }
31
+ | {
32
+ type: "REMOVE";
33
+ source?: string;
34
+ target?: string;
22
35
  };
23
36
 
24
37
  export const BottomSheetActions = {
25
38
  ...StackActions,
26
- snapTo(index: number): BottomSheetActionType {
27
- return { type: "SNAP_TO", index };
28
- },
39
+ /**
40
+ * Snap the bottom sheet to a specific index.
41
+ */
42
+ snapTo: (index: number): BottomSheetActionType => ({ type: "SNAP_TO", index }),
43
+ /**
44
+ * Dismiss the current bottom sheet.
45
+ */
46
+ dismiss: (): BottomSheetActionType => ({ type: "DISMISS" }),
47
+ /**
48
+ * Remove the sheet from navigation state after dismiss animation completes.
49
+ */
50
+ remove: (): BottomSheetActionType => ({ type: "REMOVE" }),
51
+ };
52
+
53
+ /**
54
+ * Ensures the base route (first screen) exists in the navigation state.
55
+ * This is important because the first screen is the main content,
56
+ * and subsequent screens are rendered as bottom sheets.
57
+ */
58
+ const ensureBaseRoute = <T extends { routes: { name: string }[] }>(
59
+ state: T,
60
+ baseRouteName: string | undefined,
61
+ routeParamList: Record<string, object | undefined> | undefined,
62
+ ): T & { index: number; routes: T["routes"] } => {
63
+ if (!baseRouteName) {
64
+ return state as T & { index: number; routes: T["routes"] };
65
+ }
66
+
67
+ const hasBaseRoute = state.routes.some((r) => r.name === baseRouteName);
68
+
69
+ if (!hasBaseRoute) {
70
+ const baseRoute = {
71
+ key: `${baseRouteName}-${nanoid()}`,
72
+ name: baseRouteName,
73
+ params: routeParamList?.[baseRouteName],
74
+ };
75
+
76
+ return {
77
+ ...state,
78
+ index: state.routes.length,
79
+ routes: [baseRoute, ...state.routes],
80
+ } as T & { index: number; routes: T["routes"] };
81
+ }
82
+
83
+ return state as T & { index: number; routes: T["routes"] };
29
84
  };
30
85
 
31
- export function BottomSheetRouter(
86
+ export const BottomSheetRouter = (
32
87
  routerOptions: StackRouterOptions,
33
- ): Router<BottomSheetNavigationState<ParamListBase>, BottomSheetActionType> {
88
+ ): Router<
89
+ BottomSheetNavigationState<ParamListBase>,
90
+ BottomSheetActionType
91
+ > => {
34
92
  const baseRouter = StackRouter(routerOptions) as unknown as Router<
35
93
  BottomSheetNavigationState<ParamListBase>,
36
94
  BottomSheetActionType
37
95
  >;
96
+
38
97
  return {
39
98
  ...baseRouter,
40
99
  type: "bottom-sheet",
100
+
41
101
  getInitialState(options) {
42
102
  const state = baseRouter.getInitialState(options);
103
+ const baseRouteName = routerOptions.initialRouteName ?? options.routeNames[0];
104
+ const stateWithBaseRoute = ensureBaseRoute(
105
+ state,
106
+ baseRouteName,
107
+ options.routeParamList,
108
+ );
43
109
 
44
110
  return {
45
- ...state,
111
+ ...stateWithBaseRoute,
46
112
  stale: false,
47
113
  type: "bottom-sheet",
48
114
  key: `bottom-sheet-${nanoid()}`,
49
115
  };
50
116
  },
117
+
51
118
  getStateForAction(state, action, options) {
52
119
  switch (action.type) {
53
120
  case "SNAP_TO": {
54
- const index =
121
+ const routeIndex =
55
122
  action.target === state.key && action.source
56
123
  ? state.routes.findIndex((r) => r.key === action.source)
57
124
  : state.index;
@@ -59,19 +126,93 @@ export function BottomSheetRouter(
59
126
  return {
60
127
  ...state,
61
128
  routes: state.routes.map((route, i) =>
62
- i === index
129
+ i === routeIndex
63
130
  ? {
64
131
  ...route,
65
132
  snapToIndex: action.index,
133
+ snapToKey: (route.snapToKey ?? 0) + 1,
66
134
  }
67
135
  : route,
68
136
  ),
69
137
  };
70
138
  }
139
+
140
+ case "GO_BACK":
141
+ case "DISMISS": {
142
+ return this.getStateForAction(state, StackActions.pop(1), options);
143
+ }
144
+
145
+ case "POP": {
146
+ // Only base screen remains - let parent navigator handle it
147
+ if (state.routes.length <= 1) {
148
+ return null;
149
+ }
150
+
151
+ const count =
152
+ "payload" in action && typeof action.payload?.count === "number"
153
+ ? action.payload.count
154
+ : 1;
155
+
156
+ // Calculate how many routes we can actually pop (don't pop base screen)
157
+ const maxPopCount = state.routes.length - 1;
158
+ const actualCount = Math.min(count, maxPopCount);
159
+
160
+ // Base screen - let parent navigator handle it
161
+ if (actualCount <= 0) {
162
+ return null;
163
+ }
164
+
165
+ // Target index is the route we want to stay on (land on after pop)
166
+ // closingIndex is the first route to be dismissed (the one after target)
167
+ const targetIndex = state.routes.length - 1 - actualCount;
168
+ const closingIndex = targetIndex + 1;
169
+
170
+ // Mark only the bottom-most route to pop as closing
171
+ // The sheet's dismiss() will handle dismissing sheets above it first
172
+ return {
173
+ ...state,
174
+ index: closingIndex,
175
+ routes: state.routes.map((route, i) =>
176
+ i === closingIndex ? { ...route, closing: true } : route,
177
+ ),
178
+ };
179
+ }
180
+
181
+ case "POP_TO_TOP": {
182
+ const popCount = state.routes.length - 1;
183
+ return this.getStateForAction(
184
+ state,
185
+ StackActions.pop(popCount),
186
+ options,
187
+ );
188
+ }
189
+
190
+ case "REMOVE": {
191
+ // Actually remove the closing route and all routes above it
192
+ const routeKey = action.source;
193
+ const routeIndex = routeKey
194
+ ? state.routes.findIndex((r) => r.key === routeKey)
195
+ : state.routes.findIndex((r) => r.closing);
196
+
197
+ if (routeIndex === -1) {
198
+ return state;
199
+ }
200
+
201
+ // Remove the route and all routes above it (they were dismissed together)
202
+ const routes = state.routes.filter((_, i) => i < routeIndex);
203
+
204
+ return {
205
+ ...state,
206
+ index: Math.min(state.index, routes.length - 1),
207
+ routes,
208
+ };
209
+ }
210
+
71
211
  default:
72
212
  return baseRouter.getStateForAction(state, action, options);
73
213
  }
74
214
  },
215
+
75
216
  getRehydratedState(partialState, { routeNames, routeParamList, routeGetIdList }) {
76
217
  if (partialState.stale === false) {
77
218
  return partialState;
@@ -83,12 +224,44 @@ export function BottomSheetRouter(
83
224
  routeGetIdList,
84
225
  });
85
226
 
227
+ const baseRouteName = routerOptions.initialRouteName ?? routeNames[0];
228
+ const stateWithBaseRoute = ensureBaseRoute(
229
+ state,
230
+ baseRouteName,
231
+ routeParamList,
232
+ );
233
+
86
234
  return {
87
- ...state,
235
+ ...stateWithBaseRoute,
88
236
  type: "bottom-sheet",
89
237
  key: `bottom-sheet-${nanoid()}`,
90
238
  };
91
239
  },
240
+
92
241
  actionCreators: BottomSheetActions,
93
242
  };
94
- }
243
+ };
244
+
245
+ /**
246
+ * Hook to access BottomSheet navigation with the snapTo helper.
247
+ *
248
+ * @example
249
+ * ```tsx
250
+ * function MySheet() {
251
+ * const navigation = useBottomSheetNavigation();
252
+ *
253
+ * // Snap to a specific index
254
+ * const handleExpand = () => {
255
+ * navigation.snapTo(1); // Snap to second index
256
+ * };
257
+ *
258
+ * return (
259
+ * <Button title="Expand" onPress={handleExpand} />
260
+ * );
261
+ * }
262
+ * ```
263
+ */
264
+ export const useBottomSheetNavigation = <
265
+ T extends ParamListBase = ParamListBase,
266
+ >(): BottomSheetNavigationProp<T> =>
267
+ useNavigation<BottomSheetNavigationProp<T>>();
@@ -1,4 +1,3 @@
1
- import type { BottomSheetModalProps } from "@gorhom/bottom-sheet";
2
1
  import type {
3
2
  DefaultNavigatorOptions,
4
3
  Descriptor,
@@ -8,43 +7,170 @@ import type {
8
7
  ParamListBase,
9
8
  RouteProp,
10
9
  StackActionHelpers,
10
+ StackNavigationState,
11
+ StackRouterOptions,
11
12
  } from "@react-navigation/native";
13
+ import type { BottomSheetModalProps, SNAP_POINT_TYPE } from "@gorhom/bottom-sheet";
12
14
 
13
- // TODO: Sheet open / close / snap / events.
14
- export type BottomSheetNavigationEventMap = {};
15
-
16
- export type BottomSheetNavigationState<ParamList extends ParamListBase> = Omit<
17
- NavigationState<ParamList>,
18
- "routes"
15
+ /**
16
+ * Bottom-sheet-specific screen options.
17
+ * These only take effect when `presentation: "bottomSheet"`.
18
+ */
19
+ export type BottomSheetScreenOptions = Omit<
20
+ BottomSheetModalProps,
21
+ // Remove props that are managed by the navigator
22
+ | "containerHeight"
23
+ | "snapPoints"
24
+ | "gestureEventsHandlersHook"
25
+ | "iosModalSheetTypeOfAnimation"
26
+ | "animatedPosition"
27
+ | "onBeforeShow"
28
+ | "onChange"
29
+ | "onClose"
30
+ | "onDismiss"
31
+ | "onAnimate"
32
+ | "children"
33
+ | "$modal"
34
+ | "waitFor"
35
+ | "simultaneousHandlers"
19
36
  > & {
20
- type: "bottom-sheet";
21
- routes: (NavigationState<ParamList>["routes"][number] & {
22
- snapToIndex?: number | null;
23
- })[];
37
+ /**
38
+ * Points for the bottom sheet to snap to.
39
+ * Accepts an array of numbers (pixels) or strings (percentages).
40
+ *
41
+ * @example
42
+ * snapPoints={[200, 500]}
43
+ * snapPoints={[200, '50%']}
44
+ * snapPoints={['100%']}
45
+ *
46
+ * @default ['66%']
47
+ */
48
+ snapPoints?: Array<string | number>;
49
+
50
+ /**
51
+ * When `true`, tapping on the backdrop will not dismiss the sheet.
52
+ * @default false
53
+ */
54
+ passThrough?: boolean;
55
+
56
+ /**
57
+ * Opacity of the backdrop overlay.
58
+ * @default 0.45
59
+ */
60
+ opacity?: number;
61
+ };
62
+
63
+ /**
64
+ * Navigation options for the bottom sheet navigator.
65
+ *
66
+ * The first screen is rendered as the base content.
67
+ * All subsequent screens are rendered as bottom sheets using these options.
68
+ */
69
+ export type BottomSheetNavigationOptions = Partial<BottomSheetScreenOptions>;
70
+
71
+ /**
72
+ * Navigation events emitted by the bottom sheet navigator.
73
+ */
74
+ export type BottomSheetNavigationEventMap = {
75
+ /**
76
+ * Event emitted when a sheet is presented.
77
+ */
78
+ sheetPresent: { data: unknown };
79
+ /**
80
+ * Event emitted when a sheet is dismissed.
81
+ */
82
+ sheetDismiss: { data: unknown };
83
+ /**
84
+ * Event emitted when a sheet changes between position/snap points
85
+ */
86
+ sheetOnChange: {
87
+ data: {
88
+ index: number;
89
+ position: number;
90
+ type: SNAP_POINT_TYPE;
91
+ };
92
+ };
93
+ /**
94
+ * Event emitted when a sheet animates between snap points.
95
+ */
96
+ sheetOnAnimate: {
97
+ data: {
98
+ fromIndex: number;
99
+ toIndex: number;
100
+ fromPosition: number;
101
+ toPosition: number;
102
+ };
103
+ };
24
104
  };
25
105
 
106
+ /**
107
+ * Extended route type with bottom sheet specific properties.
108
+ */
109
+ export type BottomSheetRoute<ParamList extends ParamListBase = ParamListBase> =
110
+ NavigationState<ParamList>["routes"][number] & {
111
+ /**
112
+ * The snap point index the sheet should animate to.
113
+ */
114
+ snapToIndex?: number | null;
115
+ /**
116
+ * Key to track snap changes for re-rendering.
117
+ */
118
+ snapToKey?: number;
119
+ /**
120
+ * Whether this route is in the process of being closed.
121
+ */
122
+ closing?: boolean;
123
+ };
124
+
125
+ /**
126
+ * Navigation state type for the bottom sheet navigator.
127
+ *
128
+ * Extends `StackNavigationState` with bottom-sheet-specific route properties
129
+ * and a `"bottom-sheet"` type discriminator.
130
+ */
131
+ export type BottomSheetNavigationState<ParamList extends ParamListBase = ParamListBase> =
132
+ Omit<StackNavigationState<ParamList>, "routes" | "type"> & {
133
+ type: "bottom-sheet";
134
+ routes: BottomSheetRoute<ParamList>[];
135
+ };
136
+
137
+ /**
138
+ * Action helpers available on the navigation object.
139
+ */
26
140
  export type BottomSheetActionHelpers<ParamList extends ParamListBase> =
27
141
  StackActionHelpers<ParamList> & {
28
142
  /**
29
- * Snap the drawer to a point.
143
+ * Snap the sheet to a specific point.
144
+ * @param index The snap point index to snap to.
30
145
  */
31
- snapTo(index?: number): void;
146
+ snapTo(index: number): void;
147
+
148
+ /**
149
+ * Dismiss the current sheet.
150
+ */
151
+ dismiss(): void;
32
152
  };
33
153
 
154
+ /**
155
+ * Navigation prop type for screens in the bottom sheet navigator.
156
+ */
34
157
  export type BottomSheetNavigationProp<
35
- ParamList extends ParamListBase,
158
+ ParamList extends ParamListBase = ParamListBase,
36
159
  RouteName extends keyof ParamList = string,
37
160
  NavigatorID extends string | undefined = undefined,
38
161
  > = NavigationProp<
39
162
  ParamList,
40
163
  RouteName,
41
164
  NavigatorID,
42
- BottomSheetNavigationState<ParamList>,
165
+ BottomSheetNavigationState<ParamListBase>,
43
166
  BottomSheetNavigationOptions,
44
167
  BottomSheetNavigationEventMap
45
168
  > &
46
169
  BottomSheetActionHelpers<ParamList>;
47
170
 
171
+ /**
172
+ * Props available to screen components in the bottom sheet navigator.
173
+ */
48
174
  export type BottomSheetScreenProps<
49
175
  ParamList extends ParamListBase,
50
176
  RouteName extends keyof ParamList = string,
@@ -54,64 +180,53 @@ export type BottomSheetScreenProps<
54
180
  route: RouteProp<ParamList, RouteName>;
55
181
  };
56
182
 
183
+ /**
184
+ * Navigation helpers type for the bottom sheet navigator.
185
+ */
57
186
  export type BottomSheetNavigationHelpers = NavigationHelpers<
58
187
  ParamListBase,
59
188
  BottomSheetNavigationEventMap
60
- >;
61
-
62
- // We want it to be an empty object because navigator does not have any additional props
63
- export type BottomSheetNavigationConfig = {};
64
-
65
- export type BottomSheetNavigationOptions = Omit<
66
- BottomSheetModalProps,
67
- // Remove some props that aren't useful as navigation options.
68
- | "containerHeight"
69
- | "snapPoints"
70
- | "gestureEventsHandlersHook"
71
- | "animatedPosition"
72
- | "animatedIndex"
73
- | "topInset"
74
- | "onChange"
75
- | "onAnimate"
76
- | "onClose"
77
- | "children"
78
- | "$modal"
79
- | "waitFor"
80
- | "simultaneousHandlers"
81
- > & {
82
- /**
83
- * Points for the bottom sheet to snap to. It accepts array of number, string or mix.
84
- * String values should be a percentage.
85
- * @example
86
- * snapPoints={[200, 500]}
87
- * snapPoints={[200, '%50']}
88
- * snapPoints={['%100']}
89
- * @type Array<string | number>
90
- */
91
- snapPoints?: Array<string | number>;
92
- /**
93
- * When `true`, tapping on the backdrop will not dismiss the modal.
94
- * @default false
95
- */
96
- clickThrough?: boolean;
97
- };
189
+ > &
190
+ BottomSheetActionHelpers<ParamListBase>;
98
191
 
192
+ /**
193
+ * Props for the bottom sheet navigator component.
194
+ */
99
195
  export type BottomSheetNavigatorProps = DefaultNavigatorOptions<
100
196
  ParamListBase,
101
- undefined, // or your ID if you want a named ID, e.g. 'BottomSheetNavigator'
197
+ string | undefined,
102
198
  BottomSheetNavigationState<ParamListBase>,
103
199
  BottomSheetNavigationOptions,
104
200
  BottomSheetNavigationEventMap,
105
- BottomSheetNavigationHelpers
201
+ BottomSheetNavigationProp<ParamListBase>
106
202
  > &
107
- BottomSheetNavigationConfig;
203
+ StackRouterOptions;
108
204
 
205
+ /**
206
+ * Descriptor type for bottom sheet screens.
207
+ */
109
208
  export type BottomSheetDescriptor = Descriptor<
110
209
  BottomSheetNavigationOptions,
111
- BottomSheetNavigationProp<ParamListBase>,
210
+ BottomSheetNavigationProp<ParamListBase, string, undefined>,
112
211
  RouteProp<ParamListBase>
113
212
  >;
114
213
 
214
+ /**
215
+ * Map of route keys to their descriptors.
216
+ */
115
217
  export type BottomSheetDescriptorMap = {
116
218
  [key: string]: BottomSheetDescriptor;
117
219
  };
220
+
221
+ export type BottomSheetModalScreenProps = Omit<BottomSheetModalProps, "onDismiss"> & {
222
+ route: BottomSheetRoute;
223
+ navigation: BottomSheetNavigationHelpers;
224
+
225
+ passThrough?: boolean;
226
+ opacity?: number;
227
+
228
+ /**
229
+ * Callback when sheet animation changes.
230
+ */
231
+ onSheetAnimate?: (from: number, to: number) => void;
232
+ };