@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
package/src/manager.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { RefObject } from "react";
2
2
 
3
- import { eventManager } from "./events";
3
+ import { BottomSheetInstance, Sheets, StackBehavior } from "./types";
4
4
  import { providerRegistryStack, sheetsRegistry } from "./provider";
5
- import { BottomSheetInstance, Sheets } from "./types";
5
+ import { eventManager } from "./events";
6
6
 
7
7
  // Array of all the ids of Sheets currently rendered in the app.
8
8
  const ids: string[] = [];
@@ -12,23 +12,39 @@ const DEFAULT_Z_INDEX = 999;
12
12
 
13
13
  const makeKey = (id: string, context: string) => `${id}:${context}`;
14
14
 
15
+ interface HistoryEntry {
16
+ id: string;
17
+ context: string;
18
+ behavior: StackBehavior;
19
+ }
20
+
15
21
  export const PrivateManager = {
16
- // Return to the previous sheet when the current sheet is closed.
17
- history: [] as { id: string; context: string }[],
22
+ // Stack of sheet history for restoration when sheets are closed
23
+ history: [] as HistoryEntry[],
18
24
 
19
25
  context(options?: { context?: string; id?: string }) {
20
26
  if (!options) options = {};
21
27
  if (!options?.context) {
22
- // If no context is provided, use to current top most context
23
- // to render the sheet.
24
- for (const context of providerRegistryStack.slice().reverse()) {
25
- // We only automatically select nested sheet providers.
26
- if (
27
- context.startsWith("$$-auto") &&
28
- !context.includes(options?.id as string)
29
- ) {
30
- options.context = context;
31
- break;
28
+ // First try to find a context where this sheet is registered
29
+ if (options?.id) {
30
+ for (const context of providerRegistryStack.slice().reverse()) {
31
+ if (sheetsRegistry[context]?.[options.id]) {
32
+ options.context = context;
33
+ break;
34
+ }
35
+ }
36
+ }
37
+
38
+ // Fall back to the top-most auto-generated nested context
39
+ if (!options.context) {
40
+ for (const context of providerRegistryStack.slice().reverse()) {
41
+ if (
42
+ context.startsWith("$$-auto") &&
43
+ !context.includes(options?.id as string)
44
+ ) {
45
+ options.context = context;
46
+ break;
47
+ }
32
48
  }
33
49
  }
34
50
  }
@@ -42,11 +58,12 @@ export const PrivateManager = {
42
58
  ) => {
43
59
  const key = makeKey(id, context);
44
60
  refs[key] = instance;
45
- keys.push(key);
61
+ if (!keys.includes(key)) {
62
+ keys.push(key);
63
+ }
46
64
  },
47
65
 
48
66
  /**
49
- *
50
67
  * Get internal ref of a sheet by the given id.
51
68
  *
52
69
  * @param id Id of the sheet
@@ -57,8 +74,8 @@ export const PrivateManager = {
57
74
  context?: string,
58
75
  ): RefObject<BottomSheetInstance<SheetId>> => {
59
76
  if (!context) {
60
- for (let ctx of providerRegistryStack.slice().reverse()) {
61
- for (let _id in sheetsRegistry[ctx]) {
77
+ for (const ctx of providerRegistryStack.slice().reverse()) {
78
+ for (const _id in sheetsRegistry[ctx]) {
62
79
  if (_id === id) {
63
80
  context = ctx;
64
81
  break;
@@ -70,14 +87,17 @@ export const PrivateManager = {
70
87
  },
71
88
 
72
89
  add: (id: string, context: string) => {
73
- if (ids.indexOf(id) < 0) {
74
- ids[ids.length] = makeKey(id, context);
90
+ const key = makeKey(id, context);
91
+ if (!ids.includes(key)) {
92
+ ids.push(key);
75
93
  }
76
94
  },
77
95
 
78
96
  remove: (id: string, context: string) => {
79
- if (ids.indexOf(makeKey(id, context)) > -1) {
80
- ids.splice(ids.indexOf(makeKey(id, context)));
97
+ const key = makeKey(id, context);
98
+ const index = ids.indexOf(key);
99
+ if (index > -1) {
100
+ ids.splice(index, 1);
81
101
  }
82
102
  },
83
103
 
@@ -87,12 +107,49 @@ export const PrivateManager = {
87
107
  },
88
108
 
89
109
  stack: () =>
90
- ids.map((id) => {
91
- return {
92
- id: id.split(":")[0],
93
- context: id.split(":")?.[1] || "global",
94
- };
95
- }),
110
+ ids.map((id) => ({
111
+ id: id.split(":")[0],
112
+ context: id.split(":")?.[1] || "global",
113
+ })),
114
+
115
+ /**
116
+ * Get the top-most sheet in the stack
117
+ */
118
+ topSheet: () => {
119
+ if (ids.length === 0) return null;
120
+ const topId = ids[ids.length - 1];
121
+ return {
122
+ id: topId.split(":")[0],
123
+ context: topId.split(":")?.[1] || "global",
124
+ };
125
+ },
126
+
127
+ /**
128
+ * Check if a sheet is currently visible
129
+ */
130
+ isSheetVisible: (id: string, context?: string): boolean => {
131
+ if (context) {
132
+ return ids.includes(makeKey(id, context));
133
+ }
134
+ return ids.some((key) => key.startsWith(`${id}:`));
135
+ },
136
+
137
+ /**
138
+ * Clear all history entries
139
+ */
140
+ clearHistory: () => {
141
+ PrivateManager.history = [];
142
+ },
143
+
144
+ /**
145
+ * Reset all internal state. Useful for testing or HMR.
146
+ */
147
+ reset: () => {
148
+ ids.length = 0;
149
+ keys.length = 0;
150
+ for (const key in refs) delete refs[key];
151
+ PrivateManager.history = [];
152
+ },
96
153
  };
97
154
 
98
155
  class _SheetManager {
@@ -119,36 +176,70 @@ class _SheetManager {
119
176
  * Provide `context` of the `SheetProvider` where you want to show the action sheet.
120
177
  */
121
178
  context?: string;
179
+
180
+ /**
181
+ * Stack behavior for this sheet.
182
+ * - `switch`: (default) Closes current sheet, shows new one
183
+ * - `replace`: Swaps content with crossfade animation
184
+ * - `push`: Stacks new sheet on top of current
185
+ */
186
+ stackBehavior?: StackBehavior;
122
187
  },
123
188
  ): Promise<Sheets[SheetId]["returnValue"]> {
124
189
  return new Promise((resolve) => {
125
190
  const currentContext = PrivateManager.context({ ...options, id: id });
126
- const handler = (data: any, context = "global") => {
191
+ const behavior = options?.stackBehavior ?? "switch";
192
+
193
+ const handler = (
194
+ data: unknown,
195
+ context = "global",
196
+ _reopened?: boolean,
197
+ _behavior?: StackBehavior,
198
+ ) => {
127
199
  if (context !== "global" && currentContext && currentContext !== context)
128
200
  return;
129
- options?.onClose?.(data);
201
+ options?.onClose?.(data as Sheets[SheetId]["returnValue"]);
130
202
  sub?.unsubscribe();
131
- resolve(data);
203
+ resolve(data as Sheets[SheetId]["returnValue"]);
132
204
  };
133
205
 
134
- var sub = eventManager.subscribe(`onclose_${id}`, handler);
135
- PrivateManager.stack().forEach(({ id, context }) => {
136
- eventManager.publish(`hide_${id}`, undefined, context, true);
137
- });
206
+ const sub = eventManager.subscribe(`onclose_${id}`, handler);
207
+
208
+ // Handle existing sheets based on stack behavior.
209
+ // For "push" we do NOT hide existing sheets — they stay underneath.
210
+ if (behavior !== "push") {
211
+ const currentStack = PrivateManager.stack();
212
+ if (currentStack.length > 0) {
213
+ currentStack.forEach(({ id: sheetId, context }) => {
214
+ eventManager.publish(
215
+ `hide_${sheetId}`,
216
+ undefined,
217
+ context,
218
+ true,
219
+ behavior,
220
+ );
221
+ });
222
+ }
223
+ }
138
224
 
139
225
  // Check if the sheet is registered with any `SheetProviders`.
140
226
  let isRegisteredWithSheetProvider = false;
141
- for (let ctx in sheetsRegistry) {
142
- for (let _id in sheetsRegistry[ctx]) {
227
+ for (const ctx in sheetsRegistry) {
228
+ for (const _id in sheetsRegistry[ctx]) {
143
229
  if (_id === id) {
144
230
  isRegisteredWithSheetProvider = true;
231
+ break;
145
232
  }
146
233
  }
234
+ if (isRegisteredWithSheetProvider) break;
147
235
  }
236
+
148
237
  eventManager.publish(
149
238
  isRegisteredWithSheetProvider ? `show_wrap_${id}` : `show_${id}`,
150
239
  options?.payload,
151
240
  currentContext || "global",
241
+ false,
242
+ behavior,
152
243
  );
153
244
  });
154
245
  }
@@ -157,7 +248,7 @@ class _SheetManager {
157
248
  * An async hide function. This is useful when you want to show one Sheet after closing another.
158
249
  *
159
250
  * @param id id of the Sheet to show
160
- * @param data
251
+ * @param options
161
252
  */
162
253
  async hide<SheetId extends keyof Sheets>(
163
254
  id: SheetId | (string & {}),
@@ -172,10 +263,11 @@ class _SheetManager {
172
263
  context?: string;
173
264
  },
174
265
  ): Promise<Sheets[SheetId]["returnValue"]> {
175
- let currentContext = PrivateManager.context({
266
+ const currentContext = PrivateManager.context({
176
267
  ...options,
177
268
  id: id,
178
269
  });
270
+
179
271
  return new Promise((resolve) => {
180
272
  let isRegisteredWithSheetProvider = false;
181
273
  // Check if the sheet is registered with any `SheetProviders`
@@ -188,14 +280,14 @@ class _SheetManager {
188
280
  }
189
281
  }
190
282
 
191
- const hideHandler = (data: any, context = "global") => {
283
+ const hideHandler = (data: unknown, context = "global") => {
192
284
  if (context !== "global" && currentContext && currentContext !== context)
193
285
  return;
194
286
  sub?.unsubscribe();
195
- resolve(data);
287
+ resolve(data as Sheets[SheetId]["returnValue"]);
196
288
  };
197
289
 
198
- var sub = eventManager.subscribe(`onclose_${id}`, hideHandler);
290
+ const sub = eventManager.subscribe(`onclose_${id}`, hideHandler);
199
291
  eventManager.publish(
200
292
  isRegisteredWithSheetProvider ? `hide_wrap_${id}` : `hide_${id}`,
201
293
  options?.payload,
@@ -210,11 +302,86 @@ class _SheetManager {
210
302
  * @param id Hide all sheets for the specific id.
211
303
  */
212
304
  hideAll<SheetId extends keyof Sheets>(id?: SheetId | (string & {})) {
305
+ // Clear history when hiding all sheets
306
+ PrivateManager.clearHistory();
307
+
213
308
  PrivateManager.stack().forEach(({ id: _id, context }) => {
214
309
  if (id && !_id.startsWith(id)) return;
215
310
  eventManager.publish(`hide_${_id}`, undefined, context);
216
311
  });
217
312
  }
313
+
314
+ /**
315
+ * Replace the current sheet with a new one using crossfade animation.
316
+ * This is a convenience method for show() with stackBehavior: 'replace'.
317
+ */
318
+ async replace<SheetId extends keyof Sheets>(
319
+ id: SheetId | (string & {}),
320
+ options?: {
321
+ payload?: Sheets[SheetId]["payload"];
322
+ onClose?: (data: Sheets[SheetId]["returnValue"] | undefined) => void;
323
+ context?: string;
324
+ },
325
+ ): Promise<Sheets[SheetId]["returnValue"]> {
326
+ return this.show(id, { ...options, stackBehavior: "replace" });
327
+ }
328
+
329
+ /**
330
+ * Push a new sheet on top of the current one, creating a stack.
331
+ * This is a convenience method for show() with stackBehavior: 'push'.
332
+ *
333
+ */
334
+ async push<SheetId extends keyof Sheets>(
335
+ id: SheetId | (string & {}),
336
+ options?: {
337
+ payload?: Sheets[SheetId]["payload"];
338
+ onClose?: (data: Sheets[SheetId]["returnValue"] | undefined) => void;
339
+ context?: string;
340
+ },
341
+ ): Promise<Sheets[SheetId]["returnValue"]> {
342
+ return this.show(id, { ...options, stackBehavior: "push" });
343
+ }
344
+
345
+ /**
346
+ * Pop the top sheet from the stack and restore the previous one.
347
+ * Only works when sheets were opened with stackBehavior: 'push'.
348
+ */
349
+ pop(): void {
350
+ const topSheet = PrivateManager.topSheet();
351
+ if (topSheet) {
352
+ eventManager.publish(`hide_${topSheet.id}`, undefined, topSheet.context);
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Get the internal ref of a sheet instance.
358
+ *
359
+ * @param id Id of the sheet
360
+ * @param context Optional context of the SheetProvider
361
+ */
362
+ get<SheetId extends keyof Sheets>(
363
+ id: SheetId | (string & {}),
364
+ context?: string,
365
+ ): BottomSheetInstance<SheetId> | undefined {
366
+ return PrivateManager.get(id, context)?.current ?? undefined;
367
+ }
368
+
369
+ /**
370
+ * Check if a specific sheet is currently visible.
371
+ */
372
+ isVisible<SheetId extends keyof Sheets>(
373
+ id: SheetId | (string & {}),
374
+ context?: string,
375
+ ): boolean {
376
+ return PrivateManager.isSheetVisible(id, context);
377
+ }
378
+
379
+ /**
380
+ * Reset all internal state. Useful for testing environments.
381
+ */
382
+ reset(): void {
383
+ PrivateManager.reset();
384
+ }
218
385
  }
219
386
 
220
387
  /**