@gtkx/navigation 2.0.0-beta.12 → 2.0.0-beta.13

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 (86) hide show
  1. package/README.md +1 -1
  2. package/dist/drawer/create-drawer-navigator.d.ts +3 -1
  3. package/dist/drawer/create-drawer-navigator.d.ts.map +1 -1
  4. package/dist/drawer/create-drawer-navigator.js +10 -10
  5. package/dist/drawer/create-drawer-navigator.js.map +1 -1
  6. package/dist/drawer/drawer-view.js +2 -2
  7. package/dist/drawer/drawer-view.js.map +1 -1
  8. package/dist/index.d.ts +5 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +4 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/shared/scene-page.d.ts.map +1 -1
  13. package/dist/shared/scene-page.js +2 -5
  14. package/dist/shared/scene-page.js.map +1 -1
  15. package/dist/shared/use-has-loaded.d.ts +3 -0
  16. package/dist/shared/use-has-loaded.d.ts.map +1 -0
  17. package/dist/shared/use-has-loaded.js +10 -0
  18. package/dist/shared/use-has-loaded.js.map +1 -0
  19. package/dist/split-view/create-split-view-navigator.d.ts +7 -2
  20. package/dist/split-view/create-split-view-navigator.d.ts.map +1 -1
  21. package/dist/split-view/create-split-view-navigator.js +2 -6
  22. package/dist/split-view/create-split-view-navigator.js.map +1 -1
  23. package/dist/split-view/split-view-collapsed.d.ts +11 -0
  24. package/dist/split-view/split-view-collapsed.d.ts.map +1 -0
  25. package/dist/split-view/split-view-collapsed.js +17 -0
  26. package/dist/split-view/split-view-collapsed.js.map +1 -0
  27. package/dist/split-view/split-view.d.ts.map +1 -1
  28. package/dist/split-view/split-view.js +39 -14
  29. package/dist/split-view/split-view.js.map +1 -1
  30. package/dist/split-view/types.d.ts +9 -2
  31. package/dist/split-view/types.d.ts.map +1 -1
  32. package/dist/split-view/types.js.map +1 -1
  33. package/dist/stack/create-stack-navigator.d.ts +3 -1
  34. package/dist/stack/create-stack-navigator.d.ts.map +1 -1
  35. package/dist/stack/create-stack-navigator.js +0 -1
  36. package/dist/stack/create-stack-navigator.js.map +1 -1
  37. package/dist/stack/stack-view.js +1 -1
  38. package/dist/stack/stack-view.js.map +1 -1
  39. package/dist/tab-view/create-tab-view-navigator.d.ts +32 -0
  40. package/dist/tab-view/create-tab-view-navigator.d.ts.map +1 -0
  41. package/dist/tab-view/create-tab-view-navigator.js +21 -0
  42. package/dist/tab-view/create-tab-view-navigator.js.map +1 -0
  43. package/dist/tab-view/page-registry.d.ts +34 -0
  44. package/dist/tab-view/page-registry.d.ts.map +1 -0
  45. package/dist/tab-view/page-registry.js +70 -0
  46. package/dist/tab-view/page-registry.js.map +1 -0
  47. package/dist/tab-view/tab-view-host.d.ts +25 -0
  48. package/dist/tab-view/tab-view-host.d.ts.map +1 -0
  49. package/dist/tab-view/tab-view-host.js +284 -0
  50. package/dist/tab-view/tab-view-host.js.map +1 -0
  51. package/dist/tab-view/tab-view-router.d.ts +34 -0
  52. package/dist/tab-view/tab-view-router.d.ts.map +1 -0
  53. package/dist/tab-view/tab-view-router.js +359 -0
  54. package/dist/tab-view/tab-view-router.js.map +1 -0
  55. package/dist/tab-view/types.d.ts +240 -0
  56. package/dist/tab-view/types.d.ts.map +1 -0
  57. package/dist/tab-view/types.js +2 -0
  58. package/dist/tab-view/types.js.map +1 -0
  59. package/dist/tabs/create-tab-navigator.d.ts +3 -1
  60. package/dist/tabs/create-tab-navigator.d.ts.map +1 -1
  61. package/dist/tabs/create-tab-navigator.js +0 -1
  62. package/dist/tabs/create-tab-navigator.js.map +1 -1
  63. package/dist/tabs/tab-view.js +1 -1
  64. package/dist/tabs/tab-view.js.map +1 -1
  65. package/dist/theme.js +2 -2
  66. package/dist/theme.js.map +1 -1
  67. package/package.json +6 -6
  68. package/src/drawer/create-drawer-navigator.tsx +23 -11
  69. package/src/drawer/drawer-view.tsx +2 -2
  70. package/src/index.ts +30 -0
  71. package/src/shared/scene-page.tsx +2 -6
  72. package/src/shared/use-has-loaded.ts +13 -0
  73. package/src/split-view/create-split-view-navigator.tsx +16 -1
  74. package/src/split-view/split-view-collapsed.ts +21 -0
  75. package/src/split-view/split-view.tsx +92 -45
  76. package/src/split-view/types.ts +9 -2
  77. package/src/stack/create-stack-navigator.tsx +11 -1
  78. package/src/stack/stack-view.tsx +1 -1
  79. package/src/tab-view/create-tab-view-navigator.tsx +122 -0
  80. package/src/tab-view/page-registry.ts +139 -0
  81. package/src/tab-view/tab-view-host.tsx +474 -0
  82. package/src/tab-view/tab-view-router.ts +528 -0
  83. package/src/tab-view/types.ts +291 -0
  84. package/src/tabs/create-tab-navigator.tsx +11 -1
  85. package/src/tabs/tab-view.tsx +2 -2
  86. package/src/theme.ts +2 -2
