@react-navigation/lynx 0.0.0 → 0.1.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,258 @@
1
+ // Ported verbatim from react-navigation's new native stack - the reducer is
2
+ // platform-agnostic, so the only edits are the renamed types and imports.
3
+ //
4
+ // packages/native-stack/src/next/views/NativeStackViewState.ts @ 7b32fd92d
5
+ //
6
+ // Keep it that way: when upstream moves, re-port rather than diverge.
7
+ import type {
8
+ ParamListBase,
9
+ Route,
10
+ StackNavigationState,
11
+ } from '@react-navigation/core';
12
+ import * as React from 'react';
13
+
14
+ import type {
15
+ LynxStackDescriptor,
16
+ LynxStackDescriptorMap,
17
+ } from '../types';
18
+
19
+ export type LynxStackViewState = {
20
+ previous: {
21
+ index: number;
22
+ routes: Route<string>[];
23
+ descriptors: LynxStackDescriptorMap;
24
+ };
25
+ renderedRoutes: Route<string>[];
26
+ poppedByKey: Map<
27
+ string,
28
+ {
29
+ descriptor: LynxStackDescriptor;
30
+ previousDescriptor: LynxStackDescriptor | undefined;
31
+ focusedReplacementKey: string | undefined;
32
+ }
33
+ >;
34
+ nativelyDismissedRouteKeys: Set<string>;
35
+ };
36
+
37
+ export type LynxStackViewStateAction =
38
+ | {
39
+ type: 'SYNC_STATE';
40
+ index: number;
41
+ routes: Route<string>[];
42
+ descriptors: LynxStackDescriptorMap;
43
+ }
44
+ | { type: 'SYNC_DESCRIPTORS'; descriptors: LynxStackDescriptorMap }
45
+ | { type: 'REMOVE_POPPED_ROUTE'; key: string }
46
+ | { type: 'ADD_NATIVELY_DISMISSED_ROUTES'; keys: string[] };
47
+
48
+ export function reducer(
49
+ state: LynxStackViewState,
50
+ action: LynxStackViewStateAction
51
+ ): LynxStackViewState {
52
+ switch (action.type) {
53
+ case 'SYNC_STATE': {
54
+ const routeIndexByKey = new Map(
55
+ action.routes.map((route, index) => [route.key, index])
56
+ );
57
+
58
+ const poppedByKey = new Map(state.poppedByKey);
59
+
60
+ // Routes might have been added back before their native dismissal finished
61
+ // So we need to remove them from the popped list
62
+ for (const key of routeIndexByKey.keys()) {
63
+ poppedByKey.delete(key);
64
+ }
65
+
66
+ const previousActiveRoutes = state.previous.routes.slice(
67
+ 0,
68
+ state.previous.index + 1
69
+ );
70
+
71
+ const focusedRoute = action.routes[action.index];
72
+
73
+ const focusedReplacementKey =
74
+ focusedRoute != null &&
75
+ !previousActiveRoutes.some((route) => route.key === focusedRoute.key)
76
+ ? focusedRoute.key
77
+ : undefined;
78
+
79
+ let previousRetainedActiveRouteIndex = -1;
80
+
81
+ // Active routes need to keep their relative order
82
+ // Detached routes can move when a preloaded route becomes active
83
+ for (const [index, route] of previousActiveRoutes.entries()) {
84
+ const retainedRouteIndex = routeIndexByKey.get(route.key);
85
+
86
+ if (retainedRouteIndex != null) {
87
+ if (retainedRouteIndex <= action.index) {
88
+ if (retainedRouteIndex < previousRetainedActiveRouteIndex) {
89
+ throw new Error(
90
+ `Changing the order of active routes is not supported in native stack.`
91
+ );
92
+ }
93
+
94
+ previousRetainedActiveRouteIndex = retainedRouteIndex;
95
+ }
96
+
97
+ continue;
98
+ }
99
+
100
+ // Track routes that were removed from the active routes and need to stay rendered
101
+ const descriptor = state.previous.descriptors[route.key];
102
+
103
+ if (
104
+ descriptor == null ||
105
+ // Skip routes that were already dismissed natively
106
+ // We don't want to keep them rendered
107
+ state.nativelyDismissedRouteKeys.has(route.key)
108
+ ) {
109
+ continue;
110
+ }
111
+
112
+ const previousRoute = previousActiveRoutes[index - 1];
113
+
114
+ // Store a snapshot of the previous route's latest available descriptor
115
+ // This is necessary to properly handle back button during the pop
116
+ // e.g. back button title, `canGoBack` etc.
117
+ const previousDescriptor =
118
+ previousRoute == null
119
+ ? undefined
120
+ : (action.descriptors[previousRoute.key] ??
121
+ // Fallback to old descriptor if previous route was also removed
122
+ state.previous.descriptors[previousRoute.key]);
123
+
124
+ poppedByKey.set(route.key, {
125
+ descriptor,
126
+ previousDescriptor,
127
+ focusedReplacementKey,
128
+ });
129
+ }
130
+
131
+ const renderedRoutes = [...action.routes];
132
+
133
+ // Add popped routes back to the rendered routes list
134
+ // They need to stay rendered till the pop animation is finished
135
+ // We anchor them according to the next route that is still present after them.
136
+ // So if a route is replaced, the new route stays below the route being popped.
137
+ // Otherwise we append them at the end in their original order.
138
+ for (const [index, route] of state.renderedRoutes.entries()) {
139
+ if (!poppedByKey.has(route.key)) {
140
+ continue;
141
+ }
142
+
143
+ const nextRoute = state.renderedRoutes
144
+ .slice(index + 1)
145
+ .find((route) => routeIndexByKey.has(route.key));
146
+
147
+ if (nextRoute == null) {
148
+ renderedRoutes.push(route);
149
+ } else {
150
+ const nextIndex = renderedRoutes.findIndex(
151
+ (route) => route.key === nextRoute.key
152
+ );
153
+
154
+ renderedRoutes.splice(nextIndex, 0, route);
155
+ }
156
+ }
157
+
158
+ // Native dismissal keys are only needed for the next state sync
159
+ // So we don't need to keep the list anymore
160
+ const nativelyDismissedRouteKeys =
161
+ state.nativelyDismissedRouteKeys.size === 0
162
+ ? state.nativelyDismissedRouteKeys
163
+ : new Set<string>();
164
+
165
+ return {
166
+ previous: {
167
+ index: action.index,
168
+ routes: action.routes,
169
+ descriptors: action.descriptors,
170
+ },
171
+ renderedRoutes,
172
+ poppedByKey,
173
+ nativelyDismissedRouteKeys,
174
+ };
175
+ }
176
+
177
+ case 'SYNC_DESCRIPTORS':
178
+ return {
179
+ ...state,
180
+ previous: {
181
+ ...state.previous,
182
+ descriptors: action.descriptors,
183
+ },
184
+ };
185
+
186
+ case 'REMOVE_POPPED_ROUTE': {
187
+ if (!state.poppedByKey.has(action.key)) {
188
+ return state;
189
+ }
190
+
191
+ const poppedByKey = new Map(state.poppedByKey);
192
+
193
+ poppedByKey.delete(action.key);
194
+
195
+ return {
196
+ ...state,
197
+ renderedRoutes: state.renderedRoutes.filter(
198
+ (route) => route.key !== action.key
199
+ ),
200
+ poppedByKey,
201
+ };
202
+ }
203
+
204
+ case 'ADD_NATIVELY_DISMISSED_ROUTES': {
205
+ if (
206
+ action.keys.every((key) => state.nativelyDismissedRouteKeys.has(key))
207
+ ) {
208
+ return state;
209
+ }
210
+
211
+ return {
212
+ ...state,
213
+ nativelyDismissedRouteKeys: new Set([
214
+ ...state.nativelyDismissedRouteKeys,
215
+ ...action.keys,
216
+ ]),
217
+ };
218
+ }
219
+ }
220
+ }
221
+
222
+ export function useViewState({
223
+ state,
224
+ descriptors,
225
+ }: {
226
+ state: StackNavigationState<ParamListBase>;
227
+ descriptors: LynxStackDescriptorMap;
228
+ }) {
229
+ const [view, dispatch] = React.useReducer(reducer, {
230
+ previous: {
231
+ index: state.index,
232
+ routes: state.routes,
233
+ descriptors,
234
+ },
235
+ renderedRoutes: state.routes,
236
+ poppedByKey: new Map(),
237
+ nativelyDismissedRouteKeys: new Set<string>(),
238
+ });
239
+
240
+ if (
241
+ state.index !== view.previous.index ||
242
+ state.routes !== view.previous.routes
243
+ ) {
244
+ dispatch({
245
+ type: 'SYNC_STATE',
246
+ index: state.index,
247
+ routes: state.routes,
248
+ descriptors,
249
+ });
250
+ } else if (descriptors !== view.previous.descriptors) {
251
+ dispatch({
252
+ type: 'SYNC_DESCRIPTORS',
253
+ descriptors,
254
+ });
255
+ }
256
+
257
+ return [view, dispatch] as const;
258
+ }