@react-navigation/routers 7.6.0 → 7.6.2

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