@react-navigation/routers 8.0.0-alpha.13 → 8.0.0-alpha.15

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,621 @@
1
+ import { nanoid } from 'nanoid/non-secure';
2
+
3
+ import { BaseRouter } from './BaseRouter';
4
+ import { createParamsFromAction } from './createParamsFromAction';
5
+ import type {
6
+ CommonNavigationAction,
7
+ DefaultRouterOptions,
8
+ NavigationState,
9
+ PartialState,
10
+ Route,
11
+ RouterConfigOptions,
12
+ } from './types';
13
+
14
+ export type SwitchActionType = {
15
+ type: 'JUMP_TO';
16
+ payload: { name: string; params?: object | undefined };
17
+ source?: string | undefined;
18
+ target?: string | undefined;
19
+ };
20
+
21
+ export type SwitchRouterOptions = DefaultRouterOptions & {
22
+ /**
23
+ * Control how going back should behave
24
+ * - `firstRoute` - return to the first defined route
25
+ * - `initialRoute` - return to the route from `initialRouteName`
26
+ * - `order` - return to the route defined before the focused route
27
+ * - `history` - return to last visited route; if the same route is visited multiple times, the older entries are dropped from the history
28
+ * - `fullHistory` - return to last visited route; doesn't drop duplicate entries unlike `history` - matches behavior of web pages
29
+ * - `none` - do not handle going back
30
+ */
31
+ backBehavior?: BackBehavior | undefined;
32
+ };
33
+
34
+ type BackBehavior =
35
+ | 'firstRoute'
36
+ | 'initialRoute'
37
+ | 'order'
38
+ | 'history'
39
+ | 'fullHistory'
40
+ | 'none';
41
+
42
+ type RouteHistory = {
43
+ type: 'route';
44
+ key: string;
45
+ params?: object | undefined;
46
+ };
47
+
48
+ type DrawerHistory = {
49
+ type: 'drawer';
50
+ status: 'open' | 'closed';
51
+ };
52
+
53
+ type SwitchRouterState<History> = Omit<NavigationState, 'history' | 'type'> & {
54
+ history: History[];
55
+ preloadedRouteKeys: string[];
56
+ };
57
+
58
+ type SwitchRouterStateMap = {
59
+ tab: SwitchRouterState<RouteHistory> & { type: 'tab' };
60
+ drawer: SwitchRouterState<RouteHistory | DrawerHistory> & {
61
+ type: 'drawer';
62
+ default: DrawerHistory['status'];
63
+ };
64
+ };
65
+
66
+ type SwitchRouterType = 'tab' | 'drawer';
67
+
68
+ const TYPE_ROUTE = 'route' as const;
69
+
70
+ const getRouteHistory = (
71
+ routes: Route<string>[],
72
+ index: number,
73
+ backBehavior: BackBehavior,
74
+ initialRouteName: string | undefined
75
+ ) => {
76
+ const route = routes[index];
77
+
78
+ if (route == null) {
79
+ throw new Error(`Couldn't find a route at index ${index}.`);
80
+ }
81
+
82
+ const history: RouteHistory[] = [
83
+ {
84
+ type: TYPE_ROUTE,
85
+ key: route.key,
86
+ params: backBehavior === 'fullHistory' ? route.params : undefined,
87
+ },
88
+ ];
89
+
90
+ let initialRouteIndex;
91
+
92
+ switch (backBehavior) {
93
+ case 'order':
94
+ for (let i = index; i > 0; i--) {
95
+ const route = routes[i - 1];
96
+
97
+ if (route == null) {
98
+ throw new Error(`Couldn't find a route at index ${i - 1}.`);
99
+ }
100
+
101
+ history.unshift({
102
+ type: TYPE_ROUTE,
103
+ key: route.key,
104
+ });
105
+ }
106
+
107
+ break;
108
+
109
+ case 'firstRoute':
110
+ if (index !== 0) {
111
+ const route = routes[0];
112
+
113
+ if (route == null) {
114
+ throw new Error("Couldn't find a route at index 0.");
115
+ }
116
+
117
+ history.unshift({
118
+ type: TYPE_ROUTE,
119
+ key: route.key,
120
+ });
121
+ }
122
+
123
+ break;
124
+
125
+ case 'initialRoute':
126
+ initialRouteIndex = routes.findIndex(
127
+ (route) => route.name === initialRouteName
128
+ );
129
+
130
+ initialRouteIndex = initialRouteIndex === -1 ? 0 : initialRouteIndex;
131
+
132
+ if (index !== initialRouteIndex) {
133
+ const route = routes[initialRouteIndex];
134
+
135
+ if (route == null) {
136
+ throw new Error(
137
+ `Couldn't find a route at index ${initialRouteIndex}.`
138
+ );
139
+ }
140
+
141
+ history.unshift({
142
+ type: TYPE_ROUTE,
143
+ key: route.key,
144
+ });
145
+ }
146
+
147
+ break;
148
+
149
+ case 'history':
150
+ case 'fullHistory':
151
+ // The history will fill up on navigation
152
+ break;
153
+ }
154
+
155
+ return history;
156
+ };
157
+
158
+ const changeIndex = <Type extends SwitchRouterType>(
159
+ state: Pick<SwitchRouterStateMap[Type], 'routes' | 'history'>,
160
+ index: number,
161
+ backBehavior: BackBehavior,
162
+ initialRouteName: string | undefined
163
+ ): Pick<SwitchRouterStateMap[Type], 'index' | 'history'> => {
164
+ let history = state.history;
165
+
166
+ if (backBehavior === 'history' || backBehavior === 'fullHistory') {
167
+ const currentRoute = state.routes[index];
168
+
169
+ if (currentRoute == null) {
170
+ throw new Error(`Couldn't find a route at index ${index}.`);
171
+ }
172
+
173
+ if (backBehavior === 'history') {
174
+ // Remove the existing key from the history to de-duplicate it
175
+ history = history.filter((it) =>
176
+ it.type === 'route' ? it.key !== currentRoute.key : true
177
+ );
178
+ } else if (backBehavior === 'fullHistory') {
179
+ const lastHistoryRouteItemIndex = history.findLastIndex(
180
+ (item) => item.type === 'route'
181
+ );
182
+
183
+ const lastHistoryRouteItem = history[lastHistoryRouteItemIndex];
184
+
185
+ if (
186
+ lastHistoryRouteItem?.type === 'route' &&
187
+ currentRoute.key === lastHistoryRouteItem.key
188
+ ) {
189
+ // For full-history, only remove if it matches the last route
190
+ // Useful for drawer, if current route was in history, then drawer state changed
191
+ // Then we only need to move the route to the front
192
+ history = [
193
+ ...history.slice(0, lastHistoryRouteItemIndex),
194
+ ...history.slice(lastHistoryRouteItemIndex + 1),
195
+ ];
196
+ }
197
+ }
198
+
199
+ history = history.concat({
200
+ type: TYPE_ROUTE,
201
+ key: currentRoute.key,
202
+ params: backBehavior === 'fullHistory' ? currentRoute.params : undefined,
203
+ });
204
+ } else {
205
+ // Preserve non-route entries (e.g. drawer status) when rebuilding the history
206
+ history = [
207
+ ...state.history.filter((it) => it.type !== 'route'),
208
+ ...getRouteHistory(state.routes, index, backBehavior, initialRouteName),
209
+ ];
210
+ }
211
+
212
+ return {
213
+ index,
214
+ history,
215
+ };
216
+ };
217
+
218
+ export function SwitchRouter<Type extends SwitchRouterType>({
219
+ initialRouteName,
220
+ backBehavior = 'firstRoute',
221
+ }: SwitchRouterOptions) {
222
+ type State = SwitchRouterStateMap[Type];
223
+
224
+ const router = {
225
+ ...BaseRouter,
226
+
227
+ getInitialState({
228
+ routeNames,
229
+ routeParamList,
230
+ }: RouterConfigOptions): SwitchRouterState<RouteHistory> {
231
+ const index =
232
+ initialRouteName !== undefined && routeNames.includes(initialRouteName)
233
+ ? routeNames.indexOf(initialRouteName)
234
+ : 0;
235
+
236
+ const routes = routeNames.map((name) => ({
237
+ name,
238
+ key: `${name}-${nanoid()}`,
239
+ params: routeParamList[name],
240
+ }));
241
+
242
+ return {
243
+ stale: false,
244
+ key: nanoid(),
245
+ index,
246
+ routeNames,
247
+ history: getRouteHistory(routes, index, backBehavior, initialRouteName),
248
+ routes,
249
+ preloadedRouteKeys: [],
250
+ };
251
+ },
252
+
253
+ getRehydratedState(
254
+ state: PartialState<State>,
255
+ { routeNames, routeParamList }: RouterConfigOptions
256
+ ): SwitchRouterState<RouteHistory> {
257
+ const routes = routeNames.map((name) => {
258
+ const route = state.routes.find((r) => r.name === name);
259
+
260
+ return {
261
+ ...route,
262
+ name,
263
+ key:
264
+ route && route.name === name && route.key
265
+ ? route.key
266
+ : `${name}-${nanoid()}`,
267
+ params:
268
+ routeParamList[name] !== undefined
269
+ ? {
270
+ ...routeParamList[name],
271
+ ...(route ? route.params : undefined),
272
+ }
273
+ : route
274
+ ? route.params
275
+ : undefined,
276
+ } satisfies Route<string>;
277
+ });
278
+
279
+ const currentRoute = state.routes[state.index ?? 0];
280
+ const index = Math.min(
281
+ Math.max(currentRoute ? routeNames.indexOf(currentRoute.name) : 0, 0),
282
+ routes.length - 1
283
+ );
284
+
285
+ const routeKeys = routes.map((route) => route.key);
286
+ const history: RouteHistory[] = [];
287
+
288
+ for (const item of state.history ?? []) {
289
+ if (item.type === 'route' && routeKeys.includes(item.key)) {
290
+ history.push(item);
291
+ }
292
+ }
293
+
294
+ const stateData: SwitchRouterState<RouteHistory> = {
295
+ stale: false,
296
+ key: nanoid(),
297
+ index,
298
+ routeNames,
299
+ history,
300
+ routes,
301
+ preloadedRouteKeys:
302
+ state.preloadedRouteKeys?.filter((key) => routeKeys.includes(key)) ??
303
+ [],
304
+ };
305
+
306
+ return {
307
+ ...stateData,
308
+ ...changeIndex<'tab'>(stateData, index, backBehavior, initialRouteName),
309
+ };
310
+ },
311
+
312
+ getStateForRouteNamesChange(
313
+ state: State,
314
+ {
315
+ routeNames,
316
+ routeParamList,
317
+ routeKeyChanges,
318
+ }: RouterConfigOptions & { routeKeyChanges: string[] }
319
+ ): State {
320
+ const routes = routeNames.map(
321
+ (name) =>
322
+ state.routes.find(
323
+ (r) => r.name === name && !routeKeyChanges.includes(r.name)
324
+ ) ?? {
325
+ name,
326
+ key: `${name}-${nanoid()}`,
327
+ params: routeParamList[name],
328
+ }
329
+ );
330
+
331
+ const routeKeys = new Set(routes.map((route) => route.key));
332
+
333
+ const currentRoute = state.routes[state.index];
334
+
335
+ if (currentRoute == null) {
336
+ throw new Error(`Couldn't find a route at index ${state.index}.`);
337
+ }
338
+
339
+ const history = state.history.filter(
340
+ // Type will always be 'route' for tabs, but could be different in a router extending this (e.g. drawer)
341
+ (item) => item.type !== 'route' || routeKeys.has(item.key)
342
+ );
343
+
344
+ let index = routeNames.indexOf(currentRoute.name);
345
+
346
+ if (index === -1) {
347
+ // The focused route was removed, so focus the most recently visited surviving route
348
+ const previousRouteItem = history.findLast(
349
+ (item): item is RouteHistory => item.type === 'route'
350
+ );
351
+
352
+ index = Math.max(
353
+ 0,
354
+ routes.findIndex((route) => route.key === previousRouteItem?.key)
355
+ );
356
+ }
357
+
358
+ return {
359
+ ...state,
360
+ routeNames,
361
+ routes,
362
+ ...changeIndex<Type>(
363
+ { routes, history },
364
+ index,
365
+ backBehavior,
366
+ initialRouteName
367
+ ),
368
+ preloadedRouteKeys: state.preloadedRouteKeys.filter((key) =>
369
+ routeKeys.has(key)
370
+ ),
371
+ };
372
+ },
373
+
374
+ getStateForRouteFocus(state: State, key: string): State {
375
+ const index = state.routes.findIndex((r) => r.key === key);
376
+
377
+ if (index === -1 || index === state.index) {
378
+ return state;
379
+ }
380
+
381
+ return {
382
+ ...state,
383
+ ...changeIndex<Type>(state, index, backBehavior, initialRouteName),
384
+ };
385
+ },
386
+
387
+ getStateForAction(
388
+ state: State,
389
+ action: SwitchActionType | CommonNavigationAction,
390
+ { routeParamList, routeGetIdList }: RouterConfigOptions
391
+ ): State | PartialState<State> | null {
392
+ switch (action.type) {
393
+ case 'JUMP_TO':
394
+ case 'NAVIGATE': {
395
+ const index = state.routes.findIndex(
396
+ (route) => route.name === action.payload.name
397
+ );
398
+
399
+ if (index === -1) {
400
+ return null;
401
+ }
402
+
403
+ const route = state.routes[index];
404
+
405
+ if (route == null) {
406
+ throw new Error(`Couldn't find a route at index ${index}.`);
407
+ }
408
+
409
+ const getId = routeGetIdList[route.name];
410
+
411
+ const currentId = getId?.({ params: route.params });
412
+ const nextId = getId?.({ params: action.payload.params });
413
+
414
+ const key =
415
+ currentId === nextId ? route.key : `${route.name}-${nanoid()}`;
416
+
417
+ let params;
418
+
419
+ if (
420
+ action.type === 'NAVIGATE' &&
421
+ action.payload.merge &&
422
+ currentId === nextId
423
+ ) {
424
+ params =
425
+ action.payload.params !== undefined ||
426
+ routeParamList[route.name] !== undefined
427
+ ? {
428
+ ...routeParamList[route.name],
429
+ ...route.params,
430
+ ...action.payload.params,
431
+ }
432
+ : route.params;
433
+ } else {
434
+ params = createParamsFromAction({ action, routeParamList });
435
+ }
436
+
437
+ const path =
438
+ action.type === 'NAVIGATE' && action.payload.path != null
439
+ ? action.payload.path
440
+ : route.path;
441
+
442
+ const nextRoute =
443
+ params !== route.params || path !== route.path || key !== route.key
444
+ ? { ...route, key, path, params }
445
+ : route;
446
+
447
+ const routes = state.routes.map((item, routeIndex) =>
448
+ routeIndex === index ? nextRoute : item
449
+ );
450
+
451
+ const updatedState: State = {
452
+ ...state,
453
+ routes,
454
+ history:
455
+ key === route.key
456
+ ? state.history
457
+ : state.history.filter(
458
+ (it) => it.type !== 'route' || it.key !== route.key
459
+ ),
460
+ };
461
+
462
+ return {
463
+ ...updatedState,
464
+ ...changeIndex<Type>(
465
+ updatedState,
466
+ index,
467
+ backBehavior,
468
+ initialRouteName
469
+ ),
470
+ preloadedRouteKeys: updatedState.preloadedRouteKeys.filter(
471
+ (key) => key !== route.key && key !== nextRoute.key
472
+ ),
473
+ };
474
+ }
475
+
476
+ case 'GO_BACK': {
477
+ const focusedRoute = state.routes[state.index];
478
+
479
+ if (focusedRoute == null) {
480
+ throw new Error(`Couldn't find a route at index ${state.index}.`);
481
+ }
482
+
483
+ if (state.history.length === 1 && !focusedRoute.history?.length) {
484
+ return null;
485
+ }
486
+
487
+ const lastHistoryItem = state.history[state.history.length - 1];
488
+
489
+ if (
490
+ lastHistoryItem?.type === 'route' &&
491
+ focusedRoute.history?.length
492
+ ) {
493
+ const routes = [...state.routes];
494
+ const history = [...focusedRoute.history];
495
+ const last = history.pop();
496
+
497
+ routes[state.index] = {
498
+ ...focusedRoute,
499
+ params:
500
+ last?.type === 'params' ? last.params : focusedRoute.params,
501
+ history,
502
+ };
503
+
504
+ return {
505
+ ...state,
506
+ routes,
507
+ };
508
+ }
509
+
510
+ const previousHistoryItem = state.history[state.history.length - 2];
511
+
512
+ if (
513
+ previousHistoryItem == null ||
514
+ previousHistoryItem.type !== 'route'
515
+ ) {
516
+ return null;
517
+ }
518
+
519
+ const previousKey = previousHistoryItem.key;
520
+ const index = state.routes.findLastIndex(
521
+ (route) => route.key === previousKey
522
+ );
523
+
524
+ if (index === -1) {
525
+ return null;
526
+ }
527
+
528
+ let routes = state.routes;
529
+
530
+ const previousRoute = routes[index];
531
+
532
+ if (previousRoute == null) {
533
+ throw new Error(`Couldn't find a route at index ${index}.`);
534
+ }
535
+
536
+ if (
537
+ backBehavior === 'fullHistory' &&
538
+ previousRoute.params !== previousHistoryItem.params
539
+ ) {
540
+ routes = [...state.routes];
541
+ routes[index] = {
542
+ ...previousRoute,
543
+ params: previousHistoryItem.params,
544
+ };
545
+ }
546
+
547
+ return {
548
+ ...state,
549
+ routes,
550
+ preloadedRouteKeys: state.preloadedRouteKeys.filter(
551
+ (key) => key !== previousRoute.key
552
+ ),
553
+ history: state.history.slice(0, -1),
554
+ index,
555
+ };
556
+ }
557
+
558
+ case 'PRELOAD': {
559
+ const routeIndex = state.routes.findIndex(
560
+ (route) => route.name === action.payload.name
561
+ );
562
+
563
+ if (routeIndex === -1) {
564
+ return null;
565
+ }
566
+
567
+ const route = state.routes[routeIndex];
568
+
569
+ if (route == null) {
570
+ throw new Error(`Couldn't find a route at index ${routeIndex}.`);
571
+ }
572
+
573
+ const getId = routeGetIdList[route.name];
574
+
575
+ const currentId = getId?.({ params: route.params });
576
+ const nextId = getId?.({ params: action.payload.params });
577
+
578
+ const key =
579
+ currentId === nextId ? route.key : `${route.name}-${nanoid()}`;
580
+
581
+ const params = createParamsFromAction({ action, routeParamList });
582
+ const newRoute =
583
+ params !== route.params ? { ...route, key, params } : route;
584
+
585
+ let history = state.history;
586
+
587
+ if (key !== route.key) {
588
+ history = history.filter(
589
+ (record) => record.type !== 'route' || record.key !== route.key
590
+ );
591
+
592
+ if (routeIndex === state.index) {
593
+ history = history.concat({
594
+ type: TYPE_ROUTE,
595
+ key: newRoute.key,
596
+ params:
597
+ backBehavior === 'fullHistory' ? newRoute.params : undefined,
598
+ });
599
+ }
600
+ }
601
+
602
+ return {
603
+ ...state,
604
+ preloadedRouteKeys: state.preloadedRouteKeys
605
+ .filter((key) => key !== route.key)
606
+ .concat(newRoute.key),
607
+ routes: state.routes.map((route, index) =>
608
+ index === routeIndex ? newRoute : route
609
+ ),
610
+ history,
611
+ };
612
+ }
613
+
614
+ default:
615
+ return BaseRouter.getStateForAction(state, action);
616
+ }
617
+ },
618
+ };
619
+
620
+ return router;
621
+ }