@idealyst/navigation 1.0.96 → 1.0.98
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 +5 -5
- package/src/context/NavigatorContext.native.tsx +178 -20
- package/src/context/NavigatorContext.web.tsx +201 -22
- package/src/context/types.ts +5 -0
- package/src/examples/ExampleNavigationRouter.tsx +145 -2
- package/src/routing/router.native.tsx +41 -3
- package/src/routing/router.web.tsx +113 -9
- package/src/routing/types.ts +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/navigation",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.98",
|
|
4
4
|
"description": "Cross-platform navigation library for React and React Native",
|
|
5
5
|
"readme": "README.md",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"publish:npm": "npm publish"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@idealyst/components": "^1.0.
|
|
47
|
-
"@idealyst/theme": "^1.0.
|
|
46
|
+
"@idealyst/components": "^1.0.98",
|
|
47
|
+
"@idealyst/theme": "^1.0.98",
|
|
48
48
|
"@react-navigation/bottom-tabs": ">=7.0.0",
|
|
49
49
|
"@react-navigation/drawer": ">=7.0.0",
|
|
50
50
|
"@react-navigation/native": ">=7.0.0",
|
|
@@ -60,10 +60,10 @@
|
|
|
60
60
|
"react-router-dom": ">=6.0.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@idealyst/components": "^1.0.
|
|
63
|
+
"@idealyst/components": "^1.0.98",
|
|
64
64
|
"@idealyst/datagrid": "^1.0.93",
|
|
65
65
|
"@idealyst/datepicker": "^1.0.93",
|
|
66
|
-
"@idealyst/theme": "^1.0.
|
|
66
|
+
"@idealyst/theme": "^1.0.98",
|
|
67
67
|
"@types/react": "^19.1.8",
|
|
68
68
|
"@types/react-dom": "^19.1.6",
|
|
69
69
|
"react": "^19.1.0",
|
|
@@ -1,18 +1,93 @@
|
|
|
1
|
-
import React, { createContext, memo,
|
|
1
|
+
import React, { createContext, memo, useContext, useMemo } from 'react';
|
|
2
2
|
import { NavigateParams, NavigatorProviderProps, NavigatorContextValue } from './types';
|
|
3
|
-
import { useNavigation,
|
|
4
|
-
import { buildNavigator } from '../routing';
|
|
3
|
+
import { useNavigation, DarkTheme, DefaultTheme, NavigationContainer, CommonActions } from '@react-navigation/native';
|
|
4
|
+
import { buildNavigator, NavigatorParam, NOT_FOUND_SCREEN_NAME } from '../routing';
|
|
5
5
|
import { useUnistyles } from 'react-native-unistyles';
|
|
6
6
|
|
|
7
|
-
const NavigatorContext = createContext<NavigatorContextValue>(
|
|
8
|
-
route: undefined,
|
|
9
|
-
navigate: () => {},
|
|
10
|
-
});
|
|
7
|
+
const NavigatorContext = createContext<NavigatorContextValue>(null!);
|
|
11
8
|
|
|
12
|
-
const DrawerNavigatorContext = createContext<NavigatorContextValue>(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
const DrawerNavigatorContext = createContext<NavigatorContextValue>(null!);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Find the nearest invalid route handler by matching path prefixes and bubbling up.
|
|
13
|
+
* Returns the handler from the deepest matching navigator, or undefined if none found.
|
|
14
|
+
*/
|
|
15
|
+
function findInvalidRouteHandler(
|
|
16
|
+
route: NavigatorParam,
|
|
17
|
+
invalidPath: string
|
|
18
|
+
): ((path: string) => NavigateParams | undefined) | undefined {
|
|
19
|
+
const pathSegments = invalidPath.split('/').filter(Boolean);
|
|
20
|
+
|
|
21
|
+
// Recursively search for handlers, collecting them from root to deepest match
|
|
22
|
+
function collectHandlers(
|
|
23
|
+
currentRoute: NavigatorParam,
|
|
24
|
+
segmentIndex: number,
|
|
25
|
+
prefix: string
|
|
26
|
+
): Array<(path: string) => NavigateParams | undefined> {
|
|
27
|
+
const handlers: Array<(path: string) => NavigateParams | undefined> = [];
|
|
28
|
+
|
|
29
|
+
// Add this navigator's handler if it exists
|
|
30
|
+
if (currentRoute.onInvalidRoute) {
|
|
31
|
+
handlers.push(currentRoute.onInvalidRoute);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if any child route matches the next segments
|
|
35
|
+
if (currentRoute.routes && segmentIndex < pathSegments.length) {
|
|
36
|
+
for (const childRoute of currentRoute.routes) {
|
|
37
|
+
if (childRoute.type !== 'navigator') continue;
|
|
38
|
+
|
|
39
|
+
const childSegments = childRoute.path.split('/').filter(Boolean);
|
|
40
|
+
|
|
41
|
+
// Check if child path matches the next path segments
|
|
42
|
+
let matches = true;
|
|
43
|
+
for (let i = 0; i < childSegments.length && segmentIndex + i < pathSegments.length; i++) {
|
|
44
|
+
const childSeg = childSegments[i];
|
|
45
|
+
const pathSeg = pathSegments[segmentIndex + i];
|
|
46
|
+
|
|
47
|
+
if (!childSeg.startsWith(':') && childSeg !== pathSeg) {
|
|
48
|
+
matches = false;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (matches && childSegments.length > 0) {
|
|
54
|
+
// Recurse into matching child navigator
|
|
55
|
+
const childPrefix = `${prefix}/${childRoute.path}`.replace(/\/+/g, '/');
|
|
56
|
+
const childHandlers = collectHandlers(
|
|
57
|
+
childRoute as NavigatorParam,
|
|
58
|
+
segmentIndex + childSegments.length,
|
|
59
|
+
childPrefix
|
|
60
|
+
);
|
|
61
|
+
handlers.push(...childHandlers);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return handlers;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const handlers = collectHandlers(route, 0, '');
|
|
70
|
+
|
|
71
|
+
// Return the deepest handler (last in the array)
|
|
72
|
+
return handlers.length > 0 ? handlers[handlers.length - 1] : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if any navigator in the tree has a notFoundComponent configured
|
|
77
|
+
*/
|
|
78
|
+
function hasNotFoundComponent(route: NavigatorParam): boolean {
|
|
79
|
+
if (route.notFoundComponent) return true;
|
|
80
|
+
|
|
81
|
+
if (route.routes) {
|
|
82
|
+
for (const child of route.routes) {
|
|
83
|
+
if (child.type === 'navigator' && hasNotFoundComponent(child as NavigatorParam)) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
16
91
|
|
|
17
92
|
// Utility function to parse path with parameters and find matching route
|
|
18
93
|
const parseParameterizedPath = (path: string, rootRoute: any): { routeName: string, params: Record<string, string> } | null => {
|
|
@@ -84,15 +159,56 @@ const UnwrappedNavigatorProvider = ({ route }: NavigatorProviderProps) => {
|
|
|
84
159
|
|
|
85
160
|
const navigation = useNavigation();
|
|
86
161
|
|
|
87
|
-
const navigate = (params: NavigateParams) => {
|
|
162
|
+
const navigate = (params: NavigateParams, _redirectCount = 0) => {
|
|
163
|
+
// Prevent infinite redirect loops
|
|
164
|
+
if (_redirectCount > 10) {
|
|
165
|
+
console.error('Navigation: Maximum redirect count exceeded. Check onInvalidRoute handlers.');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
88
169
|
// Parse parameterized path for mobile
|
|
89
170
|
const parsed = parseParameterizedPath(params.path, route);
|
|
90
|
-
|
|
171
|
+
|
|
172
|
+
if (!parsed) {
|
|
173
|
+
// Invalid route - try to find a handler
|
|
174
|
+
const handler = findInvalidRouteHandler(route, params.path);
|
|
175
|
+
if (handler) {
|
|
176
|
+
const redirectParams = handler(params.path);
|
|
177
|
+
if (redirectParams) {
|
|
178
|
+
// Handler returned NavigateParams - redirect
|
|
179
|
+
return navigate(
|
|
180
|
+
{ ...redirectParams, replace: true },
|
|
181
|
+
_redirectCount + 1
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
// Handler returned undefined - fall through to 404 screen
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Navigate to 404 screen if configured
|
|
188
|
+
if (route.notFoundComponent) {
|
|
189
|
+
navigation.navigate(NOT_FOUND_SCREEN_NAME as never, {
|
|
190
|
+
path: params.path,
|
|
191
|
+
params: params.vars
|
|
192
|
+
} as never);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// No handler and no 404 screen - log warning
|
|
197
|
+
console.warn(`Navigation: Invalid route "${params.path}" and no handler or notFoundComponent configured.`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (params.replace) {
|
|
202
|
+
// Use CommonActions.reset to replace the current route
|
|
203
|
+
navigation.dispatch(
|
|
204
|
+
CommonActions.reset({
|
|
205
|
+
index: 0,
|
|
206
|
+
routes: [{ name: parsed.routeName, params: parsed.params }],
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
} else {
|
|
91
210
|
// Navigate to the pattern route with extracted parameters
|
|
92
211
|
navigation.navigate(parsed.routeName as never, parsed.params as never);
|
|
93
|
-
} else {
|
|
94
|
-
// Fallback to direct navigation
|
|
95
|
-
navigation.navigate(params.path as never, params.vars as never);
|
|
96
212
|
}
|
|
97
213
|
};
|
|
98
214
|
|
|
@@ -125,14 +241,56 @@ const NavigatorProvider = ({ route }: NavigatorProviderProps) => {
|
|
|
125
241
|
|
|
126
242
|
const DrawerNavigatorProvider = ({ navigation, route, children }: { navigation: any, route: any, children: React.ReactNode }) => {
|
|
127
243
|
|
|
128
|
-
const navigate = (params: NavigateParams) => {
|
|
244
|
+
const navigate = (params: NavigateParams, _redirectCount = 0) => {
|
|
245
|
+
// Prevent infinite redirect loops
|
|
246
|
+
if (_redirectCount > 10) {
|
|
247
|
+
console.error('Navigation: Maximum redirect count exceeded. Check onInvalidRoute handlers.');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
129
251
|
// Parse parameterized path for mobile
|
|
130
252
|
const parsed = parseParameterizedPath(params.path, route);
|
|
131
|
-
|
|
253
|
+
|
|
254
|
+
if (!parsed) {
|
|
255
|
+
// Invalid route - try to find a handler
|
|
256
|
+
const handler = findInvalidRouteHandler(route, params.path);
|
|
257
|
+
if (handler) {
|
|
258
|
+
const redirectParams = handler(params.path);
|
|
259
|
+
if (redirectParams) {
|
|
260
|
+
// Handler returned NavigateParams - redirect
|
|
261
|
+
return navigate(
|
|
262
|
+
{ ...redirectParams, replace: true },
|
|
263
|
+
_redirectCount + 1
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
// Handler returned undefined - fall through to 404 screen
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Navigate to 404 screen if configured
|
|
270
|
+
if (route.notFoundComponent) {
|
|
271
|
+
navigation.navigate(NOT_FOUND_SCREEN_NAME as never, {
|
|
272
|
+
path: params.path,
|
|
273
|
+
params: params.vars
|
|
274
|
+
} as never);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// No handler and no 404 screen - log warning
|
|
279
|
+
console.warn(`Navigation: Invalid route "${params.path}" and no handler or notFoundComponent configured.`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (params.replace) {
|
|
284
|
+
// Use CommonActions.reset to replace the current route
|
|
285
|
+
navigation.dispatch(
|
|
286
|
+
CommonActions.reset({
|
|
287
|
+
index: 0,
|
|
288
|
+
routes: [{ name: parsed.routeName, params: parsed.params }],
|
|
289
|
+
})
|
|
290
|
+
);
|
|
291
|
+
} else {
|
|
132
292
|
// Navigate to the pattern route with extracted parameters
|
|
133
293
|
navigation.navigate(parsed.routeName as never, parsed.params as never);
|
|
134
|
-
} else {
|
|
135
|
-
// Fallback to direct navigation
|
|
136
294
|
}
|
|
137
295
|
};
|
|
138
296
|
|
|
@@ -1,45 +1,224 @@
|
|
|
1
1
|
import React, { createContext, memo, useContext, useMemo } from 'react';
|
|
2
2
|
import { useNavigate, useParams } from '../router';
|
|
3
3
|
import { NavigateParams, NavigatorProviderProps, NavigatorContextValue } from './types';
|
|
4
|
-
import { buildNavigator } from '../routing';
|
|
4
|
+
import { buildNavigator, NavigatorParam } from '../routing';
|
|
5
5
|
|
|
6
6
|
const NavigatorContext = createContext<NavigatorContextValue>({
|
|
7
7
|
navigate: () => {},
|
|
8
8
|
route: undefined,
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Normalize a path and substitute variables
|
|
13
|
+
*/
|
|
14
|
+
function normalizePath(path: string, vars?: Record<string, string>): string {
|
|
15
|
+
let normalizedPath = path;
|
|
16
|
+
|
|
17
|
+
// Convert empty string to '/'
|
|
18
|
+
if (normalizedPath === '' || normalizedPath === '/') {
|
|
19
|
+
normalizedPath = '/';
|
|
20
|
+
} else if (!normalizedPath.startsWith('/')) {
|
|
21
|
+
normalizedPath = `/${normalizedPath}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Substitute variables in the path if provided
|
|
25
|
+
if (vars) {
|
|
26
|
+
Object.entries(vars).forEach(([key, value]) => {
|
|
27
|
+
normalizedPath = normalizedPath.replace(`:${key}`, value);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return normalizedPath;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build a list of valid route patterns from the route tree
|
|
36
|
+
*/
|
|
37
|
+
function buildValidPatterns(route: NavigatorParam, prefix = ''): string[] {
|
|
38
|
+
const patterns: string[] = [];
|
|
39
|
+
|
|
40
|
+
if (!route.routes) return patterns;
|
|
41
|
+
|
|
42
|
+
for (const childRoute of route.routes) {
|
|
43
|
+
const childPath = childRoute.path.startsWith('/')
|
|
44
|
+
? childRoute.path
|
|
45
|
+
: `${prefix}/${childRoute.path}`.replace(/\/+/g, '/');
|
|
46
|
+
|
|
47
|
+
// Add the pattern (keeping :param placeholders)
|
|
48
|
+
if (childPath) {
|
|
49
|
+
patterns.push(childPath === '' ? '/' : childPath);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Recursively add nested routes
|
|
53
|
+
if (childRoute.type === 'navigator') {
|
|
54
|
+
patterns.push(...buildValidPatterns(childRoute as NavigatorParam, childPath));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Also add the root pattern
|
|
59
|
+
if (prefix === '' || prefix === '/') {
|
|
60
|
+
patterns.push('/');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return patterns;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if a path matches any of the valid route patterns
|
|
68
|
+
*/
|
|
69
|
+
function isValidRoute(path: string, patterns: string[]): boolean {
|
|
70
|
+
const pathSegments = path.split('/').filter(Boolean);
|
|
71
|
+
|
|
72
|
+
for (const pattern of patterns) {
|
|
73
|
+
const patternSegments = pattern.split('/').filter(Boolean);
|
|
74
|
+
|
|
75
|
+
// Check for root path match
|
|
76
|
+
if (path === '/' && (pattern === '/' || pattern === '')) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Length must match
|
|
81
|
+
if (pathSegments.length !== patternSegments.length) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let isMatch = true;
|
|
86
|
+
for (let i = 0; i < patternSegments.length; i++) {
|
|
87
|
+
const patternSeg = patternSegments[i];
|
|
88
|
+
const pathSeg = pathSegments[i];
|
|
89
|
+
|
|
90
|
+
// Parameter segments match anything
|
|
91
|
+
if (patternSeg.startsWith(':')) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Literal segments must match exactly
|
|
96
|
+
if (patternSeg !== pathSeg) {
|
|
97
|
+
isMatch = false;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (isMatch) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Find the nearest invalid route handler by matching path prefixes and bubbling up.
|
|
112
|
+
* Returns the handler from the deepest matching navigator, or undefined if none found.
|
|
113
|
+
*/
|
|
114
|
+
function findInvalidRouteHandler(
|
|
115
|
+
route: NavigatorParam,
|
|
116
|
+
invalidPath: string
|
|
117
|
+
): ((path: string) => NavigateParams | undefined) | undefined {
|
|
118
|
+
const pathSegments = invalidPath.split('/').filter(Boolean);
|
|
119
|
+
|
|
120
|
+
// Recursively search for handlers, collecting them from root to deepest match
|
|
121
|
+
function collectHandlers(
|
|
122
|
+
currentRoute: NavigatorParam,
|
|
123
|
+
segmentIndex: number,
|
|
124
|
+
prefix: string
|
|
125
|
+
): Array<(path: string) => NavigateParams | undefined> {
|
|
126
|
+
const handlers: Array<(path: string) => NavigateParams | undefined> = [];
|
|
127
|
+
|
|
128
|
+
// Add this navigator's handler if it exists
|
|
129
|
+
if (currentRoute.onInvalidRoute) {
|
|
130
|
+
handlers.push(currentRoute.onInvalidRoute);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check if any child route matches the next segments
|
|
134
|
+
if (currentRoute.routes && segmentIndex < pathSegments.length) {
|
|
135
|
+
for (const childRoute of currentRoute.routes) {
|
|
136
|
+
if (childRoute.type !== 'navigator') continue;
|
|
137
|
+
|
|
138
|
+
const childSegments = childRoute.path.split('/').filter(Boolean);
|
|
139
|
+
|
|
140
|
+
// Check if child path matches the next path segments
|
|
141
|
+
let matches = true;
|
|
142
|
+
for (let i = 0; i < childSegments.length && segmentIndex + i < pathSegments.length; i++) {
|
|
143
|
+
const childSeg = childSegments[i];
|
|
144
|
+
const pathSeg = pathSegments[segmentIndex + i];
|
|
145
|
+
|
|
146
|
+
if (!childSeg.startsWith(':') && childSeg !== pathSeg) {
|
|
147
|
+
matches = false;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (matches && childSegments.length > 0) {
|
|
153
|
+
// Recurse into matching child navigator
|
|
154
|
+
const childPrefix = `${prefix}/${childRoute.path}`.replace(/\/+/g, '/');
|
|
155
|
+
const childHandlers = collectHandlers(
|
|
156
|
+
childRoute as NavigatorParam,
|
|
157
|
+
segmentIndex + childSegments.length,
|
|
158
|
+
childPrefix
|
|
159
|
+
);
|
|
160
|
+
handlers.push(...childHandlers);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return handlers;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const handlers = collectHandlers(route, 0, '');
|
|
169
|
+
|
|
170
|
+
// Return the deepest handler (last in the array)
|
|
171
|
+
return handlers.length > 0 ? handlers[handlers.length - 1] : undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
11
174
|
export const NavigatorProvider = ({
|
|
12
175
|
route,
|
|
13
176
|
}: NavigatorProviderProps) => {
|
|
14
|
-
const reactRouterNavigate = useNavigate()
|
|
15
|
-
|
|
16
|
-
|
|
177
|
+
const reactRouterNavigate = useNavigate();
|
|
178
|
+
|
|
179
|
+
// Memoize the list of valid route patterns
|
|
180
|
+
const validPatterns = useMemo(() => buildValidPatterns(route), [route]);
|
|
181
|
+
|
|
182
|
+
const navigateFunction = (params: NavigateParams, _redirectCount = 0) => {
|
|
183
|
+
// Prevent infinite redirect loops
|
|
184
|
+
if (_redirectCount > 10) {
|
|
185
|
+
console.error('Navigation: Maximum redirect count exceeded. Check onInvalidRoute handlers.');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
17
189
|
if (params.path) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
190
|
+
const path = normalizePath(params.path, params.vars);
|
|
191
|
+
|
|
192
|
+
// Check if route is valid
|
|
193
|
+
if (!isValidRoute(path, validPatterns)) {
|
|
194
|
+
// Try to find a handler for the invalid route
|
|
195
|
+
const handler = findInvalidRouteHandler(route, path);
|
|
196
|
+
if (handler) {
|
|
197
|
+
const redirectParams = handler(path);
|
|
198
|
+
if (redirectParams) {
|
|
199
|
+
// Handler returned NavigateParams - redirect
|
|
200
|
+
return navigateFunction(
|
|
201
|
+
{ ...redirectParams, replace: true },
|
|
202
|
+
_redirectCount + 1
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
// Handler returned undefined - let React Router show 404 via catch-all
|
|
206
|
+
}
|
|
207
|
+
// No handler or handler returned undefined
|
|
208
|
+
// Navigate anyway - React Router's catch-all will show notFoundComponent
|
|
209
|
+
// If no notFoundComponent configured, React Router will render nothing
|
|
31
210
|
}
|
|
32
|
-
|
|
33
|
-
// Use React Router's navigate function
|
|
34
|
-
reactRouterNavigate(path);
|
|
211
|
+
|
|
212
|
+
// Use React Router's navigate function with replace option
|
|
213
|
+
reactRouterNavigate(path, { replace: params.replace });
|
|
35
214
|
}
|
|
36
215
|
};
|
|
37
|
-
|
|
216
|
+
|
|
38
217
|
const RouteComponent = useMemo(() => {
|
|
39
218
|
// Memoize the router to prevent unnecessary re-renders
|
|
40
219
|
return memo(buildNavigator(route));
|
|
41
220
|
}, [route]);
|
|
42
|
-
|
|
221
|
+
|
|
43
222
|
return (
|
|
44
223
|
<NavigatorContext.Provider value={{
|
|
45
224
|
route,
|
package/src/context/types.ts
CHANGED
|
@@ -6,6 +6,11 @@ import { NavigatorParam } from "../routing";
|
|
|
6
6
|
export type NavigateParams = {
|
|
7
7
|
path: string;
|
|
8
8
|
vars?: Record<string, string>;
|
|
9
|
+
/**
|
|
10
|
+
* If true, replaces the current history entry instead of pushing a new one.
|
|
11
|
+
* On web, this uses history.replace(). On native, this resets the navigation state.
|
|
12
|
+
*/
|
|
13
|
+
replace?: boolean;
|
|
9
14
|
};
|
|
10
15
|
|
|
11
16
|
export type NavigatorProviderProps = {
|
|
@@ -2,11 +2,151 @@ import React from 'react';
|
|
|
2
2
|
import { AvatarExamples, BadgeExamples, ButtonExamples, CardExamples, CheckboxExamples, DialogExamples, DividerExamples, IconExamples, InputExamples, LinkExamples, PopoverExamples, ScreenExamples, SelectExamples, SliderExamples, SVGImageExamples, TextExamples, ViewExamples, ThemeExtensionExamples, SwitchExamples, RadioButtonExamples, ProgressExamples, TextAreaExamples, TabBarExamples, TooltipExamples, AccordionExamples, ListExamples, TableExamples, MenuExamples, ImageExamples, VideoExamples, AlertExamples, SkeletonExamples, ChipExamples, BreadcrumbExamples } from '@idealyst/components/examples';
|
|
3
3
|
import { DataGridShowcase } from '@idealyst/datagrid/examples';
|
|
4
4
|
import { DatePickerExamples } from '@idealyst/datepicker/examples';
|
|
5
|
-
import { Text, View, Card, Screen } from '@idealyst/components';
|
|
6
|
-
import { NavigatorParam, RouteParam } from '../routing';
|
|
5
|
+
import { Text, View, Card, Screen, Icon, Button } from '@idealyst/components';
|
|
6
|
+
import { NavigatorParam, RouteParam, NotFoundComponentProps } from '../routing';
|
|
7
7
|
import { ExampleWebLayout } from './ExampleWebLayout';
|
|
8
8
|
import ExampleSidebar from './ExampleSidebar';
|
|
9
9
|
import HeaderRight from './HeaderRight';
|
|
10
|
+
import { useNavigator } from '../context';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Global 404 Not Found screen - shown for invalid routes at the root level
|
|
14
|
+
*/
|
|
15
|
+
const NotFoundScreen = ({ path, params }: NotFoundComponentProps) => {
|
|
16
|
+
const { navigate } = useNavigator();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Screen>
|
|
20
|
+
<View spacing='lg' padding={12} style={{ alignItems: 'center', justifyContent: 'center', flex: 1 }}>
|
|
21
|
+
<Icon name="alert-circle-outline" size={64} color="red" />
|
|
22
|
+
<Text size="xl" weight="bold">
|
|
23
|
+
Page Not Found
|
|
24
|
+
</Text>
|
|
25
|
+
<Text size="md" color="secondary">
|
|
26
|
+
The page you're looking for doesn't exist.
|
|
27
|
+
</Text>
|
|
28
|
+
<Card style={{ marginTop: 16, padding: 16 }}>
|
|
29
|
+
<View spacing="sm">
|
|
30
|
+
<Text size="sm" weight="semibold">Attempted path:</Text>
|
|
31
|
+
<Text size="sm" color="secondary">{path}</Text>
|
|
32
|
+
{params && Object.keys(params).length > 0 && (
|
|
33
|
+
<>
|
|
34
|
+
<Text size="sm" weight="semibold" style={{ marginTop: 8 }}>Params:</Text>
|
|
35
|
+
<Text size="sm" color="secondary">{JSON.stringify(params, null, 2)}</Text>
|
|
36
|
+
</>
|
|
37
|
+
)}
|
|
38
|
+
</View>
|
|
39
|
+
</Card>
|
|
40
|
+
<Button
|
|
41
|
+
style={{ marginTop: 24 }}
|
|
42
|
+
onPress={() => navigate({ path: '/', replace: true })}
|
|
43
|
+
>
|
|
44
|
+
Go Home
|
|
45
|
+
</Button>
|
|
46
|
+
</View>
|
|
47
|
+
</Screen>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Settings-specific 404 screen - a simpler, more minimal style
|
|
53
|
+
*/
|
|
54
|
+
const SettingsNotFoundScreen = ({ path, params }: NotFoundComponentProps) => {
|
|
55
|
+
const { navigate } = useNavigator();
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Screen>
|
|
59
|
+
<View spacing='md' padding={12} style={{ alignItems: 'center', justifyContent: 'center', flex: 1, backgroundColor: '#f5f5f5' }}>
|
|
60
|
+
<Icon name="cog-off-outline" size={48} color="orange" />
|
|
61
|
+
<Text size="lg" weight="semibold">
|
|
62
|
+
Settings Page Not Found
|
|
63
|
+
</Text>
|
|
64
|
+
<Text size="sm" color="secondary" style={{ textAlign: 'center', maxWidth: 300 }}>
|
|
65
|
+
The settings page "{path}" doesn't exist. You've been redirected here.
|
|
66
|
+
</Text>
|
|
67
|
+
<View direction="row" spacing="md" style={{ marginTop: 16 }}>
|
|
68
|
+
<Button
|
|
69
|
+
type="outlined"
|
|
70
|
+
size="sm"
|
|
71
|
+
onPress={() => navigate({ path: '/settings', replace: true })}
|
|
72
|
+
>
|
|
73
|
+
Settings Home
|
|
74
|
+
</Button>
|
|
75
|
+
<Button
|
|
76
|
+
size="sm"
|
|
77
|
+
onPress={() => navigate({ path: '/', replace: true })}
|
|
78
|
+
>
|
|
79
|
+
Go Home
|
|
80
|
+
</Button>
|
|
81
|
+
</View>
|
|
82
|
+
</View>
|
|
83
|
+
</Screen>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Settings section screens
|
|
88
|
+
const SettingsHomeScreen = () => (
|
|
89
|
+
<Screen>
|
|
90
|
+
<View spacing='lg' padding={12}>
|
|
91
|
+
<Text size="xl" weight="bold">Settings</Text>
|
|
92
|
+
<Text size="md" color="secondary">Manage your application settings</Text>
|
|
93
|
+
<Card style={{ marginTop: 16, padding: 16 }}>
|
|
94
|
+
<View spacing="sm">
|
|
95
|
+
<Text size="sm">Navigate to:</Text>
|
|
96
|
+
<Text size="sm" color="secondary">• /settings/general - General settings</Text>
|
|
97
|
+
<Text size="sm" color="secondary">• /settings/account - Account settings</Text>
|
|
98
|
+
<Text size="sm" color="secondary">• /settings/invalid - Test 404 (will show SettingsNotFoundScreen)</Text>
|
|
99
|
+
<Text size="sm" color="secondary">• /settings/redirect-me - Test redirect handler</Text>
|
|
100
|
+
</View>
|
|
101
|
+
</Card>
|
|
102
|
+
</View>
|
|
103
|
+
</Screen>
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const GeneralSettingsScreen = () => (
|
|
107
|
+
<Screen>
|
|
108
|
+
<View spacing='lg' padding={12}>
|
|
109
|
+
<Text size="xl" weight="bold">General Settings</Text>
|
|
110
|
+
<Text size="md" color="secondary">Configure general app preferences</Text>
|
|
111
|
+
</View>
|
|
112
|
+
</Screen>
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const AccountSettingsScreen = () => (
|
|
116
|
+
<Screen>
|
|
117
|
+
<View spacing='lg' padding={12}>
|
|
118
|
+
<Text size="xl" weight="bold">Account Settings</Text>
|
|
119
|
+
<Text size="md" color="secondary">Manage your account</Text>
|
|
120
|
+
</View>
|
|
121
|
+
</Screen>
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Nested Settings Navigator with its own error handling:
|
|
126
|
+
* - onInvalidRoute: Redirects /settings/redirect-* to /settings/general
|
|
127
|
+
* - notFoundComponent: Shows SettingsNotFoundScreen for other invalid routes
|
|
128
|
+
*/
|
|
129
|
+
const SettingsNavigator: NavigatorParam = {
|
|
130
|
+
path: "settings",
|
|
131
|
+
type: 'navigator',
|
|
132
|
+
layout: 'stack',
|
|
133
|
+
notFoundComponent: SettingsNotFoundScreen,
|
|
134
|
+
onInvalidRoute: (invalidPath) => {
|
|
135
|
+
// Example: Redirect old/deprecated paths to the general settings
|
|
136
|
+
if (invalidPath.includes('redirect')) {
|
|
137
|
+
console.log(`[Settings] Redirecting "${invalidPath}" to /settings/general`);
|
|
138
|
+
return { path: '/settings/general', replace: true };
|
|
139
|
+
}
|
|
140
|
+
// Return undefined to show the notFoundComponent instead
|
|
141
|
+
console.log(`[Settings] Showing 404 for "${invalidPath}"`);
|
|
142
|
+
return undefined;
|
|
143
|
+
},
|
|
144
|
+
routes: [
|
|
145
|
+
{ path: "", type: 'screen', component: SettingsHomeScreen, options: { title: "Settings" } },
|
|
146
|
+
{ path: "general", type: 'screen', component: GeneralSettingsScreen, options: { title: "General" } },
|
|
147
|
+
{ path: "account", type: 'screen', component: AccountSettingsScreen, options: { title: "Account" } },
|
|
148
|
+
],
|
|
149
|
+
};
|
|
10
150
|
|
|
11
151
|
const HomeScreen = () => {
|
|
12
152
|
return (
|
|
@@ -65,11 +205,14 @@ const ExampleNavigationRouter: NavigatorParam = {
|
|
|
65
205
|
layout: 'drawer',
|
|
66
206
|
sidebarComponent: ExampleSidebar,
|
|
67
207
|
layoutComponent: ExampleWebLayout,
|
|
208
|
+
notFoundComponent: NotFoundScreen,
|
|
68
209
|
options: {
|
|
69
210
|
headerRight: HeaderRight,
|
|
70
211
|
},
|
|
71
212
|
routes: [
|
|
72
213
|
{ path: "/", type: 'screen', component: HomeScreen, options: { title: "Home" } },
|
|
214
|
+
// Nested settings navigator with its own 404 handling
|
|
215
|
+
SettingsNavigator,
|
|
73
216
|
{ path: "avatar", type: 'screen', component: AvatarExamples, options: { title: "Avatar" } },
|
|
74
217
|
{ path: "badge", type: 'screen', component: BadgeExamples, options: { title: "Badge" } },
|
|
75
218
|
{ path: "button", type: 'screen', component: ButtonExamples, options: { title: "Button" } },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NavigatorParam, RouteParam, ScreenOptions } from './types'
|
|
1
|
+
import { NavigatorParam, RouteParam, ScreenOptions, NotFoundComponentProps } from './types'
|
|
2
2
|
|
|
3
3
|
import { TypedNavigator } from "@react-navigation/native";
|
|
4
4
|
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
|
@@ -75,6 +75,24 @@ const createThemeAwareComponent = (OriginalComponent: React.ComponentType<any>)
|
|
|
75
75
|
return Wrapped;
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Internal screen name for 404 pages
|
|
80
|
+
*/
|
|
81
|
+
export const NOT_FOUND_SCREEN_NAME = '__notFound__';
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Creates a NotFound screen component that receives path and params from route params
|
|
85
|
+
*/
|
|
86
|
+
const createNotFoundScreen = (NotFoundComponent: React.ComponentType<NotFoundComponentProps>) => {
|
|
87
|
+
return React.memo((props: any) => {
|
|
88
|
+
const { route } = props;
|
|
89
|
+
const path = route?.params?.path ?? '';
|
|
90
|
+
const params = route?.params?.params;
|
|
91
|
+
|
|
92
|
+
return <NotFoundComponent path={path} params={params} />;
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
78
96
|
/**
|
|
79
97
|
* Build the Mobile navigator using React Navigation
|
|
80
98
|
* @param params
|
|
@@ -104,6 +122,26 @@ export const buildNavigator = (params: NavigatorParam, parentPath = '') => {
|
|
|
104
122
|
}
|
|
105
123
|
: params.options;
|
|
106
124
|
|
|
125
|
+
// Build screens including optional 404 screen
|
|
126
|
+
const buildScreens = () => {
|
|
127
|
+
const screens = params.routes.map((child, index) => buildScreen(child, NavigatorType, parentPath, index));
|
|
128
|
+
|
|
129
|
+
// Add 404 screen if notFoundComponent is configured
|
|
130
|
+
if (params.notFoundComponent) {
|
|
131
|
+
const NotFoundScreen = createNotFoundScreen(params.notFoundComponent);
|
|
132
|
+
screens.push(
|
|
133
|
+
<NavigatorType.Screen
|
|
134
|
+
key={NOT_FOUND_SCREEN_NAME}
|
|
135
|
+
name={NOT_FOUND_SCREEN_NAME}
|
|
136
|
+
component={NotFoundScreen}
|
|
137
|
+
options={{ headerShown: false }}
|
|
138
|
+
/>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return screens;
|
|
143
|
+
};
|
|
144
|
+
|
|
107
145
|
// Special handling for drawer navigator with custom sidebar
|
|
108
146
|
if (params.layout === 'drawer' && params.sidebarComponent) {
|
|
109
147
|
return () => (
|
|
@@ -117,14 +155,14 @@ export const buildNavigator = (params: NavigatorParam, parentPath = '') => {
|
|
|
117
155
|
/>
|
|
118
156
|
)}
|
|
119
157
|
>
|
|
120
|
-
{
|
|
158
|
+
{buildScreens()}
|
|
121
159
|
</NavigatorType.Navigator>
|
|
122
160
|
);
|
|
123
161
|
}
|
|
124
162
|
|
|
125
163
|
return () => (
|
|
126
164
|
<NavigatorType.Navigator screenOptions={screenOptions}>
|
|
127
|
-
{
|
|
165
|
+
{buildScreens()}
|
|
128
166
|
</NavigatorType.Navigator>
|
|
129
167
|
)
|
|
130
168
|
}
|
|
@@ -1,8 +1,93 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { Routes, Route } from '../router'
|
|
1
|
+
import React, { useEffect, useState, useRef } from 'react'
|
|
2
|
+
import { Routes, Route, useLocation, useParams } from '../router'
|
|
3
3
|
import { DefaultStackLayout } from '../layouts/DefaultStackLayout'
|
|
4
4
|
import { DefaultTabLayout } from '../layouts/DefaultTabLayout'
|
|
5
|
-
import { NavigatorParam, RouteParam } from './types'
|
|
5
|
+
import { NavigatorParam, RouteParam, NotFoundComponentProps } from './types'
|
|
6
|
+
import { NavigateParams } from '../context/types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Wrapper component for catch-all routes that:
|
|
10
|
+
* 1. Checks onInvalidRoute handler first
|
|
11
|
+
* 2. If handler returns NavigateParams, redirects (using absolute path)
|
|
12
|
+
* 3. If handler returns undefined or no handler, renders notFoundComponent (if provided)
|
|
13
|
+
*/
|
|
14
|
+
const NotFoundWrapper = ({
|
|
15
|
+
component: Component,
|
|
16
|
+
onInvalidRoute
|
|
17
|
+
}: {
|
|
18
|
+
component?: React.ComponentType<NotFoundComponentProps>
|
|
19
|
+
onInvalidRoute?: (path: string) => NavigateParams | undefined
|
|
20
|
+
}) => {
|
|
21
|
+
const location = useLocation()
|
|
22
|
+
const params = useParams()
|
|
23
|
+
const [shouldRender, setShouldRender] = useState(false)
|
|
24
|
+
const redirectingRef = useRef(false)
|
|
25
|
+
const lastPathRef = useRef(location.pathname)
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
// Reset state if path changed (navigated to a different invalid route)
|
|
29
|
+
if (lastPathRef.current !== location.pathname) {
|
|
30
|
+
lastPathRef.current = location.pathname
|
|
31
|
+
redirectingRef.current = false
|
|
32
|
+
setShouldRender(false)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Prevent multiple redirects
|
|
36
|
+
if (redirectingRef.current) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if handler wants to redirect
|
|
41
|
+
if (onInvalidRoute) {
|
|
42
|
+
const redirectParams = onInvalidRoute(location.pathname)
|
|
43
|
+
if (redirectParams) {
|
|
44
|
+
// Mark as redirecting to prevent loops
|
|
45
|
+
redirectingRef.current = true
|
|
46
|
+
|
|
47
|
+
// Handler returned NavigateParams - redirect using absolute path
|
|
48
|
+
// Ensure path starts with / for absolute navigation
|
|
49
|
+
let targetPath = redirectParams.path
|
|
50
|
+
if (!targetPath.startsWith('/')) {
|
|
51
|
+
targetPath = `/${targetPath}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Substitute any vars in the path
|
|
55
|
+
if (redirectParams.vars) {
|
|
56
|
+
Object.entries(redirectParams.vars).forEach(([key, value]) => {
|
|
57
|
+
targetPath = targetPath.replace(`:${key}`, value)
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Use window.history for truly absolute navigation
|
|
62
|
+
// React Router's navigate can have issues in catch-all contexts
|
|
63
|
+
const replaceMode = redirectParams.replace ?? true
|
|
64
|
+
if (replaceMode) {
|
|
65
|
+
window.history.replaceState(null, '', targetPath)
|
|
66
|
+
} else {
|
|
67
|
+
window.history.pushState(null, '', targetPath)
|
|
68
|
+
}
|
|
69
|
+
// Trigger React Router to sync with the new URL
|
|
70
|
+
window.dispatchEvent(new PopStateEvent('popstate'))
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// No redirect - show the 404 component (if provided)
|
|
75
|
+
setShouldRender(true)
|
|
76
|
+
}, [location.pathname, onInvalidRoute])
|
|
77
|
+
|
|
78
|
+
// Don't render until we've checked the handler
|
|
79
|
+
if (!shouldRender) {
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// If no component provided, render nothing (handler-only mode)
|
|
84
|
+
if (!Component) {
|
|
85
|
+
console.warn(`[Navigation] Invalid route "${location.pathname}" - no notFoundComponent configured and onInvalidRoute returned undefined`)
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return <Component path={location.pathname} params={params as Record<string, string>} />
|
|
90
|
+
}
|
|
6
91
|
|
|
7
92
|
/**
|
|
8
93
|
* Build the Web navigator using React Router v7 nested routes
|
|
@@ -46,18 +131,37 @@ const buildRoute = (params: RouteParam, index: number, isNested = false) => {
|
|
|
46
131
|
);
|
|
47
132
|
} else if (params.type === 'navigator') {
|
|
48
133
|
// Get the layout component directly
|
|
49
|
-
const LayoutComponent = params.layoutComponent ||
|
|
134
|
+
const LayoutComponent = params.layoutComponent ||
|
|
50
135
|
(params.layout === 'tab' ? DefaultTabLayout : DefaultStackLayout);
|
|
51
|
-
|
|
136
|
+
|
|
52
137
|
// Transform routes to include full paths for layout component
|
|
53
138
|
const routesWithFullPaths = params.routes.map(route => ({
|
|
54
139
|
...route,
|
|
55
140
|
fullPath: route.path
|
|
56
141
|
}));
|
|
57
|
-
|
|
142
|
+
|
|
143
|
+
// Build child routes including catch-all for 404
|
|
144
|
+
const childRoutes = params.routes.map((child, childIndex) => buildRoute(child, childIndex, true));
|
|
145
|
+
|
|
146
|
+
// Add catch-all route if notFoundComponent or onInvalidRoute is configured
|
|
147
|
+
if (params.notFoundComponent || params.onInvalidRoute) {
|
|
148
|
+
childRoutes.push(
|
|
149
|
+
<Route
|
|
150
|
+
key="__notFound__"
|
|
151
|
+
path="*"
|
|
152
|
+
element={
|
|
153
|
+
<NotFoundWrapper
|
|
154
|
+
component={params.notFoundComponent}
|
|
155
|
+
onInvalidRoute={params.onInvalidRoute}
|
|
156
|
+
/>
|
|
157
|
+
}
|
|
158
|
+
/>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
58
162
|
return (
|
|
59
|
-
<Route
|
|
60
|
-
key={`${params.path}-${index}`}
|
|
163
|
+
<Route
|
|
164
|
+
key={`${params.path}-${index}`}
|
|
61
165
|
path={routePath}
|
|
62
166
|
element={
|
|
63
167
|
<LayoutComponent
|
|
@@ -67,7 +171,7 @@ const buildRoute = (params: RouteParam, index: number, isNested = false) => {
|
|
|
67
171
|
/>
|
|
68
172
|
}
|
|
69
173
|
>
|
|
70
|
-
{
|
|
174
|
+
{childRoutes}
|
|
71
175
|
</Route>
|
|
72
176
|
);
|
|
73
177
|
}
|
package/src/routing/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import type { NavigateParams } from "../context/types";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Tab bar specific screen options
|
|
@@ -64,10 +65,37 @@ export type ScreenOptions = {
|
|
|
64
65
|
} & NavigatorOptions;
|
|
65
66
|
|
|
66
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Props passed to the notFoundComponent when an invalid route is accessed
|
|
70
|
+
*/
|
|
71
|
+
export type NotFoundComponentProps = {
|
|
72
|
+
/** The full path that was attempted */
|
|
73
|
+
path: string
|
|
74
|
+
/** Any route parameters that were parsed from the path */
|
|
75
|
+
params?: Record<string, string>
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
export type BaseNavigatorParam = {
|
|
68
79
|
path: string
|
|
69
80
|
type: 'navigator'
|
|
70
81
|
options?: NavigatorOptions
|
|
82
|
+
/**
|
|
83
|
+
* Handler called when an invalid route is accessed.
|
|
84
|
+
* - Return NavigateParams to redirect to a different route
|
|
85
|
+
* - Return undefined to show the notFoundComponent (if set)
|
|
86
|
+
* If not defined, bubbles up to parent navigator.
|
|
87
|
+
*
|
|
88
|
+
* @param invalidPath - The path that was attempted but not found
|
|
89
|
+
* @returns NavigateParams to redirect, or undefined to use notFoundComponent
|
|
90
|
+
*/
|
|
91
|
+
onInvalidRoute?: (invalidPath: string) => NavigateParams | undefined
|
|
92
|
+
/**
|
|
93
|
+
* Component to render/navigate to when route is invalid and onInvalidRoute returns undefined.
|
|
94
|
+
* - Web: Renders at the current URL via catch-all route
|
|
95
|
+
* - Native: Navigated to as a screen
|
|
96
|
+
* - Optional: If not set and nothing handles the route, a warning is logged
|
|
97
|
+
*/
|
|
98
|
+
notFoundComponent?: React.ComponentType<NotFoundComponentProps>
|
|
71
99
|
}
|
|
72
100
|
|
|
73
101
|
export type TabNavigatorParam = {
|