@@ -0,0 +1,528 @@
1
+ import type {
2
+ CommonNavigationAction,
3
+ NavigationAction,
4
+ ParamListBase,
5
+ PartialState,
6
+ Router,
7
+ RouterConfigOptions,
8
+ } from "@react-navigation/core";
9
+ import { BaseRouter } from "@react-navigation/routers";
10
+ import type { TabViewActionType, TabViewNavigationState, TabViewOpenOptions, TabViewRouterOptions } from "./types.js";
11
+
12
+ type State = TabViewNavigationState<ParamListBase>;
13
+ type Route = State["routes"][number];
14
+ type Action = TabViewActionType | CommonNavigationAction;
15
+ type Result = State | PartialState<State> | null;
16
+ type OpenTabAction = Extract<TabViewActionType, { type: "OPEN_TAB" }>;
17
+ type OpenPayload = OpenTabAction["payload"];
18
+ type Placement = Pick<State, "routes" | "index" | "pinnedKeys" | "parents">;
19
+
20
+ /** A page already in the native view when the navigator mounts. */
21
+ type ExistingTab = {
22
+ route: Route;
23
+ pinned: boolean;
24
+ parent?: string;
25
+ selected: boolean;
26
+ };
27
+
28
+ type CloseRange = "CLOSE_OTHER_TABS" | "CLOSE_TABS_BEFORE" | "CLOSE_TABS_AFTER";
29
+
30
+ type TabViewRouterFactory = (options: TabViewRouterOptions) => Router<State, Action>;
31
+
32
+ const TabViewActions = {
33
+ openTab: (name: string, params?: object, options: TabViewOpenOptions = {}): TabViewActionType => ({
34
+ type: "OPEN_TAB",
35
+ payload: { name, params, ...options },
36
+ }),
37
+ closeTab: (key?: string): TabViewActionType => ({ type: "CLOSE_TAB", payload: { key } }),
38
+ closeOtherTabs: (key?: string): TabViewActionType => ({ type: "CLOSE_OTHER_TABS", payload: { key } }),
39
+ closeTabsBefore: (key?: string): TabViewActionType => ({ type: "CLOSE_TABS_BEFORE", payload: { key } }),
40
+ closeTabsAfter: (key?: string): TabViewActionType => ({ type: "CLOSE_TABS_AFTER", payload: { key } }),
41
+ selectTab: (key: string): TabViewActionType => ({ type: "SELECT_TAB", payload: { key } }),
42
+ reorderTab: (key: string, position: number): TabViewActionType => ({
43
+ type: "REORDER_TAB",
44
+ payload: { key, position },
45
+ }),
46
+ pinTab: (key: string, pinned: boolean): TabViewActionType => ({ type: "PIN_TAB", payload: { key, pinned } }),
47
+ };
48
+
49
+ const createRouteKey = (name: string): string => `${name}-${crypto.randomUUID()}`;
50
+
51
+ const mergeParams = (defaults: object | undefined, params: object | undefined): object | undefined =>
52
+ defaults === undefined ? params : { ...defaults, ...params };
53
+
54
+ const indexOfKey = (routes: readonly Route[], key: string | undefined): number =>
55
+ key === undefined ? -1 : routes.findIndex((route) => route.key === key);
56
+
57
+ const clamp = (value: number, min: number, max: number): number => Math.min(Math.max(value, min), max);
58
+
59
+ const isOpenTab = (action: NavigationAction): action is OpenTabAction => action.type === "OPEN_TAB";
60
+
61
+ const focusedKey = (state: Pick<State, "routes" | "index">): string | undefined => state.routes[state.index]?.key;
62
+
63
+ const hasRoute = (state: Pick<State, "routes">, key: string): boolean =>
64
+ state.routes.some((route) => route.key === key);
65
+
66
+ const closeRanges: Record<CloseRange, (position: number, target: number) => boolean> = {
67
+ CLOSE_OTHER_TABS: () => true,
68
+ CLOSE_TABS_BEFORE: (position, target) => position < target,
69
+ CLOSE_TABS_AFTER: (position, target) => position > target,
70
+ };
71
+
72
+ const withRoutes = (state: State, routes: Route[], focused: string | undefined): State => ({
73
+ ...state,
74
+ routes,
75
+ index: Math.max(indexOfKey(routes, focused), 0),
76
+ });
77
+
78
+ const isDescendantOf = (parents: Record<string, string>, key: string, ancestor: string): boolean => {
79
+ const seen = new Set<string>();
80
+ let current: string | undefined = key;
81
+
82
+ while (current !== undefined && !seen.has(current)) {
83
+ if (current === ancestor) {
84
+ return true;
85
+ }
86
+
87
+ seen.add(current);
88
+ current = parents[current];
89
+ }
90
+
91
+ return false;
92
+ };
93
+
94
+ const pruneParents = (routes: readonly Route[], parents: Record<string, string>): Record<string, string> => {
95
+ const kept: Record<string, string> = {};
96
+
97
+ for (const route of routes) {
98
+ let current: string | undefined = route.key;
99
+
100
+ while (current !== undefined && !(current in kept)) {
101
+ const parent: string | undefined = parents[current];
102
+
103
+ if (parent === undefined) {
104
+ break;
105
+ }
106
+
107
+ kept[current] = parent;
108
+ current = parent;
109
+ }
110
+ }
111
+
112
+ return kept;
113
+ };
114
+
115
+ const childPosition = (state: State, parent: string): number => {
116
+ let position = state.pinnedKeys.includes(parent) ? state.pinnedKeys.length - 1 : indexOfKey(state.routes, parent);
117
+
118
+ for (;;) {
119
+ position += 1;
120
+ const route = state.routes[position];
121
+
122
+ if (route === undefined || !isDescendantOf(state.parents, route.key, parent)) {
123
+ return position;
124
+ }
125
+ }
126
+ };
127
+
128
+ const selectAfterClose = (state: State, position: number): Route | undefined => {
129
+ const route = state.routes[position];
130
+ const parent = route === undefined ? undefined : state.parents[route.key];
131
+ const previous = state.routes[position - 1];
132
+
133
+ if (parent !== undefined && previous !== undefined) {
134
+ if (isDescendantOf(state.parents, previous.key, parent)) {
135
+ return previous;
136
+ }
137
+
138
+ if (state.pinnedKeys.includes(previous.key) && state.pinnedKeys.includes(parent)) {
139
+ return state.routes.find((candidate) => candidate.key === parent) ?? previous;
140
+ }
141
+ }
142
+
143
+ return state.routes[position + 1] ?? previous;
144
+ };
145
+
146
+ const replaceRoutes = (state: State, route: Route): State => ({
147
+ ...withRoutes(state, [route], route.key),
148
+ pinnedKeys: [],
149
+ parents: {},
150
+ });
151
+
152
+ const closeOne = (state: State, key: string, replacement?: Route): State | null => {
153
+ const position = indexOfKey(state.routes, key);
154
+
155
+ if (position === -1) {
156
+ return state;
157
+ }
158
+
159
+ if (state.routes.length === 1) {
160
+ return replacement === undefined ? null : replaceRoutes(state, replacement);
161
+ }
162
+
163
+ const focused = focusedKey(state) === key ? selectAfterClose(state, position)?.key : focusedKey(state);
164
+ const routes = state.routes.filter((route) => route.key !== key);
165
+
166
+ return {
167
+ ...withRoutes(state, routes, focused),
168
+ pinnedKeys: state.pinnedKeys.filter((pinned) => pinned !== key),
169
+ parents: pruneParents(routes, state.parents),
170
+ };
171
+ };
172
+
173
+ const closeMany = (state: State, keys: readonly string[]): State | null => {
174
+ const ordered = [...keys].sort((left, right) => indexOfKey(state.routes, right) - indexOfKey(state.routes, left));
175
+ let next = state;
176
+
177
+ for (const key of ordered) {
178
+ const result = closeOne(next, key);
179
+
180
+ if (result === null) {
181
+ return null;
182
+ }
183
+
184
+ next = result;
185
+ }
186
+
187
+ return next;
188
+ };
189
+
190
+ const unpinnedKeys = (state: State, target: number, keep: (position: number, target: number) => boolean): string[] =>
191
+ state.routes
192
+ .filter(
193
+ (route, position) => position !== target && !state.pinnedKeys.includes(route.key) && keep(position, target),
194
+ )
195
+ .map((route) => route.key);
196
+
197
+ const routeId = (
198
+ { routeGetIdList }: RouterConfigOptions,
199
+ name: string,
200
+ params: object | undefined,
201
+ ): string | undefined => routeGetIdList[name]?.({ params });
202
+
203
+ const lastRouteNamed = (state: State, name: string): Route | undefined =>
204
+ state.routes.findLast((route) => route.name === name);
205
+
206
+ const routeWithId = (state: State, name: string, id: string, config: RouterConfigOptions): Route | undefined =>
207
+ state.routes.findLast((route) => route.name === name && routeId(config, name, route.params) === id);
208
+
209
+ const focusRoute = (state: State, route: Route, params: object | undefined, select: boolean): State => {
210
+ const routes =
211
+ params === route.params
212
+ ? state.routes
213
+ : state.routes.map((candidate) => (candidate === route ? { ...route, params } : candidate));
214
+ const index = select ? indexOfKey(routes, route.key) : state.index;
215
+
216
+ return routes === state.routes && index === state.index ? state : { ...state, routes, index };
217
+ };
218
+
219
+ const openTab = (state: State, payload: OpenPayload, config: RouterConfigOptions): Result => {
220
+ const { name, key, select, position, pinned = false } = payload;
221
+ const { routeNames, routeParamList } = config;
222
+
223
+ if (!routeNames.includes(name)) {
224
+ return null;
225
+ }
226
+
227
+ const existing = indexOfKey(state.routes, key);
228
+
229
+ if (existing !== -1) {
230
+ return select === false ? state : { ...state, index: existing };
231
+ }
232
+
233
+ const params = mergeParams(routeParamList[name], payload.params);
234
+ const id = key === undefined ? routeId(config, name, payload.params) : undefined;
235
+ const same = id === undefined ? undefined : routeWithId(state, name, id, config);
236
+
237
+ if (same !== undefined) {
238
+ return focusRoute(state, same, params, select !== false);
239
+ }
240
+
241
+ const route: Route = {
242
+ key: key ?? createRouteKey(name),
243
+ name,
244
+ params,
245
+ };
246
+ const parent =
247
+ payload.parent !== undefined && indexOfKey(state.routes, payload.parent) !== -1 ? payload.parent : undefined;
248
+ const pinnedCount = state.pinnedKeys.length;
249
+ const index = pinned
250
+ ? clamp(position ?? pinnedCount, 0, pinnedCount)
251
+ : position !== undefined
252
+ ? clamp(position, pinnedCount, state.routes.length)
253
+ : parent !== undefined
254
+ ? childPosition(state, parent)
255
+ : state.routes.length;
256
+ const routes = [...state.routes.slice(0, index), route, ...state.routes.slice(index)];
257
+
258
+ return {
259
+ ...withRoutes(state, routes, select === false ? focusedKey(state) : route.key),
260
+ pinnedKeys: pinned
261
+ ? [...state.pinnedKeys.slice(0, index), route.key, ...state.pinnedKeys.slice(index)]
262
+ : state.pinnedKeys,
263
+ parents: parent === undefined ? state.parents : { ...state.parents, [route.key]: parent },
264
+ };
265
+ };
266
+
267
+ const selectTab = (state: State, key: string): State | null => {
268
+ const index = indexOfKey(state.routes, key);
269
+
270
+ if (index === -1) {
271
+ return null;
272
+ }
273
+
274
+ return index === state.index ? state : { ...state, index };
275
+ };
276
+
277
+ const moveRoute = (state: State, key: string, position: number): State => {
278
+ const route = state.routes.find((candidate) => candidate.key === key);
279
+
280
+ if (route === undefined) {
281
+ return state;
282
+ }
283
+
284
+ const others = state.routes.filter((candidate) => candidate.key !== key);
285
+
286
+ return withRoutes(state, [...others.slice(0, position), route, ...others.slice(position)], focusedKey(state));
287
+ };
288
+
289
+ const reorderTab = (state: State, key: string, position: number): State | null => {
290
+ const from = indexOfKey(state.routes, key);
291
+
292
+ if (from === -1) {
293
+ return null;
294
+ }
295
+
296
+ const pinnedCount = state.pinnedKeys.length;
297
+ const isPinned = state.pinnedKeys.includes(key);
298
+ const target = isPinned
299
+ ? clamp(position, 0, pinnedCount - 1)
300
+ : clamp(position, pinnedCount, state.routes.length - 1);
301
+
302
+ if (target === from) {
303
+ return state;
304
+ }
305
+
306
+ const moved = moveRoute(state, key, target);
307
+
308
+ return isPinned ? { ...moved, pinnedKeys: moved.routes.slice(0, pinnedCount).map((route) => route.key) } : moved;
309
+ };
310
+
311
+ const pinTab = (state: State, key: string, pinned: boolean): State | null => {
312
+ if (indexOfKey(state.routes, key) === -1) {
313
+ return null;
314
+ }
315
+
316
+ if (state.pinnedKeys.includes(key) === pinned) {
317
+ return state;
318
+ }
319
+
320
+ const pinnedCount = state.pinnedKeys.length;
321
+ const moved = moveRoute(state, key, pinned ? pinnedCount : pinnedCount - 1);
322
+
323
+ return {
324
+ ...moved,
325
+ pinnedKeys: pinned ? [...state.pinnedKeys, key] : state.pinnedKeys.filter((candidate) => candidate !== key),
326
+ };
327
+ };
328
+
329
+ const navigate = (
330
+ state: State,
331
+ payload: { name: string; params?: object; merge?: boolean },
332
+ config: RouterConfigOptions,
333
+ ): Result => {
334
+ const { name, params, merge } = payload;
335
+
336
+ if (!config.routeNames.includes(name)) {
337
+ return null;
338
+ }
339
+
340
+ const id = routeId(config, name, params);
341
+ const target = id === undefined ? lastRouteNamed(state, name) : routeWithId(state, name, id, config);
342
+
343
+ if (target === undefined) {
344
+ return openTab(state, { name, params }, config);
345
+ }
346
+
347
+ const next =
348
+ params === undefined
349
+ ? target.params
350
+ : merge === true
351
+ ? { ...target.params, ...params }
352
+ : mergeParams(config.routeParamList[name], params);
353
+
354
+ return focusRoute(state, target, next, true);
355
+ };
356
+
357
+ const initialRouteName = (options: TabViewRouterOptions, routeNames: string[]): string => {
358
+ const name =
359
+ options.initialRouteName !== undefined && routeNames.includes(options.initialRouteName)
360
+ ? options.initialRouteName
361
+ : routeNames[0];
362
+
363
+ if (name === undefined) {
364
+ throw new Error("A tab view navigator needs at least one screen.");
365
+ }
366
+
367
+ return name;
368
+ };
369
+
370
+ const initialRoute = (options: TabViewRouterOptions, { routeNames, routeParamList }: RouterConfigOptions): Route => {
371
+ const name = initialRouteName(options, routeNames);
372
+
373
+ return { key: createRouteKey(name), name, params: routeParamList[name] };
374
+ };
375
+
376
+ const initialPlacement = (options: TabViewRouterOptions, config: RouterConfigOptions): Placement => ({
377
+ routes: [initialRoute(options, config)],
378
+ index: 0,
379
+ pinnedKeys: [],
380
+ parents: {},
381
+ });
382
+
383
+ const existingPlacement = (tabs: readonly ExistingTab[], { routeParamList }: RouterConfigOptions): Placement => ({
384
+ routes: tabs.map(({ route }) => ({ ...route, params: mergeParams(routeParamList[route.name], route.params) })),
385
+ index: Math.max(
386
+ tabs.findIndex((tab) => tab.selected),
387
+ 0,
388
+ ),
389
+ pinnedKeys: tabs.filter((tab) => tab.pinned).map((tab) => tab.route.key),
390
+ parents: Object.fromEntries(
391
+ tabs.flatMap((tab) =>
392
+ tab.parent !== undefined && tabs.some((candidate) => candidate.route.key === tab.parent)
393
+ ? [[tab.route.key, tab.parent]]
394
+ : [],
395
+ ),
396
+ ),
397
+ });
398
+
399
+ const freshState = (placement: Placement, routeNames: string[]): State => ({
400
+ stale: false,
401
+ type: "tabview",
402
+ key: `tabview-${crypto.randomUUID()}`,
403
+ routeNames,
404
+ ...placement,
405
+ });
406
+
407
+ /**
408
+ * Creates the router factory of a tab view navigator.
409
+ *
410
+ * @param readExistingTabs Returns the pages already in the view when the navigator mounts, in view order.
411
+ */
412
+ const createTabViewRouter =
413
+ (readExistingTabs: () => readonly ExistingTab[]): TabViewRouterFactory =>
414
+ (options) => ({
415
+ ...BaseRouter,
416
+ type: "tabview",
417
+
418
+ getInitialState(config) {
419
+ const existing = readExistingTabs().filter((tab) => config.routeNames.includes(tab.route.name));
420
+
421
+ return freshState(
422
+ existing.length === 0 ? initialPlacement(options, config) : existingPlacement(existing, config),
423
+ config.routeNames,
424
+ );
425
+ },
426
+
427
+ getRehydratedState(partialState, config) {
428
+ if (partialState.stale === false) {
429
+ return partialState;
430
+ }
431
+
432
+ const { routeNames, routeParamList } = config;
433
+ const known = partialState.routes
434
+ .filter((route) => routeNames.includes(route.name))
435
+ .map((route) => ({
436
+ ...route,
437
+ key: route.key ?? createRouteKey(route.name),
438
+ params: mergeParams(routeParamList[route.name], route.params),
439
+ }));
440
+ const pinned = new Set(partialState.pinnedKeys ?? []);
441
+ const pinnedRoutes = known.filter((route) => pinned.has(route.key));
442
+ const routes = [...pinnedRoutes, ...known.filter((route) => !pinned.has(route.key))];
443
+
444
+ if (routes.length === 0) {
445
+ return freshState(initialPlacement(options, config), routeNames);
446
+ }
447
+
448
+ const focused = known[clamp(partialState.index ?? 0, 0, known.length - 1)]?.key;
449
+
450
+ return freshState(
451
+ {
452
+ routes,
453
+ index: Math.max(indexOfKey(routes, focused), 0),
454
+ pinnedKeys: pinnedRoutes.map((route) => route.key),
455
+ parents: pruneParents(routes, partialState.parents ?? {}),
456
+ },
457
+ routeNames,
458
+ );
459
+ },
460
+
461
+ getStateForRouteNamesChange(state, config) {
462
+ const { routeNames, routeKeyChanges } = config;
463
+ const routes = state.routes.filter(
464
+ (route) => routeNames.includes(route.name) && !routeKeyChanges.includes(route.name),
465
+ );
466
+
467
+ if (routes.length === 0) {
468
+ return { ...state, routeNames, ...initialPlacement(options, config) };
469
+ }
470
+
471
+ const keys = new Set(routes.map((route) => route.key));
472
+
473
+ return {
474
+ ...withRoutes(state, routes, focusedKey(state)),
475
+ routeNames,
476
+ pinnedKeys: state.pinnedKeys.filter((key) => keys.has(key)),
477
+ parents: pruneParents(routes, state.parents),
478
+ };
479
+ },
480
+
481
+ getStateForRouteFocus(state, key) {
482
+ return selectTab(state, key) ?? state;
483
+ },
484
+
485
+ getStateForAction(state, action, config) {
486
+ switch (action.type) {
487
+ case "OPEN_TAB":
488
+ return openTab(state, action.payload, config);
489
+ case "CLOSE_TAB": {
490
+ const key = action.payload.key ?? focusedKey(state);
491
+ const replacement = options.replaceLastTab === true ? initialRoute(options, config) : undefined;
492
+
493
+ return key === undefined ? null : closeOne(state, key, replacement);
494
+ }
495
+ case "CLOSE_OTHER_TABS":
496
+ case "CLOSE_TABS_BEFORE":
497
+ case "CLOSE_TABS_AFTER": {
498
+ const target = indexOfKey(state.routes, action.payload.key ?? focusedKey(state));
499
+
500
+ return target === -1
501
+ ? state
502
+ : closeMany(state, unpinnedKeys(state, target, closeRanges[action.type]));
503
+ }
504
+ case "SELECT_TAB":
505
+ return selectTab(state, action.payload.key);
506
+ case "REORDER_TAB":
507
+ return reorderTab(state, action.payload.key, action.payload.position);
508
+ case "PIN_TAB":
509
+ return pinTab(state, action.payload.key, action.payload.pinned);
510
+ case "NAVIGATE":
511
+ return navigate(state, action.payload, config);
512
+ default:
513
+ return BaseRouter.getStateForAction(state, action);
514
+ }
515
+ },
516
+
517
+ shouldActionChangeFocus(action) {
518
+ return (
519
+ action.type === "NAVIGATE" ||
520
+ action.type === "SELECT_TAB" ||
521
+ (isOpenTab(action) && action.payload.select !== false)
522
+ );
523
+ },
524
+
525
+ actionCreators: TabViewActions,
526
+ });
527
+
528
+ export { createRouteKey, createTabViewRouter, type ExistingTab, focusedKey, hasRoute, TabViewActions };