@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.
- package/README.md +422 -0
- package/package.json +44 -0
- package/src/__tests__/navigation-reducer.test.tsx +348 -0
- package/src/contexts.tsx +8 -0
- package/src/index.ts +3 -0
- package/src/navigation-reducer.ts +467 -0
- package/src/navigation-store.ts +55 -0
- package/src/navigation.tsx +141 -0
- package/src/stack.tsx +255 -0
- package/src/tabs.tsx +297 -0
- package/src/types.ts +48 -0
- package/src/utils.ts +14 -0
|
@@ -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 };
|