@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.
- package/package.json +52 -4
- package/src/NavigationContainer.tsx +79 -0
- package/src/createStaticNavigation.tsx +41 -0
- package/src/index.tsx +13 -0
- package/src/react-compat.ts +40 -0
- package/src/stack/index.tsx +23 -0
- package/src/stack/navigators/createLynxStackNavigator.tsx +72 -0
- package/src/stack/types.ts +66 -0
- package/src/stack/views/CardScreen.tsx +102 -0
- package/src/stack/views/LynxStackView.tsx +145 -0
- package/src/stack/views/LynxStackViewState.ts +258 -0
- package/src/stack/views/__tests__/LynxStackViewState.test.tsx +506 -0
- package/src/theming/DarkTheme.ts +16 -0
- package/src/theming/LightTheme.ts +17 -0
- package/src/theming/fonts.ts +25 -0
- package/src/types.ts +42 -0
|
@@ -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
|
+
}
|