@react-navigation/core 8.0.0-alpha.26 → 8.0.0-alpha.28
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/lib/module/BaseNavigationContainer.js +12 -6
- package/lib/module/BaseNavigationContainer.js.map +1 -1
- package/lib/module/ConsumedParamsContext.js.map +1 -1
- package/lib/module/DataLoading.js +29 -4
- package/lib/module/DataLoading.js.map +1 -1
- package/lib/module/NavigationIndependentTree.js +8 -4
- package/lib/module/NavigationIndependentTree.js.map +1 -1
- package/lib/module/SceneView.js +16 -39
- package/lib/module/SceneView.js.map +1 -1
- package/lib/module/StaticNavigation.js +4 -3
- package/lib/module/StaticNavigation.js.map +1 -1
- package/lib/module/findFocusedRoute.js +1 -1
- package/lib/module/findFocusedRoute.js.map +1 -1
- package/lib/module/getActionFromState.js +1 -2
- package/lib/module/getActionFromState.js.map +1 -1
- package/lib/module/getPathFromState.js +41 -26
- package/lib/module/getPathFromState.js.map +1 -1
- package/lib/module/getPatternParts.js +15 -2
- package/lib/module/getPatternParts.js.map +1 -1
- package/lib/module/getStateFromPath.js +45 -19
- package/lib/module/getStateFromPath.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/useFocusEvents.js +6 -2
- package/lib/module/useFocusEvents.js.map +1 -1
- package/lib/module/useLazyValue.js.map +1 -1
- package/lib/module/useNavigationBuilder.js +51 -27
- package/lib/module/useNavigationBuilder.js.map +1 -1
- package/lib/module/useOnAction.js +25 -17
- package/lib/module/useOnAction.js.map +1 -1
- package/lib/module/useOnPreventRemove.js +1 -3
- package/lib/module/useOnPreventRemove.js.map +1 -1
- package/lib/module/useRouteCache.js +1 -3
- package/lib/module/useRouteCache.js.map +1 -1
- package/lib/module/useSyncState.js +3 -2
- package/lib/module/useSyncState.js.map +1 -1
- package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
- package/lib/typescript/src/ConsumedParamsContext.d.ts +1 -6
- package/lib/typescript/src/ConsumedParamsContext.d.ts.map +1 -1
- package/lib/typescript/src/DataLoading.d.ts +1 -0
- package/lib/typescript/src/DataLoading.d.ts.map +1 -1
- package/lib/typescript/src/NavigationIndependentTree.d.ts.map +1 -1
- package/lib/typescript/src/SceneView.d.ts.map +1 -1
- package/lib/typescript/src/StaticNavigation.d.ts +39 -30
- package/lib/typescript/src/StaticNavigation.d.ts.map +1 -1
- package/lib/typescript/src/getPathFromState.d.ts.map +1 -1
- package/lib/typescript/src/getPatternParts.d.ts.map +1 -1
- package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +2 -2
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/useFocusEvents.d.ts.map +1 -1
- package/lib/typescript/src/useLazyValue.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
- package/lib/typescript/src/useOnAction.d.ts.map +1 -1
- package/lib/typescript/src/useOnPreventRemove.d.ts.map +1 -1
- package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
- package/lib/typescript/src/useSyncState.d.ts.map +1 -1
- package/lib/typescript/src/utilities.d.ts +10 -0
- package/lib/typescript/src/utilities.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/BaseNavigationContainer.tsx +13 -7
- package/src/ConsumedParamsContext.tsx +1 -6
- package/src/DataLoading.tsx +68 -4
- package/src/NavigationIndependentTree.tsx +8 -5
- package/src/SceneView.tsx +18 -52
- package/src/StaticNavigation.tsx +88 -64
- package/src/findFocusedRoute.tsx +1 -1
- package/src/getActionFromState.tsx +7 -3
- package/src/getPathFromState.tsx +59 -32
- package/src/getPatternParts.tsx +15 -2
- package/src/getStateFromPath.tsx +77 -17
- package/src/index.tsx +5 -0
- package/src/types.tsx +2 -2
- package/src/useDescriptors.tsx +1 -1
- package/src/useFocusEvents.tsx +7 -5
- package/src/useLazyValue.tsx +2 -2
- package/src/useNavigationBuilder.tsx +82 -37
- package/src/useOnAction.tsx +36 -40
- package/src/useOnPreventRemove.tsx +4 -3
- package/src/useRouteCache.tsx +1 -4
- package/src/useSyncState.tsx +3 -2
- package/src/utilities.tsx +19 -0
package/src/getPathFromState.tsx
CHANGED
|
@@ -16,12 +16,22 @@ type State = NavigationState | Omit<PartialState<NavigationState>, 'stale'>;
|
|
|
16
16
|
|
|
17
17
|
type StringifyConfig = Record<string, ((value: unknown) => string) | undefined>;
|
|
18
18
|
|
|
19
|
+
type SerializedParamValue = string | string[] | null;
|
|
20
|
+
|
|
19
21
|
type ConfigItem = {
|
|
20
22
|
parts?: PatternPart[] | undefined;
|
|
23
|
+
ownParts: PatternPart[];
|
|
21
24
|
stringify?: StringifyConfig | undefined;
|
|
22
25
|
screens?: Record<string, ConfigItem> | undefined;
|
|
23
26
|
};
|
|
24
27
|
|
|
28
|
+
const serializeParamValue = (value: unknown): SerializedParamValue =>
|
|
29
|
+
value === null
|
|
30
|
+
? null
|
|
31
|
+
: Array.isArray(value)
|
|
32
|
+
? value.map(String)
|
|
33
|
+
: String(value);
|
|
34
|
+
|
|
25
35
|
const encodePathParam = (value: string) => {
|
|
26
36
|
let result = '';
|
|
27
37
|
|
|
@@ -170,8 +180,6 @@ export function getPathFromState<ParamList extends {}>(
|
|
|
170
180
|
let path = '/';
|
|
171
181
|
let current: State | undefined = state;
|
|
172
182
|
|
|
173
|
-
const allParams: Record<string, string> = {};
|
|
174
|
-
|
|
175
183
|
while (current) {
|
|
176
184
|
let index: number = typeof current.index === 'number' ? current.index : 0;
|
|
177
185
|
|
|
@@ -186,7 +194,11 @@ export function getPathFromState<ParamList extends {}>(
|
|
|
186
194
|
|
|
187
195
|
let parts: PatternPart[] | undefined;
|
|
188
196
|
|
|
189
|
-
|
|
197
|
+
// Values for each pattern part object, so the same param name
|
|
198
|
+
// at different nesting levels gets the value from its own level
|
|
199
|
+
const partValues = new Map<PatternPart, SerializedParamValue | undefined>();
|
|
200
|
+
|
|
201
|
+
let focusedParams: Record<string, SerializedParamValue> | undefined;
|
|
190
202
|
let currentOptions = configs;
|
|
191
203
|
|
|
192
204
|
// Keep all the route names that appeared during going deeper in config in case the pattern is resolved to undefined
|
|
@@ -203,12 +215,14 @@ export function getPathFromState<ParamList extends {}>(
|
|
|
203
215
|
|
|
204
216
|
parts = config.parts;
|
|
205
217
|
|
|
218
|
+
const ownParts = config.ownParts;
|
|
219
|
+
|
|
206
220
|
nestedRouteNames.push(route.name);
|
|
207
221
|
|
|
208
222
|
if (route.params) {
|
|
209
223
|
const options = config;
|
|
210
224
|
const params = route.params as Record<string, unknown>;
|
|
211
|
-
const currentParams: Record<string,
|
|
225
|
+
const currentParams: Record<string, SerializedParamValue> = {};
|
|
212
226
|
|
|
213
227
|
for (const key in params) {
|
|
214
228
|
const value = params[key];
|
|
@@ -216,7 +230,7 @@ export function getPathFromState<ParamList extends {}>(
|
|
|
216
230
|
if (value === undefined) {
|
|
217
231
|
let optional = false;
|
|
218
232
|
|
|
219
|
-
for (const part of
|
|
233
|
+
for (const part of ownParts) {
|
|
220
234
|
if (part.param === key) {
|
|
221
235
|
optional = part.optional === true;
|
|
222
236
|
break;
|
|
@@ -228,39 +242,52 @@ export function getPathFromState<ParamList extends {}>(
|
|
|
228
242
|
}
|
|
229
243
|
}
|
|
230
244
|
|
|
231
|
-
const stringify = options.stringify?.[key]
|
|
245
|
+
const stringify = options.stringify?.[key];
|
|
232
246
|
|
|
233
|
-
currentParams[key] = stringify
|
|
247
|
+
currentParams[key] = stringify
|
|
248
|
+
? stringify(value)
|
|
249
|
+
: serializeParamValue(value);
|
|
234
250
|
}
|
|
235
251
|
|
|
236
|
-
|
|
237
|
-
|
|
252
|
+
const claimedParams = new Set<string>();
|
|
253
|
+
|
|
254
|
+
for (const part of ownParts) {
|
|
255
|
+
if (part.param && part.param in currentParams) {
|
|
256
|
+
const value = currentParams[part.param];
|
|
257
|
+
|
|
258
|
+
if (value !== undefined) {
|
|
259
|
+
partValues.set(part, value);
|
|
260
|
+
claimedParams.add(part.param);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
238
263
|
}
|
|
239
264
|
|
|
240
265
|
if (focusedRoute === route) {
|
|
241
266
|
// If this is the focused route, keep the params for later use
|
|
242
267
|
// We save it here since it's been stringified already
|
|
268
|
+
// Params claimed by the pattern shouldn't be repeated in the query string
|
|
243
269
|
focusedParams = {};
|
|
244
270
|
|
|
245
271
|
for (const key in currentParams) {
|
|
246
|
-
let inPattern = false;
|
|
247
|
-
|
|
248
|
-
for (const part of parts ?? []) {
|
|
249
|
-
if (part.param === key) {
|
|
250
|
-
inPattern = true;
|
|
251
|
-
break;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
272
|
const value = currentParams[key];
|
|
256
273
|
|
|
257
|
-
if (
|
|
274
|
+
if (
|
|
275
|
+
!claimedParams.has(key) &&
|
|
276
|
+
value !== undefined &&
|
|
277
|
+
value !== 'undefined'
|
|
278
|
+
) {
|
|
258
279
|
focusedParams[key] = value;
|
|
259
280
|
}
|
|
260
281
|
}
|
|
261
282
|
}
|
|
262
283
|
}
|
|
263
284
|
|
|
285
|
+
for (const part of ownParts) {
|
|
286
|
+
if (part.param && !partValues.has(part)) {
|
|
287
|
+
partValues.set(part, undefined);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
264
291
|
// If there is no `screens` property or no nested state, we return pattern
|
|
265
292
|
const routeState = getRouteState(route, config);
|
|
266
293
|
|
|
@@ -298,7 +325,9 @@ export function getPathFromState<ParamList extends {}>(
|
|
|
298
325
|
if (parts) {
|
|
299
326
|
let index = 0;
|
|
300
327
|
|
|
301
|
-
for (const
|
|
328
|
+
for (const part of parts) {
|
|
329
|
+
const { segment, param, optional } = part;
|
|
330
|
+
|
|
302
331
|
if (index > 0) {
|
|
303
332
|
path += '/';
|
|
304
333
|
}
|
|
@@ -315,7 +344,7 @@ export function getPathFromState<ParamList extends {}>(
|
|
|
315
344
|
|
|
316
345
|
// If the path has a pattern for a param, put the param in the path
|
|
317
346
|
if (param) {
|
|
318
|
-
const value =
|
|
347
|
+
const value = partValues.get(part);
|
|
319
348
|
|
|
320
349
|
if (value === undefined && optional) {
|
|
321
350
|
// Optional params without value assigned in route.params should be ignored
|
|
@@ -341,7 +370,7 @@ export function getPathFromState<ParamList extends {}>(
|
|
|
341
370
|
const params = focusedRoute.params as Record<string, unknown>;
|
|
342
371
|
|
|
343
372
|
for (const key in params) {
|
|
344
|
-
const value =
|
|
373
|
+
const value = serializeParamValue(params[key]);
|
|
345
374
|
|
|
346
375
|
if (value !== 'undefined') {
|
|
347
376
|
focusedParams[key] = value;
|
|
@@ -385,14 +414,13 @@ const createConfigItem = (
|
|
|
385
414
|
parentParts?: PatternPart[]
|
|
386
415
|
): ConfigItem => {
|
|
387
416
|
if (typeof config === 'string') {
|
|
388
|
-
|
|
389
|
-
const parts = getPatternParts(config);
|
|
417
|
+
const ownParts = getPatternParts(config);
|
|
390
418
|
|
|
391
419
|
if (parentParts) {
|
|
392
|
-
return { parts: [...parentParts, ...
|
|
420
|
+
return { parts: [...parentParts, ...ownParts], ownParts };
|
|
393
421
|
}
|
|
394
422
|
|
|
395
|
-
return { parts };
|
|
423
|
+
return { parts: ownParts, ownParts };
|
|
396
424
|
}
|
|
397
425
|
|
|
398
426
|
if (config.exact && config.path === undefined) {
|
|
@@ -403,14 +431,12 @@ const createConfigItem = (
|
|
|
403
431
|
|
|
404
432
|
// If an object is specified as the value (e.g. Foo: { ... }),
|
|
405
433
|
// It can have `path` property and `screens` prop which has nested configs
|
|
434
|
+
const ownParts = config.path ? getPatternParts(config.path) : [];
|
|
406
435
|
const parts =
|
|
407
436
|
config.exact !== true
|
|
408
|
-
? [
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
]
|
|
412
|
-
: config.path
|
|
413
|
-
? getPatternParts(config.path)
|
|
437
|
+
? [...(parentParts || []), ...ownParts]
|
|
438
|
+
: ownParts.length
|
|
439
|
+
? ownParts
|
|
414
440
|
: undefined;
|
|
415
441
|
|
|
416
442
|
const screens =
|
|
@@ -420,6 +446,7 @@ const createConfigItem = (
|
|
|
420
446
|
|
|
421
447
|
return {
|
|
422
448
|
parts,
|
|
449
|
+
ownParts,
|
|
423
450
|
stringify: config.stringify,
|
|
424
451
|
screens,
|
|
425
452
|
};
|
package/src/getPatternParts.tsx
CHANGED
|
@@ -15,6 +15,8 @@ export function getPatternParts(path: string): PatternPart[] {
|
|
|
15
15
|
|
|
16
16
|
let isRegex = false;
|
|
17
17
|
let isParam = false;
|
|
18
|
+
let isEscaped = false;
|
|
19
|
+
let isInCharClass = false;
|
|
18
20
|
let regexInnerParens = 0;
|
|
19
21
|
|
|
20
22
|
// One extra iteration to add the last character
|
|
@@ -34,7 +36,7 @@ export function getPatternParts(path: string): PatternPart[] {
|
|
|
34
36
|
`Encountered ':' in the middle of a segment in path: ${path}`
|
|
35
37
|
);
|
|
36
38
|
}
|
|
37
|
-
} else if (char === '(') {
|
|
39
|
+
} else if (char === '(' && !isEscaped && !isInCharClass) {
|
|
38
40
|
if (isParam) {
|
|
39
41
|
if (isRegex) {
|
|
40
42
|
// The '(' is part of the regex if we're already inside one
|
|
@@ -47,7 +49,7 @@ export function getPatternParts(path: string): PatternPart[] {
|
|
|
47
49
|
`Encountered '(' without preceding ':' in path: ${path}`
|
|
48
50
|
);
|
|
49
51
|
}
|
|
50
|
-
} else if (char === ')') {
|
|
52
|
+
} else if (char === ')' && !isEscaped && !isInCharClass) {
|
|
51
53
|
if (isParam && isRegex) {
|
|
52
54
|
if (regexInnerParens) {
|
|
53
55
|
// The ')' is part of the regex if we're already inside one
|
|
@@ -102,6 +104,17 @@ export function getPatternParts(path: string): PatternPart[] {
|
|
|
102
104
|
if (isRegex) {
|
|
103
105
|
current.regex = current.regex || '';
|
|
104
106
|
current.regex += char;
|
|
107
|
+
|
|
108
|
+
// Track escapes and character classes so parens inside them are not special
|
|
109
|
+
if (isEscaped) {
|
|
110
|
+
isEscaped = false;
|
|
111
|
+
} else if (char === '\\') {
|
|
112
|
+
isEscaped = true;
|
|
113
|
+
} else if (char === '[') {
|
|
114
|
+
isInCharClass = true;
|
|
115
|
+
} else if (char === ']') {
|
|
116
|
+
isInCharClass = false;
|
|
117
|
+
}
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
if (isParam && !isRegex) {
|
package/src/getStateFromPath.tsx
CHANGED
|
@@ -39,7 +39,12 @@ type RouteConfig = {
|
|
|
39
39
|
regex?: RegExp | undefined;
|
|
40
40
|
pattern: string;
|
|
41
41
|
segments: string[];
|
|
42
|
-
params: {
|
|
42
|
+
params: {
|
|
43
|
+
screen: string;
|
|
44
|
+
name: string;
|
|
45
|
+
index: number;
|
|
46
|
+
regex?: RegExp | undefined;
|
|
47
|
+
}[];
|
|
43
48
|
routeNames: string[];
|
|
44
49
|
parse?: ParseConfig | undefined;
|
|
45
50
|
explicitParamNames?: Set<string> | undefined;
|
|
@@ -69,6 +74,7 @@ type ConfigResources = {
|
|
|
69
74
|
configs: RouteConfig[];
|
|
70
75
|
configsByScreen: Record<string, RouteConfig[]>;
|
|
71
76
|
configsByPattern: Record<string, RouteConfig[]>;
|
|
77
|
+
prefixRegex?: RegExp | undefined;
|
|
72
78
|
};
|
|
73
79
|
|
|
74
80
|
const INVALID_SCHEMA_RESULT_ERROR =
|
|
@@ -88,6 +94,17 @@ const NESTED_SCREEN_PARAM_NAMES = [
|
|
|
88
94
|
'pop',
|
|
89
95
|
];
|
|
90
96
|
|
|
97
|
+
const getStaticSegmentPattern = (segment: string) =>
|
|
98
|
+
Array.from(segment, (char) => {
|
|
99
|
+
const encoded = encodeURIComponent(char);
|
|
100
|
+
const percentEncoded =
|
|
101
|
+
encoded === char
|
|
102
|
+
? `%${char.charCodeAt(0).toString(16).padStart(2, '0').toUpperCase()}`
|
|
103
|
+
: encoded;
|
|
104
|
+
|
|
105
|
+
return `(?:${escape(char)}|${escape(percentEncoded)})`;
|
|
106
|
+
}).join('');
|
|
107
|
+
|
|
91
108
|
const getExplicitParamNames = (parse?: ParseConfig) => {
|
|
92
109
|
const names: string[] = [];
|
|
93
110
|
|
|
@@ -161,8 +178,13 @@ export function getStateFromPath<ParamList extends {}>(
|
|
|
161
178
|
options?: Options<ParamList>,
|
|
162
179
|
previous?: NavigationState
|
|
163
180
|
): ResultState | undefined {
|
|
164
|
-
const {
|
|
165
|
-
|
|
181
|
+
const {
|
|
182
|
+
initialRoutes,
|
|
183
|
+
configs,
|
|
184
|
+
configsByScreen,
|
|
185
|
+
configsByPattern,
|
|
186
|
+
prefixRegex,
|
|
187
|
+
} = getConfigResources(options);
|
|
166
188
|
|
|
167
189
|
const screens = options?.screens;
|
|
168
190
|
|
|
@@ -183,24 +205,20 @@ export function getStateFromPath<ParamList extends {}>(
|
|
|
183
205
|
let remaining = path
|
|
184
206
|
.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
|
|
185
207
|
.replace(/^\//, '') // Remove extra leading slash
|
|
186
|
-
.replace(/\?.*$/, '')
|
|
208
|
+
.replace(/\?.*$/, '') // Remove query params which we will handle later
|
|
209
|
+
.replace(/%[0-9a-f]{2}/gi, (m) => m.toUpperCase()); // Normalize percent-encoding as hex digits are case-insensitive
|
|
187
210
|
|
|
188
211
|
// Make sure there is a trailing slash
|
|
189
212
|
remaining = remaining.endsWith('/') ? remaining : `${remaining}/`;
|
|
190
213
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (prefix) {
|
|
194
|
-
// Make sure there is a trailing slash
|
|
195
|
-
const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
|
214
|
+
if (prefixRegex) {
|
|
215
|
+
const prefixMatch = remaining.match(prefixRegex);
|
|
196
216
|
|
|
197
|
-
|
|
198
|
-
if (!remaining.startsWith(normalizedPrefix)) {
|
|
217
|
+
if (prefixMatch == null) {
|
|
199
218
|
return undefined;
|
|
200
219
|
}
|
|
201
220
|
|
|
202
|
-
|
|
203
|
-
remaining = remaining.replace(normalizedPrefix, '');
|
|
221
|
+
remaining = remaining.slice(prefixMatch[0].length);
|
|
204
222
|
}
|
|
205
223
|
|
|
206
224
|
if (screens === undefined) {
|
|
@@ -358,6 +376,19 @@ function getConfigResources<ParamList extends {}>(
|
|
|
358
376
|
function prepareConfigResources(options?: Options<{}>) {
|
|
359
377
|
const initialRoutes = getInitialRoutes(options);
|
|
360
378
|
const configs = getSortedNormalizedConfigs(initialRoutes, options?.screens);
|
|
379
|
+
const prefix = options?.path?.replace(/^\//, '');
|
|
380
|
+
|
|
381
|
+
let prefixRegex: RegExp | undefined;
|
|
382
|
+
|
|
383
|
+
if (prefix) {
|
|
384
|
+
const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
|
385
|
+
const prefixPattern = normalizedPrefix
|
|
386
|
+
.split('/')
|
|
387
|
+
.map(getStaticSegmentPattern)
|
|
388
|
+
.join('/');
|
|
389
|
+
|
|
390
|
+
prefixRegex = new RegExp(`^${prefixPattern}`);
|
|
391
|
+
}
|
|
361
392
|
|
|
362
393
|
const configsByScreen: Record<string, RouteConfig[]> = {};
|
|
363
394
|
const configsByPattern: Record<string, RouteConfig[]> = {};
|
|
@@ -380,6 +411,7 @@ function prepareConfigResources(options?: Options<{}>) {
|
|
|
380
411
|
configs,
|
|
381
412
|
configsByScreen,
|
|
382
413
|
configsByPattern,
|
|
414
|
+
prefixRegex,
|
|
383
415
|
};
|
|
384
416
|
}
|
|
385
417
|
|
|
@@ -428,7 +460,21 @@ function getSortedNormalizedConfigs(
|
|
|
428
460
|
// If 2 patterns are same, move the one with less route names up
|
|
429
461
|
// This is an error state, so it's only useful for consistent error messages
|
|
430
462
|
if (isArrayEqual(a.segments, b.segments)) {
|
|
431
|
-
|
|
463
|
+
if (
|
|
464
|
+
a.routeNames.length > b.routeNames.length &&
|
|
465
|
+
arrayStartsWith(a.routeNames, b.routeNames)
|
|
466
|
+
) {
|
|
467
|
+
return -1;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (
|
|
471
|
+
b.routeNames.length > a.routeNames.length &&
|
|
472
|
+
arrayStartsWith(b.routeNames, a.routeNames)
|
|
473
|
+
) {
|
|
474
|
+
return 1;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return a.routeNames.length - b.routeNames.length || a.order - b.order;
|
|
432
478
|
}
|
|
433
479
|
|
|
434
480
|
// If one of the patterns starts with the other, it's more exhaustive
|
|
@@ -585,6 +631,12 @@ const matchAgainstConfig = (
|
|
|
585
631
|
return undefined;
|
|
586
632
|
}
|
|
587
633
|
|
|
634
|
+
// Percent-encoded values are matched permissively
|
|
635
|
+
// So validate the decoded value against the custom regex
|
|
636
|
+
if (param.regex && value !== decoded && !param.regex.test(decoded)) {
|
|
637
|
+
return undefined;
|
|
638
|
+
}
|
|
639
|
+
|
|
588
640
|
const parser = routeConfig.parse?.[param.name];
|
|
589
641
|
|
|
590
642
|
if (!parser) {
|
|
@@ -799,20 +851,27 @@ const createConfigItem = (
|
|
|
799
851
|
segments.push(part.segment);
|
|
800
852
|
|
|
801
853
|
if (part.param) {
|
|
802
|
-
|
|
854
|
+
// A custom regex may not match the percent-encoded form of the value
|
|
855
|
+
// So also accept segments containing an encoded character and validate them after decoding
|
|
856
|
+
const reg = part.regex
|
|
857
|
+
? `(?:${part.regex})|(?=[^/]*%[0-9A-F]{2})[^/]+`
|
|
858
|
+
: '[^/]+';
|
|
803
859
|
|
|
804
860
|
regexString += `(((?<${PARAM_GROUP_PREFIX}${index}>${reg})\\/)${part.optional ? '?' : ''})`;
|
|
805
861
|
params.push({
|
|
806
862
|
index,
|
|
807
863
|
screen: part.screen,
|
|
808
864
|
name: part.param,
|
|
865
|
+
regex: part.regex ? new RegExp(`^(?:${part.regex})$`) : undefined,
|
|
809
866
|
});
|
|
810
867
|
|
|
811
868
|
if (part.screen === screen) {
|
|
812
869
|
pathParamNames.add(part.param);
|
|
813
870
|
}
|
|
871
|
+
} else if (part.segment === '*') {
|
|
872
|
+
regexString += `.*\\/`;
|
|
814
873
|
} else {
|
|
815
|
-
regexString += `${
|
|
874
|
+
regexString += `${getStaticSegmentPattern(part.segment)}\\/`;
|
|
816
875
|
}
|
|
817
876
|
|
|
818
877
|
index++;
|
|
@@ -1083,7 +1142,8 @@ const parseQueryParams = (
|
|
|
1083
1142
|
):
|
|
1084
1143
|
| { valid: true; params?: Record<string, unknown> | undefined }
|
|
1085
1144
|
| { valid: false } => {
|
|
1086
|
-
const
|
|
1145
|
+
const queryIndex = path.indexOf('?');
|
|
1146
|
+
const query = queryIndex === -1 ? undefined : path.slice(queryIndex + 1);
|
|
1087
1147
|
const params: Record<string, unknown> = query ? queryString.parse(query) : {};
|
|
1088
1148
|
|
|
1089
1149
|
// Path params should always win over same-named query params.
|
package/src/index.tsx
CHANGED
|
@@ -11,6 +11,10 @@ export { getFocusedRouteNameFromRoute } from './getFocusedRouteNameFromRoute';
|
|
|
11
11
|
export { getPathFromState } from './getPathFromState';
|
|
12
12
|
export { getStateFromPath } from './getStateFromPath';
|
|
13
13
|
export { NavigationContainerRefContext } from './NavigationContainerRefContext';
|
|
14
|
+
export {
|
|
15
|
+
type FocusedRouteState,
|
|
16
|
+
NavigationFocusedRouteStateContext,
|
|
17
|
+
} from './NavigationFocusedRouteStateContext';
|
|
14
18
|
export { NavigationHelpersContext } from './NavigationHelpersContext';
|
|
15
19
|
export { NavigationIndependentTree } from './NavigationIndependentTree';
|
|
16
20
|
export { NavigationMetaContext } from './NavigationMetaContext';
|
|
@@ -61,6 +65,7 @@ export type {
|
|
|
61
65
|
NavigatorTypeBagBase,
|
|
62
66
|
NavigatorTypeBagFor,
|
|
63
67
|
NestedNavigatorsForParamList,
|
|
68
|
+
ParamListForNavigator,
|
|
64
69
|
PathConfig,
|
|
65
70
|
PathConfigMap,
|
|
66
71
|
RootNavigator,
|
package/src/types.tsx
CHANGED
|
@@ -298,7 +298,7 @@ type NavigatorLike =
|
|
|
298
298
|
getComponent: () => React.ComponentType<any>;
|
|
299
299
|
};
|
|
300
300
|
|
|
301
|
-
type ParamListForNavigator<T extends {}> = T extends NavigatorLike
|
|
301
|
+
export type ParamListForNavigator<T extends {}> = T extends NavigatorLike
|
|
302
302
|
? T extends PrivateValueStore<[infer ParamList extends {}, any, any, any]>
|
|
303
303
|
? ParamList
|
|
304
304
|
: T
|
|
@@ -1654,7 +1654,7 @@ export type PathConfig<Params> = FlatType<
|
|
|
1654
1654
|
path?: string | undefined;
|
|
1655
1655
|
/**
|
|
1656
1656
|
* An object mapping the param name to a function which converts the param value to a string.
|
|
1657
|
-
* By default,
|
|
1657
|
+
* By default, arrays and null are preserved for query params, and other values are converted using `String(value)`.
|
|
1658
1658
|
* Keys are constrained to valid param names when Params type is provided.
|
|
1659
1659
|
*
|
|
1660
1660
|
* @example
|
package/src/useDescriptors.tsx
CHANGED
package/src/useFocusEvents.tsx
CHANGED
|
@@ -51,7 +51,12 @@ export function useFocusEvents<State extends NavigationState>({
|
|
|
51
51
|
React.useEffect(() => {
|
|
52
52
|
const lastFocusedKey = lastFocusedKeyRef.current;
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
const isFocused = navigation ? navigation.isFocused() : true;
|
|
55
|
+
|
|
56
|
+
if (isFocused) {
|
|
57
|
+
// Remember the current route as focused if the navigator is focused
|
|
58
|
+
lastFocusedKeyRef.current = currentFocusedKey;
|
|
59
|
+
}
|
|
55
60
|
|
|
56
61
|
// We wouldn't have `lastFocusedKey` on initial mount
|
|
57
62
|
// Fire focus event for the current route on mount if there's no parent navigator
|
|
@@ -61,10 +66,7 @@ export function useFocusEvents<State extends NavigationState>({
|
|
|
61
66
|
|
|
62
67
|
// We should only emit events when the focused key changed and navigator is focused
|
|
63
68
|
// When navigator is not focused, screens inside shouldn't receive focused status either
|
|
64
|
-
if (
|
|
65
|
-
lastFocusedKey === currentFocusedKey ||
|
|
66
|
-
!(navigation ? navigation.isFocused() : true)
|
|
67
|
-
) {
|
|
69
|
+
if (lastFocusedKey === currentFocusedKey || !isFocused) {
|
|
68
70
|
return;
|
|
69
71
|
}
|
|
70
72
|
|
package/src/useLazyValue.tsx
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
|
|
3
|
-
export function useLazyValue<T>(create: () => T) {
|
|
3
|
+
export function useLazyValue<T>(create: () => T): T {
|
|
4
4
|
const lazyRef = React.useRef<T>(undefined);
|
|
5
5
|
|
|
6
6
|
if (lazyRef.current === undefined) {
|
|
7
7
|
lazyRef.current = create();
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
return lazyRef.current
|
|
10
|
+
return lazyRef.current;
|
|
11
11
|
}
|