@rn-tools/navigation 2.0.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.
@@ -0,0 +1,467 @@
1
+ import * as React from "react";
2
+ import { ScreenProps as RNScreenProps } from "react-native-screens";
3
+
4
+ import type {
5
+ NavigationState,
6
+ PushScreenOptions,
7
+ RenderCharts,
8
+ StackItem,
9
+ ScreenItem,
10
+ TabItem,
11
+ } from "./types";
12
+ import {
13
+ generateScreenId,
14
+ generateStackId,
15
+ generateTabId,
16
+ serializeTabIndexKey,
17
+ } from "./utils";
18
+
19
+ export let DEFAULT_SLOT_NAME = "DEFAULT_SLOT";
20
+
21
+ function getInitialState(): NavigationState {
22
+ return {
23
+ stacks: {
24
+ ids: [],
25
+ lookup: {},
26
+ },
27
+ screens: {
28
+ ids: [],
29
+ lookup: {},
30
+ },
31
+ tabs: {
32
+ ids: [],
33
+ lookup: {},
34
+ },
35
+ debugModeEnabled: false,
36
+ };
37
+ }
38
+
39
+ export let initialState = getInitialState();
40
+
41
+ export let initialRenderCharts: RenderCharts = {
42
+ stacksByDepth: {},
43
+ tabsByDepth: {},
44
+ tabParentsById: {},
45
+ stackParentsById: {},
46
+ stacksByTabIndex: {},
47
+ };
48
+
49
+ type CreateStackAction = {
50
+ type: "CREATE_STACK_INSTANCE";
51
+ stackId?: string;
52
+ defaultSlotName?: string;
53
+ };
54
+
55
+ type RegisterStackAction = {
56
+ type: "REGISTER_STACK";
57
+ depth: number;
58
+ isActive: boolean;
59
+ stackId: string;
60
+ parentStackId: string;
61
+ parentTabId: string;
62
+ tabIndex: number;
63
+ };
64
+
65
+ type UnregisterStackAction = {
66
+ type: "UNREGISTER_STACK";
67
+ stackId: string;
68
+ };
69
+
70
+ type PushScreenStackAction = PushScreenOptions & {
71
+ type: "PUSH_SCREEN";
72
+ element: React.ReactElement<RNScreenProps>;
73
+ };
74
+
75
+ type PopScreenByCountAction = {
76
+ type: "POP_SCREEN_BY_COUNT";
77
+ count: number;
78
+ stackId: string;
79
+ };
80
+
81
+ type PopScreenByKeyAction = {
82
+ type: "POP_SCREEN_BY_KEY";
83
+ key: string;
84
+ };
85
+
86
+ type StackActions =
87
+ | CreateStackAction
88
+ | RegisterStackAction
89
+ | UnregisterStackAction
90
+ | PushScreenStackAction
91
+ | PopScreenByCountAction
92
+ | PopScreenByKeyAction;
93
+
94
+ type CreateTabAction = {
95
+ type: "CREATE_TAB_INSTANCE";
96
+ tabId?: string;
97
+ initialActiveIndex?: number;
98
+ };
99
+
100
+ type SetTabIndexAction = {
101
+ type: "SET_TAB_INDEX";
102
+ index: number;
103
+ tabId: string;
104
+ };
105
+
106
+ type RegisterTabAction = {
107
+ type: "REGISTER_TAB";
108
+ depth: number;
109
+ tabId: string;
110
+ isActive: boolean;
111
+ parentTabId?: string;
112
+ };
113
+
114
+ type UnregisterTabAction = {
115
+ type: "UNREGISTER_TAB";
116
+ tabId: string;
117
+ };
118
+
119
+ type TabBackAction = {
120
+ type: "TAB_BACK";
121
+ tabId: string;
122
+ };
123
+
124
+ type TabActions =
125
+ | CreateTabAction
126
+ | SetTabIndexAction
127
+ | RegisterTabAction
128
+ | UnregisterTabAction
129
+ | TabBackAction;
130
+
131
+ type SetDebugModeAction = {
132
+ type: "SET_DEBUG_MODE";
133
+ enabled: boolean;
134
+ };
135
+
136
+ type DebugActions = SetDebugModeAction;
137
+
138
+ type ResetNavigationAction = {
139
+ type: "RESET_NAVIGATION";
140
+ };
141
+
142
+ export type NavigationAction =
143
+ | StackActions
144
+ | TabActions
145
+ | DebugActions
146
+ | ResetNavigationAction;
147
+
148
+ export function reducer(
149
+ state: NavigationState,
150
+ action: NavigationAction,
151
+ context: { renderCharts: RenderCharts }
152
+ ): NavigationState {
153
+ switch (action.type) {
154
+ case "CREATE_STACK_INSTANCE": {
155
+ action.stackId = action.stackId || generateStackId();
156
+
157
+ let initialStack: StackItem = {
158
+ id: action.stackId,
159
+ defaultSlotName: action.defaultSlotName || DEFAULT_SLOT_NAME,
160
+ screens: [],
161
+ };
162
+
163
+ let nextState = Object.assign({}, state);
164
+ nextState.stacks.ids = nextState.stacks.ids
165
+ .filter((id) => id !== initialStack.id)
166
+ .concat(initialStack.id);
167
+
168
+ nextState.stacks.lookup[action.stackId] = initialStack;
169
+ return nextState;
170
+ }
171
+
172
+ case "REGISTER_STACK": {
173
+ let { depth, isActive, stackId, parentStackId, parentTabId, tabIndex } =
174
+ action;
175
+ let { renderCharts } = context;
176
+
177
+ renderCharts.stacksByDepth[depth] =
178
+ renderCharts.stacksByDepth[depth] || [];
179
+
180
+ Object.keys(renderCharts.stacksByDepth).forEach((depth) => {
181
+ renderCharts.stacksByDepth[depth] = renderCharts.stacksByDepth[
182
+ depth
183
+ ].filter((id) => id !== stackId);
184
+ });
185
+
186
+ if (isActive && !renderCharts.stacksByDepth[depth].includes(stackId)) {
187
+ renderCharts.stacksByDepth[depth].push(stackId);
188
+ }
189
+
190
+ if (parentStackId) {
191
+ renderCharts.stackParentsById[stackId] = parentStackId;
192
+ }
193
+
194
+ if (parentTabId) {
195
+ let tabIndexKey = serializeTabIndexKey(parentTabId, tabIndex);
196
+ renderCharts.stacksByTabIndex[tabIndexKey] =
197
+ renderCharts.stacksByTabIndex[tabIndexKey] || [];
198
+
199
+ if (!renderCharts.stacksByTabIndex[tabIndexKey].includes(stackId)) {
200
+ renderCharts.stacksByTabIndex[tabIndexKey].push(stackId);
201
+ }
202
+ }
203
+
204
+ return state;
205
+ }
206
+
207
+ case "UNREGISTER_STACK": {
208
+ let { stackId } = action;
209
+ let { renderCharts } = context;
210
+
211
+ let nextState = Object.assign({}, state);
212
+
213
+ for (let depth in renderCharts.stacksByDepth) {
214
+ renderCharts.stacksByDepth[depth] = renderCharts.stacksByDepth[
215
+ depth
216
+ ].filter((id) => id !== stackId);
217
+ }
218
+
219
+ let stack = state.stacks.lookup[stackId];
220
+
221
+ if (stack && renderCharts.stackParentsById[stackId] != null) {
222
+ stack.screens.forEach((screenId) => {
223
+ delete nextState.screens.lookup[screenId];
224
+ nextState.screens.ids = nextState.screens.ids.filter(
225
+ (id) => id !== screenId
226
+ );
227
+ });
228
+
229
+ nextState.stacks.ids = nextState.stacks.ids.filter(
230
+ (id) => id !== stackId
231
+ );
232
+ delete nextState.stacks.lookup[stackId];
233
+ }
234
+
235
+ return nextState;
236
+ }
237
+
238
+ case "PUSH_SCREEN": {
239
+ let { element, stackId, screenId, slotName } = action;
240
+ let stack = state.stacks.lookup[stackId];
241
+
242
+ if (!stack) {
243
+ if (state.debugModeEnabled) {
244
+ console.warn("Stack not found: ", stackId);
245
+ }
246
+ return state;
247
+ }
248
+
249
+ if (screenId && state.screens.lookup[screenId] != null) {
250
+ return state;
251
+ }
252
+
253
+ let nextState = Object.assign({}, state);
254
+
255
+ let screenItem: ScreenItem = {
256
+ element,
257
+ slotName: slotName || stack.defaultSlotName,
258
+ id: screenId || generateScreenId(),
259
+ stackId: stack.id,
260
+ };
261
+
262
+ nextState.screens.ids = nextState.screens.ids.concat(screenItem.id);
263
+ nextState.screens.lookup[screenItem.id] = screenItem;
264
+
265
+ nextState.stacks.lookup[stackId] = Object.assign(stack, {
266
+ screens: stack.screens
267
+ .filter((id) => id !== screenItem.id)
268
+ .concat(screenItem.id),
269
+ });
270
+
271
+ return nextState;
272
+ }
273
+
274
+ case "POP_SCREEN_BY_COUNT": {
275
+ let { count, stackId } = action;
276
+ let stack = state.stacks.lookup[stackId];
277
+
278
+ if (!stack) {
279
+ if (state.debugModeEnabled) {
280
+ console.warn("Stack not found: ", stackId);
281
+ }
282
+ return state;
283
+ }
284
+
285
+ if (count === -1) {
286
+ count = stack.screens.length;
287
+ }
288
+
289
+ let nextState = Object.assign({}, state);
290
+ let poppedScreenIds = nextState.stacks.lookup[stackId].screens.splice(
291
+ -count,
292
+ count
293
+ );
294
+
295
+ poppedScreenIds.forEach((screenId) => {
296
+ delete nextState.screens.lookup[screenId];
297
+ nextState.screens.ids = nextState.screens.ids.filter(
298
+ (id) => id !== screenId
299
+ );
300
+ });
301
+
302
+ return nextState;
303
+ }
304
+
305
+ case "POP_SCREEN_BY_KEY": {
306
+ let { key } = action;
307
+
308
+ let stackId = state.screens.lookup[key]?.stackId;
309
+ let stack = state.stacks.lookup[stackId];
310
+
311
+ if (!stack) {
312
+ if (state.debugModeEnabled) {
313
+ console.warn("Stack not found: ", stackId);
314
+ }
315
+
316
+ return state;
317
+ }
318
+
319
+ let nextState = Object.assign({}, state);
320
+
321
+ nextState.stacks.lookup[stackId] = Object.assign(stack, {
322
+ screens: stack.screens.filter((screenId) => screenId !== key),
323
+ });
324
+
325
+ delete nextState.screens.lookup[key];
326
+
327
+ nextState.screens = {
328
+ ids: nextState.screens.ids.filter((id) => id !== key),
329
+ lookup: nextState.screens.lookup,
330
+ };
331
+
332
+ return nextState;
333
+ }
334
+
335
+ case "CREATE_TAB_INSTANCE": {
336
+ let { tabId, initialActiveIndex = 0 } = action;
337
+
338
+ let initialTabs: TabItem = {
339
+ id: tabId || generateTabId(),
340
+ activeIndex: initialActiveIndex,
341
+ history: [],
342
+ };
343
+
344
+ let nextState = Object.assign({}, state);
345
+
346
+ nextState.tabs.lookup[initialTabs.id] = initialTabs;
347
+ nextState.tabs.ids = nextState.tabs.ids
348
+ .filter((id) => id !== initialTabs.id)
349
+ .concat(initialTabs.id);
350
+
351
+ return nextState;
352
+ }
353
+
354
+ case "SET_TAB_INDEX": {
355
+ let { tabId, index } = action;
356
+ let { renderCharts } = context;
357
+
358
+ let tab = state.tabs.lookup[tabId];
359
+ if (!tab) {
360
+ if (state.debugModeEnabled) {
361
+ console.warn("Tab not found: ", tabId);
362
+ }
363
+
364
+ return state;
365
+ }
366
+
367
+ let nextState: NavigationState = Object.assign({}, state);
368
+ nextState.tabs.lookup[tabId] = Object.assign(
369
+ {},
370
+ {
371
+ ...nextState.tabs.lookup[tabId],
372
+ activeIndex: index,
373
+ history: tab.history.filter((i) => i !== index).concat(index),
374
+ }
375
+ );
376
+
377
+ if (tab.activeIndex === index) {
378
+ let tabKey = serializeTabIndexKey(tabId, index);
379
+ let stackIds = renderCharts.stacksByTabIndex[tabKey];
380
+
381
+ if (stackIds?.length > 0) {
382
+ stackIds.forEach((stackId) => {
383
+ let stack = nextState.stacks.lookup[stackId];
384
+ let screenIdsToRemove = stack.screens;
385
+
386
+ let nextScreensLookup = Object.assign({}, nextState.screens.lookup);
387
+
388
+ screenIdsToRemove.forEach((id) => {
389
+ delete nextScreensLookup[id];
390
+ });
391
+
392
+ nextState.stacks.lookup[stackId].screens = [];
393
+ nextState.screens.ids = nextState.screens.ids.filter(
394
+ (id) => !screenIdsToRemove.includes(id)
395
+ );
396
+ nextState.screens.lookup = nextScreensLookup;
397
+ });
398
+ }
399
+ }
400
+
401
+ return nextState;
402
+ }
403
+
404
+ case "REGISTER_TAB": {
405
+ let { depth, tabId, parentTabId, isActive } = action;
406
+ let { renderCharts } = context;
407
+ renderCharts.tabsByDepth[depth] = renderCharts.tabsByDepth[depth] || [];
408
+
409
+ Object.keys(renderCharts.tabsByDepth).forEach((depth) => {
410
+ renderCharts.tabsByDepth[depth] = renderCharts.tabsByDepth[
411
+ depth
412
+ ].filter((id) => id !== tabId);
413
+ });
414
+
415
+ renderCharts.tabParentsById[tabId] = parentTabId ?? "";
416
+
417
+ if (isActive) {
418
+ renderCharts.tabsByDepth[depth]?.push(tabId);
419
+ }
420
+ return state;
421
+ }
422
+
423
+ case "UNREGISTER_TAB": {
424
+ let { tabId } = action;
425
+ let { renderCharts } = context;
426
+ for (let depth in renderCharts.tabsByDepth) {
427
+ renderCharts.tabsByDepth[depth] = renderCharts.tabsByDepth[
428
+ depth
429
+ ].filter((id) => id !== tabId);
430
+ }
431
+
432
+ let nextState: NavigationState = Object.assign({}, state);
433
+
434
+ nextState.tabs.ids = state.tabs.ids.filter((id) => id !== tabId);
435
+ delete nextState.tabs.lookup[tabId];
436
+
437
+ return nextState;
438
+ }
439
+
440
+ case "TAB_BACK": {
441
+ let { tabId } = action;
442
+ let nextState: NavigationState = Object.assign({}, state);
443
+
444
+ let tab = nextState.tabs.lookup[tabId];
445
+ let last = tab.history.pop();
446
+ if (last != null) {
447
+ tab.activeIndex = last;
448
+ }
449
+
450
+ return nextState;
451
+ }
452
+
453
+ case "SET_DEBUG_MODE": {
454
+ let { enabled } = action;
455
+ return Object.assign(state, { debugModeEnabled: enabled });
456
+ }
457
+
458
+ case "RESET_NAVIGATION": {
459
+ context.renderCharts = initialRenderCharts;
460
+ return getInitialState();
461
+ }
462
+
463
+ default: {
464
+ return state;
465
+ }
466
+ }
467
+ }
@@ -0,0 +1,55 @@
1
+ import * as React from "react";
2
+ import { createStore, useStore as useStoreContext } from "zustand";
3
+ import { devtools, redux } from "zustand/middleware";
4
+
5
+ import {
6
+ initialRenderCharts,
7
+ initialState,
8
+ reducer as navigationReducer,
9
+ type NavigationAction,
10
+ } from "./navigation-reducer";
11
+ import type { NavigationState } from "./types";
12
+
13
+ export type NavigationStore = ReturnType<typeof createNavigationStore>;
14
+
15
+ export function createNavigationStore() {
16
+ let renderCharts = Object.assign({}, initialRenderCharts);
17
+
18
+ let reducer = (state: NavigationState, action: NavigationAction) => {
19
+ let nextState = navigationReducer(state, action, { renderCharts });
20
+ return { ...nextState };
21
+ };
22
+
23
+ let store = createStore(devtools(redux(reducer, initialState)));
24
+
25
+ store.subscribe((state) => {
26
+ if (state.debugModeEnabled) {
27
+ console.debug("[@rntoolkit/navigation] state updated: ", {
28
+ state,
29
+ renderCharts,
30
+ });
31
+ }
32
+ });
33
+
34
+ return {
35
+ store: store,
36
+ dispatch: store.dispatch,
37
+ renderCharts,
38
+ };
39
+ }
40
+
41
+ export let rootStore = createNavigationStore();
42
+ export let NavigationStateContext = React.createContext(rootStore.store);
43
+ export let NavigationDispatchContext = React.createContext(rootStore.dispatch);
44
+
45
+ export function useNavigationState<T>(
46
+ selector?: (state: NavigationState) => T
47
+ ) {
48
+ let context = React.useContext(NavigationStateContext);
49
+ return useStoreContext(context, selector);
50
+ }
51
+
52
+ export function useNavigationDispatch() {
53
+ let dispatch = React.useContext(NavigationDispatchContext);
54
+ return dispatch;
55
+ }
@@ -0,0 +1,141 @@
1
+ import * as React from "react";
2
+
3
+ import {
4
+ createNavigationStore,
5
+ NavigationDispatchContext,
6
+ NavigationStateContext,
7
+ rootStore,
8
+ type NavigationStore,
9
+ } from "./navigation-store";
10
+ import type { StackScreenProps } from "./stack";
11
+ import type { PushScreenOptions } from "./types";
12
+
13
+ /**
14
+ * Ideas:
15
+ * - lifecycles / screen tracking
16
+ * - testing - internal and jest plugin
17
+ */
18
+
19
+ export function createNavigation() {
20
+ let store = createNavigationStore();
21
+ let navigation = getNavigationFns(store);
22
+
23
+ let NavigationContainer = ({ children }: { children: React.ReactNode }) => {
24
+ return (
25
+ <NavigationStateContext.Provider value={store.store}>
26
+ <NavigationDispatchContext.Provider value={store.dispatch}>
27
+ {children}
28
+ </NavigationDispatchContext.Provider>
29
+ </NavigationStateContext.Provider>
30
+ );
31
+ };
32
+
33
+ return {
34
+ navigation,
35
+ NavigationContainer,
36
+ };
37
+ }
38
+
39
+ function getNavigationFns({ store, dispatch, renderCharts }: NavigationStore) {
40
+ function getFocusedStackId() {
41
+ let maxDepth = Math.max(
42
+ ...Object.keys(renderCharts.stacksByDepth)
43
+ .filter((key) => renderCharts.stacksByDepth[key].length > 0)
44
+ .map(Number)
45
+ );
46
+ let stackIds = renderCharts.stacksByDepth[maxDepth];
47
+
48
+ if (!stackIds || stackIds?.length === 0) {
49
+ if (store.getState().debugModeEnabled) {
50
+ console.warn("No focused stack found");
51
+ }
52
+
53
+ return;
54
+ }
55
+
56
+ let topStackId = stackIds[stackIds.length - 1];
57
+ return topStackId;
58
+ }
59
+
60
+ function getFocusedTabsId() {
61
+ let maxDepth = Math.max(
62
+ ...Object.keys(renderCharts.tabsByDepth)
63
+ .filter((key) => renderCharts.tabsByDepth[key].length > 0)
64
+ .map(Number),
65
+ 0
66
+ );
67
+ let tabIds = renderCharts.tabsByDepth[maxDepth];
68
+ let topTabId = tabIds[tabIds.length - 1];
69
+ return topTabId;
70
+ }
71
+
72
+ function pushScreen(
73
+ element: React.ReactElement<StackScreenProps>,
74
+ options?: PushScreenOptions
75
+ ) {
76
+ let stackId = options?.stackId || getFocusedStackId();
77
+ let screenId = options?.screenId;
78
+
79
+ dispatch({
80
+ type: "PUSH_SCREEN",
81
+ stackId,
82
+ screenId,
83
+ element,
84
+ });
85
+
86
+ return screenId;
87
+ }
88
+
89
+ function popScreen(count = 1) {
90
+ let stackId = getFocusedStackId();
91
+ let stack = store.getState().stacks.lookup[stackId];
92
+ let numScreens = stack?.screens.length || 0;
93
+
94
+ let screensToPop = Math.max(Math.min(numScreens, count), 0);
95
+
96
+ dispatch({ type: "POP_SCREEN_BY_COUNT", count: screensToPop, stackId });
97
+ let remainingScreens = count - screensToPop;
98
+
99
+ let parentStackId = renderCharts.stackParentsById[stackId];
100
+ let parentStack = store.getState().stacks.lookup[parentStackId];
101
+
102
+ while (remainingScreens > 0 && parentStackId && parentStack) {
103
+ let screensToPop = Math.min(parentStack.screens.length, remainingScreens);
104
+ dispatch({
105
+ type: "POP_SCREEN_BY_COUNT",
106
+ count: screensToPop,
107
+ stackId: parentStackId,
108
+ });
109
+
110
+ remainingScreens = remainingScreens - screensToPop;
111
+ let nextParentStack = renderCharts.stackParentsById[parentStack.id];
112
+
113
+ parentStackId = nextParentStack;
114
+ parentStack = store.getState().stacks.lookup[parentStackId];
115
+ }
116
+ }
117
+
118
+ function setTabIndex(index: number, options?: { tabId?: string }) {
119
+ let focusedTabsId = options?.tabId || getFocusedTabsId();
120
+ dispatch({ type: "SET_TAB_INDEX", index, tabId: focusedTabsId });
121
+ }
122
+
123
+ function reset() {
124
+ dispatch({ type: "RESET_NAVIGATION" });
125
+ }
126
+
127
+ function setDebugModeEnabled(enabled: boolean) {
128
+ dispatch({ type: "SET_DEBUG_MODE", enabled });
129
+ }
130
+
131
+ return {
132
+ pushScreen,
133
+ popScreen,
134
+ setTabIndex,
135
+ reset,
136
+ setDebugModeEnabled,
137
+ };
138
+ }
139
+
140
+ let rootNavigation = getNavigationFns(rootStore);
141
+ export { rootNavigation as navigation };