@ionic/react-router 8.8.1-dev.11773168858.1f9c0eb8 → 8.8.1-dev.11773432149.19826d0c
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/dist/index.js +570 -318
- package/dist/index.js.map +1 -1
- package/dist/types/ReactRouter/IonRouteInner.d.ts +1 -1
- package/dist/types/ReactRouter/ReactRouterViewStack.d.ts +18 -1
- package/dist/types/ReactRouter/utils/computeParentPath.d.ts +10 -2
- package/dist/types/ReactRouter/utils/viewItemUtils.d.ts +22 -2
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { __rest } from 'tslib';
|
|
2
2
|
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
|
3
|
-
import { Route, matchPath as matchPath$1, Routes, Navigate, UNSAFE_RouteContext, useLocation, useNavigate, BrowserRouter, useNavigationType, HashRouter } from 'react-router-dom';
|
|
4
|
-
import { ViewStacks, generateId,
|
|
3
|
+
import { Route, matchPath as matchPath$1, Routes, Navigate, UNSAFE_RouteContext, matchRoutes, useLocation, useNavigate, BrowserRouter, useNavigationType, HashRouter } from 'react-router-dom';
|
|
4
|
+
import { IonRoute, ViewStacks, generateId, ViewLifeCycleManager, StackContext, RouteManagerContext, getConfig, LocationHistory, NavManager } from '@ionic/react';
|
|
5
5
|
import { MemoryRouter, useLocation as useLocation$1, useNavigationType as useNavigationType$1 } from 'react-router';
|
|
6
6
|
|
|
7
|
-
const IonRouteInner = ({ path, element }) => {
|
|
8
|
-
return React.createElement(Route, { path: path, element: element });
|
|
7
|
+
const IonRouteInner = ({ path, index, caseSensitive, element }) => {
|
|
8
|
+
return React.createElement(Route, { path: path, index: index, caseSensitive: caseSensitive, element: element });
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -57,7 +57,7 @@ const matchPath = ({ pathname, componentProps }) => {
|
|
|
57
57
|
const match = matchPath$1(matchOptions, normalizedPathname);
|
|
58
58
|
if (match) {
|
|
59
59
|
// Adjust the match to remove the leading '/' we added
|
|
60
|
-
return Object.assign(Object.assign({}, match), { pathname: pathname, pathnameBase: match.pathnameBase === '/' ? '' : match.pathnameBase.slice(1), pattern: Object.assign(Object.assign({}, match.pattern), { path: path }) });
|
|
60
|
+
return Object.assign(Object.assign({}, match), { pathname: pathname, pathnameBase: match.pathnameBase === '/' ? '/' : match.pathnameBase.slice(1), pattern: Object.assign(Object.assign({}, match.pattern), { path: path }) });
|
|
61
61
|
}
|
|
62
62
|
return null;
|
|
63
63
|
}
|
|
@@ -162,6 +162,16 @@ const computeCommonPrefix = (paths) => {
|
|
|
162
162
|
}
|
|
163
163
|
return commonSegments.length > 0 ? '/' + commonSegments.join('/') : '';
|
|
164
164
|
};
|
|
165
|
+
/**
|
|
166
|
+
* Checks if a pathname falls within the scope of a mount path using
|
|
167
|
+
* segment-aware comparison. Prevents false positives like "/tabs-secondary"
|
|
168
|
+
* matching mount path "/tabs".
|
|
169
|
+
*/
|
|
170
|
+
const isPathnameInScope = (pathname, mountPath) => {
|
|
171
|
+
if (mountPath === '/')
|
|
172
|
+
return true;
|
|
173
|
+
return pathname === mountPath || pathname.startsWith(mountPath + '/');
|
|
174
|
+
};
|
|
165
175
|
/**
|
|
166
176
|
* Checks if a route path is a "splat-only" route (just `*` or `/*`).
|
|
167
177
|
*/
|
|
@@ -218,9 +228,20 @@ const analyzeRouteChildren = (routeChildren) => {
|
|
|
218
228
|
const findSpecificMatch = (routeChildren, remainingPath) => {
|
|
219
229
|
return routeChildren.some((route) => isSpecificRouteMatch(route, remainingPath) || matchesEmbeddedWildcardRoute(route, remainingPath));
|
|
220
230
|
};
|
|
231
|
+
/**
|
|
232
|
+
* Returns the first route that matches as a specific (non-wildcard, non-index) route.
|
|
233
|
+
*/
|
|
234
|
+
const findFirstSpecificMatchingRoute = (routeChildren, remainingPath) => {
|
|
235
|
+
return routeChildren.find((route) => isSpecificRouteMatch(route, remainingPath) || matchesEmbeddedWildcardRoute(route, remainingPath));
|
|
236
|
+
};
|
|
221
237
|
/**
|
|
222
238
|
* Checks if any specific route could plausibly match the remaining path.
|
|
223
239
|
* Used to determine if we should fall back to a wildcard match.
|
|
240
|
+
*
|
|
241
|
+
* Uses exact first-segment matching: the remaining path's first segment
|
|
242
|
+
* must exactly equal a route's first segment to block the wildcard.
|
|
243
|
+
* The outlet's mount path is always known from React Router's RouteContext,
|
|
244
|
+
* so no heuristic-based discovery is needed.
|
|
224
245
|
*/
|
|
225
246
|
const couldSpecificRouteMatch = (routeChildren, remainingPath) => {
|
|
226
247
|
const remainingFirstSegment = remainingPath.split('/')[0];
|
|
@@ -233,26 +254,9 @@ const couldSpecificRouteMatch = (routeChildren, remainingPath) => {
|
|
|
233
254
|
const routeFirstSegment = routePath.split('/')[0].replace(/[*:]/g, '');
|
|
234
255
|
if (!routeFirstSegment)
|
|
235
256
|
return false;
|
|
236
|
-
|
|
237
|
-
return (routeFirstSegment.startsWith(remainingFirstSegment.slice(0, 3)) ||
|
|
238
|
-
remainingFirstSegment.startsWith(routeFirstSegment.slice(0, 3)));
|
|
257
|
+
return routeFirstSegment === remainingFirstSegment;
|
|
239
258
|
});
|
|
240
259
|
};
|
|
241
|
-
/**
|
|
242
|
-
* Checks for index route match when remaining path is empty.
|
|
243
|
-
* Index routes only match at the outlet's mount path level.
|
|
244
|
-
*/
|
|
245
|
-
const checkIndexMatch = (parentPath, remainingPath, hasIndexRoute, outletMountPath) => {
|
|
246
|
-
if ((remainingPath === '' || remainingPath === '/') && hasIndexRoute) {
|
|
247
|
-
if (outletMountPath) {
|
|
248
|
-
// Index should only match at the existing mount path
|
|
249
|
-
return parentPath === outletMountPath ? parentPath : undefined;
|
|
250
|
-
}
|
|
251
|
-
// No mount path yet - this would establish it
|
|
252
|
-
return parentPath;
|
|
253
|
-
}
|
|
254
|
-
return undefined;
|
|
255
|
-
};
|
|
256
260
|
/**
|
|
257
261
|
* Determines the best parent path from the available matches.
|
|
258
262
|
* Priority: specific > wildcard > index
|
|
@@ -287,30 +291,55 @@ const computeAbsoluteRoutesParentPath = (routeChildren, currentPathname, outletM
|
|
|
287
291
|
* Computes the parent path for a nested outlet based on the current pathname
|
|
288
292
|
* and the outlet's route configuration.
|
|
289
293
|
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
294
|
+
* When the mount path is known (seeded from React Router's RouteContext), the
|
|
295
|
+
* parent path is simply the mount path — no iterative discovery needed. The
|
|
296
|
+
* iterative fallback only runs for outlets where RouteContext doesn't provide
|
|
297
|
+
* a parent match (typically root-level outlets on first render).
|
|
292
298
|
*
|
|
293
299
|
* @param options The options for computing the parent path.
|
|
294
300
|
* @returns The computed parent path result.
|
|
295
301
|
*/
|
|
296
302
|
const computeParentPath = (options) => {
|
|
297
303
|
const { currentPathname, outletMountPath, routeChildren, hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = options;
|
|
298
|
-
// If pathname is outside the established mount path scope, skip computation
|
|
299
|
-
|
|
304
|
+
// If pathname is outside the established mount path scope, skip computation.
|
|
305
|
+
// Use segment-aware comparison: /tabs-secondary must NOT match /tabs scope.
|
|
306
|
+
if (outletMountPath && !isPathnameInScope(currentPathname, outletMountPath)) {
|
|
300
307
|
return { parentPath: undefined, outletMountPath };
|
|
301
308
|
}
|
|
302
|
-
|
|
309
|
+
// Fast path: when the mount path is known (from React Router's RouteContext),
|
|
310
|
+
// the parent path IS the mount path. The iterative segment-by-segment discovery
|
|
311
|
+
// below was needed when the mount depth had to be guessed from URL structure,
|
|
312
|
+
// but with RouteContext we already know exactly where this outlet is mounted.
|
|
313
|
+
if (outletMountPath && (hasRelativeRoutes || hasIndexRoute)) {
|
|
314
|
+
return { parentPath: outletMountPath, outletMountPath };
|
|
315
|
+
}
|
|
316
|
+
// Fallback: mount path not yet known. Iterate through path segments to discover
|
|
317
|
+
// the correct parent depth. This only runs on first render of outlets where
|
|
318
|
+
// RouteContext doesn't provide a parent match (typically root-level outlets,
|
|
319
|
+
// which usually have absolute routes and take the absolute routes path below).
|
|
320
|
+
if (!outletMountPath && (hasRelativeRoutes || hasIndexRoute) && currentPathname.includes('/')) {
|
|
303
321
|
const segments = currentPathname.split('/').filter(Boolean);
|
|
304
322
|
if (segments.length >= 1) {
|
|
305
323
|
let firstSpecificMatch;
|
|
306
324
|
let firstWildcardMatch;
|
|
307
325
|
let indexMatchAtMount;
|
|
308
|
-
// Iterate through path segments to find the shortest matching parent path
|
|
309
326
|
for (let i = 1; i <= segments.length; i++) {
|
|
310
327
|
const parentPath = '/' + segments.slice(0, i).join('/');
|
|
311
328
|
const remainingPath = segments.slice(i).join('/');
|
|
312
329
|
// Check for specific route match (highest priority)
|
|
313
330
|
if (!firstSpecificMatch && findSpecificMatch(routeChildren, remainingPath)) {
|
|
331
|
+
// Don't let empty/default path routes (path="" or undefined) drive
|
|
332
|
+
// the parent deeper than a wildcard match. An empty path route matching
|
|
333
|
+
// when remainingPath is "" just means all segments were consumed.
|
|
334
|
+
if (firstWildcardMatch) {
|
|
335
|
+
const matchingRoute = findFirstSpecificMatchingRoute(routeChildren, remainingPath);
|
|
336
|
+
if (matchingRoute) {
|
|
337
|
+
const matchingPath = matchingRoute.props.path;
|
|
338
|
+
if (!matchingPath || matchingPath === '') {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
314
343
|
firstSpecificMatch = parentPath;
|
|
315
344
|
break;
|
|
316
345
|
}
|
|
@@ -322,9 +351,8 @@ const computeParentPath = (options) => {
|
|
|
322
351
|
}
|
|
323
352
|
}
|
|
324
353
|
// Check for index route match
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
indexMatchAtMount = indexMatch;
|
|
354
|
+
if ((remainingPath === '' || remainingPath === '/') && hasIndexRoute) {
|
|
355
|
+
indexMatchAtMount = parentPath;
|
|
328
356
|
}
|
|
329
357
|
}
|
|
330
358
|
// Fallback: check root level for embedded wildcard routes (e.g., "tab1/*")
|
|
@@ -335,12 +363,7 @@ const computeParentPath = (options) => {
|
|
|
335
363
|
}
|
|
336
364
|
}
|
|
337
365
|
const bestPath = selectBestMatch(firstSpecificMatch, firstWildcardMatch, indexMatchAtMount);
|
|
338
|
-
|
|
339
|
-
const newOutletMountPath = outletMountPath || bestPath;
|
|
340
|
-
if (newOutletMountPath && !currentPathname.startsWith(newOutletMountPath)) {
|
|
341
|
-
return { parentPath: undefined, outletMountPath: newOutletMountPath };
|
|
342
|
-
}
|
|
343
|
-
return { parentPath: bestPath, outletMountPath: newOutletMountPath };
|
|
366
|
+
return { parentPath: bestPath, outletMountPath: bestPath };
|
|
344
367
|
}
|
|
345
368
|
}
|
|
346
369
|
// Handle outlets with only absolute routes
|
|
@@ -419,7 +442,7 @@ const getRoutesChildren = (node) => {
|
|
|
419
442
|
const extractRouteChildren = (children) => {
|
|
420
443
|
var _a;
|
|
421
444
|
const routesChildren = (_a = getRoutesChildren(children)) !== null && _a !== void 0 ? _a : children;
|
|
422
|
-
return React.Children.toArray(routesChildren).filter((child) => React.isValidElement(child) && child.type === Route);
|
|
445
|
+
return React.Children.toArray(routesChildren).filter((child) => React.isValidElement(child) && (child.type === Route || child.type === IonRoute));
|
|
423
446
|
};
|
|
424
447
|
/**
|
|
425
448
|
* Checks if a React element is a Navigate component (redirect).
|
|
@@ -432,28 +455,57 @@ const isNavigateElement = (element) => {
|
|
|
432
455
|
(element.type === Navigate || (typeof element.type === 'function' && element.type.name === 'Navigate')));
|
|
433
456
|
};
|
|
434
457
|
|
|
458
|
+
/**
|
|
459
|
+
* Compares two routes by specificity for sorting (most specific first).
|
|
460
|
+
*
|
|
461
|
+
* Sort order:
|
|
462
|
+
* 1. Index routes come first
|
|
463
|
+
* 2. Wildcard-only routes (* or /*) come last
|
|
464
|
+
* 3. Exact matches (no wildcards/params) before wildcard/param routes
|
|
465
|
+
* 4. Among routes with same status, longer paths are more specific
|
|
466
|
+
*/
|
|
467
|
+
const compareRouteSpecificity = (a, b) => {
|
|
468
|
+
// Index routes come first
|
|
469
|
+
if (a.index && !b.index)
|
|
470
|
+
return -1;
|
|
471
|
+
if (!a.index && b.index)
|
|
472
|
+
return 1;
|
|
473
|
+
// Wildcard-only routes (* or /*) should come last
|
|
474
|
+
const aIsWildcardOnly = a.path === '*' || a.path === '/*';
|
|
475
|
+
const bIsWildcardOnly = b.path === '*' || b.path === '/*';
|
|
476
|
+
if (!aIsWildcardOnly && bIsWildcardOnly)
|
|
477
|
+
return -1;
|
|
478
|
+
if (aIsWildcardOnly && !bIsWildcardOnly)
|
|
479
|
+
return 1;
|
|
480
|
+
// Exact matches (no wildcards/params) come before wildcard/param routes
|
|
481
|
+
const aHasWildcard = a.path.includes('*') || a.path.includes(':');
|
|
482
|
+
const bHasWildcard = b.path.includes('*') || b.path.includes(':');
|
|
483
|
+
if (!aHasWildcard && bHasWildcard)
|
|
484
|
+
return -1;
|
|
485
|
+
if (aHasWildcard && !bHasWildcard)
|
|
486
|
+
return 1;
|
|
487
|
+
// Among routes with same wildcard status, longer paths are more specific
|
|
488
|
+
if (a.path.length !== b.path.length) {
|
|
489
|
+
return b.path.length - a.path.length;
|
|
490
|
+
}
|
|
491
|
+
return 0;
|
|
492
|
+
};
|
|
435
493
|
/**
|
|
436
494
|
* Sorts view items by route specificity (most specific first).
|
|
437
|
-
*
|
|
438
|
-
*
|
|
495
|
+
*
|
|
496
|
+
* Sort order aligns with findViewItemByPath in ReactRouterViewStack.tsx:
|
|
497
|
+
* 1. Index routes come first
|
|
498
|
+
* 2. Wildcard-only routes (* or /*) come last
|
|
499
|
+
* 3. Exact matches (no wildcards/params) come before wildcard/param routes
|
|
500
|
+
* 4. Among routes with same wildcard status, longer paths are more specific
|
|
439
501
|
*
|
|
440
502
|
* @param views The view items to sort.
|
|
441
503
|
* @returns A new sorted array of view items.
|
|
442
504
|
*/
|
|
443
505
|
const sortViewsBySpecificity = (views) => {
|
|
444
506
|
return [...views].sort((a, b) => {
|
|
445
|
-
var _a, _b, _c, _d;
|
|
446
|
-
|
|
447
|
-
const pathB = ((_d = (_c = b.routeData) === null || _c === void 0 ? void 0 : _c.childProps) === null || _d === void 0 ? void 0 : _d.path) || '';
|
|
448
|
-
// Exact matches (no wildcards/params) come first
|
|
449
|
-
const aHasWildcard = pathA.includes('*') || pathA.includes(':');
|
|
450
|
-
const bHasWildcard = pathB.includes('*') || pathB.includes(':');
|
|
451
|
-
if (!aHasWildcard && bHasWildcard)
|
|
452
|
-
return -1;
|
|
453
|
-
if (aHasWildcard && !bHasWildcard)
|
|
454
|
-
return 1;
|
|
455
|
-
// Among wildcard routes, longer paths are more specific
|
|
456
|
-
return pathB.length - pathA.length;
|
|
507
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
508
|
+
return compareRouteSpecificity({ path: ((_b = (_a = a.routeData) === null || _a === void 0 ? void 0 : _a.childProps) === null || _b === void 0 ? void 0 : _b.path) || '', index: !!((_d = (_c = a.routeData) === null || _c === void 0 ? void 0 : _c.childProps) === null || _d === void 0 ? void 0 : _d.index) }, { path: ((_f = (_e = b.routeData) === null || _e === void 0 ? void 0 : _e.childProps) === null || _f === void 0 ? void 0 : _f.path) || '', index: !!((_h = (_g = b.routeData) === null || _g === void 0 ? void 0 : _g.childProps) === null || _h === void 0 ? void 0 : _h.index) });
|
|
457
509
|
});
|
|
458
510
|
};
|
|
459
511
|
|
|
@@ -504,7 +556,7 @@ const computeAbsolutePathnameBase = (routeElement, routeMatch, parentPathnameBas
|
|
|
504
556
|
*/
|
|
505
557
|
const getFallbackParamsFromViewItems = (allViewItems, currentOutletId, currentPathname) => {
|
|
506
558
|
var _a;
|
|
507
|
-
const
|
|
559
|
+
const matchingViews = [];
|
|
508
560
|
for (const otherViewItem of allViewItems) {
|
|
509
561
|
if (otherViewItem.outletId === currentOutletId)
|
|
510
562
|
continue;
|
|
@@ -512,10 +564,20 @@ const getFallbackParamsFromViewItems = (allViewItems, currentOutletId, currentPa
|
|
|
512
564
|
if ((otherMatch === null || otherMatch === void 0 ? void 0 : otherMatch.params) && Object.keys(otherMatch.params).length > 0) {
|
|
513
565
|
const matchedPathname = otherMatch.pathnameBase || otherMatch.pathname;
|
|
514
566
|
if (matchedPathname && currentPathname.startsWith(matchedPathname)) {
|
|
515
|
-
|
|
567
|
+
matchingViews.push({
|
|
568
|
+
params: otherMatch.params,
|
|
569
|
+
pathLength: matchedPathname.length,
|
|
570
|
+
});
|
|
516
571
|
}
|
|
517
572
|
}
|
|
518
573
|
}
|
|
574
|
+
// Sort ascending by path length so more-specific (longer) paths are applied
|
|
575
|
+
// last and their params take priority over less-specific ones.
|
|
576
|
+
matchingViews.sort((a, b) => a.pathLength - b.pathLength);
|
|
577
|
+
const params = {};
|
|
578
|
+
for (const view of matchingViews) {
|
|
579
|
+
Object.assign(params, view.params);
|
|
580
|
+
}
|
|
519
581
|
return params;
|
|
520
582
|
};
|
|
521
583
|
/**
|
|
@@ -596,6 +658,18 @@ const resolveIndexRouteMatch = (viewItem, pathname, parentPath) => {
|
|
|
596
658
|
class ReactRouterViewStack extends ViewStacks {
|
|
597
659
|
constructor() {
|
|
598
660
|
super();
|
|
661
|
+
/**
|
|
662
|
+
* Stores the computed parent path for each outlet.
|
|
663
|
+
* Used by findViewItemByPath to correctly evaluate index route matches
|
|
664
|
+
* without requiring the outlet's React element or route children.
|
|
665
|
+
*/
|
|
666
|
+
this.outletParentPaths = new Map();
|
|
667
|
+
/**
|
|
668
|
+
* Stores the computed mount path for each outlet.
|
|
669
|
+
* Fed back into computeParentPath on subsequent calls to stabilize
|
|
670
|
+
* the parent path computation across navigations (mirrors StackManager.outletMountPath).
|
|
671
|
+
*/
|
|
672
|
+
this.outletMountPaths = new Map();
|
|
599
673
|
/**
|
|
600
674
|
* Creates a new view item for the given outlet and react route element.
|
|
601
675
|
* Associates route props with the matched route path for further lookups.
|
|
@@ -639,7 +713,7 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
639
713
|
if (hasParams) {
|
|
640
714
|
if (isWildcard) {
|
|
641
715
|
const existingPathnameBase = (_f = (_e = v.routeData) === null || _e === void 0 ? void 0 : _e.match) === null || _f === void 0 ? void 0 : _f.pathnameBase;
|
|
642
|
-
const newMatch = matchComponent$1(reactElement, routeInfo.pathname, false);
|
|
716
|
+
const newMatch = matchComponent$1(reactElement, routeInfo.pathname, false, this.outletParentPaths.get(outletId));
|
|
643
717
|
const newPathnameBase = newMatch === null || newMatch === void 0 ? void 0 : newMatch.pathnameBase;
|
|
644
718
|
if (existingPathnameBase !== newPathnameBase) {
|
|
645
719
|
return false;
|
|
@@ -665,7 +739,7 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
665
739
|
existingViewItem.reactElement = reactElement;
|
|
666
740
|
existingViewItem.mount = true;
|
|
667
741
|
existingViewItem.ionPageElement = page || existingViewItem.ionPageElement;
|
|
668
|
-
const updatedMatch = matchComponent$1(reactElement, routeInfo.pathname, false) ||
|
|
742
|
+
const updatedMatch = matchComponent$1(reactElement, routeInfo.pathname, false, this.outletParentPaths.get(outletId)) ||
|
|
669
743
|
((_a = existingViewItem.routeData) === null || _a === void 0 ? void 0 : _a.match) ||
|
|
670
744
|
createDefaultMatch(routeInfo.pathname, reactElement.props);
|
|
671
745
|
existingViewItem.routeData = {
|
|
@@ -687,7 +761,7 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
687
761
|
if (reactElement.type === IonRoute) {
|
|
688
762
|
viewItem.disableIonPageManagement = reactElement.props.disableIonPageManagement;
|
|
689
763
|
}
|
|
690
|
-
const initialMatch = matchComponent$1(reactElement, routeInfo.pathname, true) ||
|
|
764
|
+
const initialMatch = matchComponent$1(reactElement, routeInfo.pathname, true, this.outletParentPaths.get(outletId)) ||
|
|
691
765
|
createDefaultMatch(routeInfo.pathname, reactElement.props);
|
|
692
766
|
viewItem.routeData = {
|
|
693
767
|
match: initialMatch,
|
|
@@ -704,10 +778,10 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
704
778
|
* - Wraps the route element in <Routes> to support nested routing and ensure remounting
|
|
705
779
|
* - Adds a unique key to <Routes> so React Router remounts routes when switching
|
|
706
780
|
*/
|
|
707
|
-
this.renderViewItem = (viewItem, routeInfo, parentPath) => {
|
|
781
|
+
this.renderViewItem = (viewItem, routeInfo, parentPath, reRender) => {
|
|
708
782
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
709
783
|
const routePath = viewItem.reactElement.props.path || '';
|
|
710
|
-
let match = matchComponent$1(viewItem.reactElement, routeInfo.pathname);
|
|
784
|
+
let match = matchComponent$1(viewItem.reactElement, routeInfo.pathname, false, parentPath);
|
|
711
785
|
if (!match) {
|
|
712
786
|
const indexMatch = resolveIndexRouteMatch(viewItem, routeInfo.pathname, parentPath);
|
|
713
787
|
if (indexMatch) {
|
|
@@ -742,6 +816,7 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
742
816
|
// This ensures the redirect completes before removal
|
|
743
817
|
setTimeout(() => {
|
|
744
818
|
this.remove(viewItem);
|
|
819
|
+
reRender === null || reRender === void 0 ? void 0 : reRender();
|
|
745
820
|
}, NAVIGATE_REDIRECT_DELAY_MS);
|
|
746
821
|
}
|
|
747
822
|
}
|
|
@@ -765,6 +840,7 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
765
840
|
const stillNotNeeded = !viewItem.mount && !viewItem.ionPageElement;
|
|
766
841
|
if (stillNotNeeded) {
|
|
767
842
|
this.remove(viewItem);
|
|
843
|
+
reRender === null || reRender === void 0 ? void 0 : reRender();
|
|
768
844
|
}
|
|
769
845
|
}, VIEW_CLEANUP_DELAY_MS);
|
|
770
846
|
}
|
|
@@ -775,25 +851,71 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
775
851
|
}
|
|
776
852
|
// Reactivate view if it matches but was previously deactivated
|
|
777
853
|
// Don't reactivate if this is a parameterized route navigating to a different path instance
|
|
778
|
-
|
|
854
|
+
// Don't reactivate catch-all wildcard routes — they are created fresh by createViewItem
|
|
855
|
+
const isCatchAllWildcard = routePath === '*' || routePath === '/*';
|
|
856
|
+
if (match && !viewItem.mount && !shouldSkipForDifferentParam && !isCatchAllWildcard) {
|
|
779
857
|
viewItem.mount = true;
|
|
780
858
|
viewItem.routeData.match = match;
|
|
781
859
|
}
|
|
782
|
-
// Deactivate wildcard
|
|
783
|
-
// This prevents "Not found" or fallback pages from showing alongside valid routes
|
|
860
|
+
// Deactivate wildcard (catch-all) and empty-path (default) routes when a more-specific route matches.
|
|
861
|
+
// This prevents "Not found" or fallback pages from showing alongside valid routes.
|
|
784
862
|
if (routePath === '*' || routePath === '') {
|
|
785
863
|
// Check if any other view in this outlet has a match for the current route
|
|
786
|
-
const
|
|
864
|
+
const outletViews = this.getViewItemsForOutlet(viewItem.outletId);
|
|
865
|
+
// When parent path context is available, compute the relative pathname once
|
|
866
|
+
// outside the loop since both routeInfo.pathname and parentPath are invariant.
|
|
867
|
+
const relativePathname = parentPath
|
|
868
|
+
? computeRelativeToParent(routeInfo.pathname, parentPath)
|
|
869
|
+
: null;
|
|
870
|
+
let hasSpecificMatch = outletViews.some((v) => {
|
|
787
871
|
var _a, _b;
|
|
788
872
|
if (v.id === viewItem.id)
|
|
789
873
|
return false; // Skip self
|
|
790
874
|
const vRoutePath = ((_b = (_a = v.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path) || '';
|
|
791
875
|
if (vRoutePath === '*' || vRoutePath === '')
|
|
792
876
|
return false; // Skip other wildcard/empty routes
|
|
793
|
-
//
|
|
877
|
+
// When parent path context is available and the route is relative, use
|
|
878
|
+
// parent-path-aware matching. This avoids false positives from
|
|
879
|
+
// derivePathnameToMatch's tail-slice heuristic, which can incorrectly
|
|
880
|
+
// match route literals that appear at the wrong position in the pathname.
|
|
881
|
+
// Example: pathname /parent/extra/details/99 with route details/:id —
|
|
882
|
+
// the tail-slice extracts ["details","99"] producing a false match.
|
|
883
|
+
if (parentPath && vRoutePath && !vRoutePath.startsWith('/')) {
|
|
884
|
+
if (relativePathname === null) {
|
|
885
|
+
return false; // Pathname is outside this outlet's parent scope
|
|
886
|
+
}
|
|
887
|
+
return !!matchPath({
|
|
888
|
+
pathname: relativePathname,
|
|
889
|
+
componentProps: v.reactElement.props,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
// Fallback to matchComponent when no parent path context is available
|
|
794
893
|
const vMatch = v.reactElement ? matchComponent$1(v.reactElement, routeInfo.pathname) : null;
|
|
795
894
|
return !!vMatch;
|
|
796
895
|
});
|
|
896
|
+
// For catch-all * routes, also deactivate when the pathname matches the outlet's
|
|
897
|
+
// parent path exactly. This means there are no remaining segments for the wildcard
|
|
898
|
+
// to catch, so the empty-path or index route should handle it instead.
|
|
899
|
+
if (!hasSpecificMatch && routePath === '*') {
|
|
900
|
+
const outletParentPath = this.outletParentPaths.get(viewItem.outletId);
|
|
901
|
+
if (outletParentPath) {
|
|
902
|
+
const normalizedParent = normalizePathnameForComparison(outletParentPath);
|
|
903
|
+
const normalizedPathname = normalizePathnameForComparison(routeInfo.pathname);
|
|
904
|
+
if (normalizedPathname === normalizedParent) {
|
|
905
|
+
// Check if there's an empty-path or index view item that should handle this
|
|
906
|
+
const hasDefaultRoute = outletViews.some((v) => {
|
|
907
|
+
var _a, _b, _c, _d;
|
|
908
|
+
if (v.id === viewItem.id)
|
|
909
|
+
return false;
|
|
910
|
+
const vRoutePath = (_b = (_a = v.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
|
|
911
|
+
return vRoutePath === '' || vRoutePath === undefined || !!((_d = (_c = v.routeData) === null || _c === void 0 ? void 0 : _c.childProps) === null || _d === void 0 ? void 0 : _d.index);
|
|
912
|
+
});
|
|
913
|
+
if (hasDefaultRoute) {
|
|
914
|
+
hasSpecificMatch = true;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
797
919
|
if (hasSpecificMatch) {
|
|
798
920
|
viewItem.mount = false;
|
|
799
921
|
if (viewItem.ionPageElement) {
|
|
@@ -838,33 +960,50 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
838
960
|
* 3. Returns a list of React components that will be rendered inside the outlet
|
|
839
961
|
* Each view is wrapped in <ViewLifeCycleManager> to manage lifecycle and rendering
|
|
840
962
|
*/
|
|
841
|
-
this.getChildrenToRender = (outletId, ionRouterOutlet, routeInfo) => {
|
|
963
|
+
this.getChildrenToRender = (outletId, ionRouterOutlet, routeInfo, reRender, parentPathnameBase) => {
|
|
842
964
|
const viewItems = this.getViewItemsForOutlet(outletId);
|
|
843
|
-
//
|
|
965
|
+
// Seed the mount path from the parent route context if available.
|
|
966
|
+
// This provides the outlet's mount path immediately on first render,
|
|
967
|
+
// eliminating the need for heuristic-based discovery in computeParentPath.
|
|
968
|
+
if (parentPathnameBase && !this.outletMountPaths.has(outletId)) {
|
|
969
|
+
this.outletMountPaths.set(outletId, parentPathnameBase);
|
|
970
|
+
}
|
|
971
|
+
// Determine parentPath for outlets with relative or index routes.
|
|
972
|
+
// This populates outletParentPaths for findViewItemByPath's matchView
|
|
973
|
+
// and the catch-all deactivation logic in renderViewItem.
|
|
844
974
|
let parentPath = undefined;
|
|
845
975
|
try {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
976
|
+
const routeChildren = extractRouteChildren(ionRouterOutlet.props.children);
|
|
977
|
+
const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);
|
|
978
|
+
if (hasRelativeRoutes || hasIndexRoute) {
|
|
979
|
+
const result = computeParentPath({
|
|
980
|
+
currentPathname: routeInfo.pathname,
|
|
981
|
+
outletMountPath: this.outletMountPaths.get(outletId),
|
|
982
|
+
routeChildren,
|
|
983
|
+
hasRelativeRoutes,
|
|
984
|
+
hasIndexRoute,
|
|
985
|
+
hasWildcardRoute,
|
|
986
|
+
});
|
|
987
|
+
parentPath = result.parentPath;
|
|
988
|
+
// Persist the mount path for subsequent calls, mirroring StackManager.outletMountPath.
|
|
989
|
+
// Unlike outletParentPaths (cleared when parentPath is undefined), the mount path is
|
|
990
|
+
// intentionally sticky — it anchors the outlet's scope and is only removed in clear().
|
|
991
|
+
if (result.outletMountPath && !this.outletMountPaths.has(outletId)) {
|
|
992
|
+
this.outletMountPaths.set(outletId, result.outletMountPath);
|
|
862
993
|
}
|
|
863
994
|
}
|
|
864
995
|
}
|
|
865
996
|
catch (e) {
|
|
866
997
|
// Non-fatal: if we fail to compute parentPath, fall back to previous behavior
|
|
867
998
|
}
|
|
999
|
+
// Store the computed parentPath for use in findViewItemByPath.
|
|
1000
|
+
// Clear stale entries when parentPath is undefined (e.g., navigated out of scope).
|
|
1001
|
+
if (parentPath !== undefined) {
|
|
1002
|
+
this.outletParentPaths.set(outletId, parentPath);
|
|
1003
|
+
}
|
|
1004
|
+
else if (this.outletParentPaths.has(outletId)) {
|
|
1005
|
+
this.outletParentPaths.delete(outletId);
|
|
1006
|
+
}
|
|
868
1007
|
// Sync child elements with stored viewItems (e.g. to reflect new props)
|
|
869
1008
|
React.Children.forEach(ionRouterOutlet.props.children, (child) => {
|
|
870
1009
|
// Ensure the child is a valid React element since we
|
|
@@ -908,7 +1047,7 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
908
1047
|
const viewRoutePath = (_d = (_c = viewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
|
|
909
1048
|
if (viewRoutePath) {
|
|
910
1049
|
// First try exact match using matchComponent
|
|
911
|
-
const routeMatch = matchComponent$1(viewItem.reactElement, routeInfo.pathname);
|
|
1050
|
+
const routeMatch = matchComponent$1(viewItem.reactElement, routeInfo.pathname, false, parentPath);
|
|
912
1051
|
if (routeMatch) {
|
|
913
1052
|
// View matches current route, keep it
|
|
914
1053
|
return true;
|
|
@@ -924,6 +1063,7 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
924
1063
|
// View is outside current route hierarchy, remove it
|
|
925
1064
|
setTimeout(() => {
|
|
926
1065
|
this.remove(viewItem);
|
|
1066
|
+
reRender();
|
|
927
1067
|
}, 0);
|
|
928
1068
|
return false;
|
|
929
1069
|
}
|
|
@@ -931,7 +1071,7 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
931
1071
|
}
|
|
932
1072
|
return true;
|
|
933
1073
|
});
|
|
934
|
-
const renderedItems = renderableViewItems.map((viewItem) => this.renderViewItem(viewItem, routeInfo, parentPath));
|
|
1074
|
+
const renderedItems = renderableViewItems.map((viewItem) => this.renderViewItem(viewItem, routeInfo, parentPath, reRender));
|
|
935
1075
|
return renderedItems;
|
|
936
1076
|
};
|
|
937
1077
|
/**
|
|
@@ -991,6 +1131,14 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
991
1131
|
super.add(viewItem);
|
|
992
1132
|
this.cleanupStaleViewItems(viewItem.outletId);
|
|
993
1133
|
};
|
|
1134
|
+
/**
|
|
1135
|
+
* Override clear to also clean up the stored parent path for the outlet.
|
|
1136
|
+
*/
|
|
1137
|
+
this.clear = (outletId) => {
|
|
1138
|
+
this.outletParentPaths.delete(outletId);
|
|
1139
|
+
this.outletMountPaths.delete(outletId);
|
|
1140
|
+
return super.clear(outletId);
|
|
1141
|
+
};
|
|
994
1142
|
/**
|
|
995
1143
|
* Override remove
|
|
996
1144
|
*/
|
|
@@ -1006,6 +1154,8 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
1006
1154
|
let viewItem;
|
|
1007
1155
|
let match = null;
|
|
1008
1156
|
let viewStack;
|
|
1157
|
+
// Capture stored parent paths for use in nested matchView/matchDefaultRoute functions
|
|
1158
|
+
const storedParentPaths = this.outletParentPaths;
|
|
1009
1159
|
if (outletId) {
|
|
1010
1160
|
viewStack = sortViewsBySpecificity(this.getViewItemsForOutlet(outletId));
|
|
1011
1161
|
viewStack.some(matchView);
|
|
@@ -1033,16 +1183,36 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
1033
1183
|
if (mustBeIonRoute && !v.ionRoute)
|
|
1034
1184
|
return false;
|
|
1035
1185
|
const viewItemPath = v.routeData.childProps.path || '';
|
|
1186
|
+
// Skip unmounted catch-all wildcard views. After back navigation unmounts
|
|
1187
|
+
// a wildcard view, it should not be reused for subsequent navigations.
|
|
1188
|
+
// A fresh wildcard view will be created by createViewItem when needed.
|
|
1189
|
+
if ((viewItemPath === '*' || viewItemPath === '/*') && !v.mount)
|
|
1190
|
+
return false;
|
|
1036
1191
|
const isIndexRoute = !!v.routeData.childProps.index;
|
|
1037
1192
|
const previousMatch = (_a = v.routeData) === null || _a === void 0 ? void 0 : _a.match;
|
|
1038
|
-
const
|
|
1193
|
+
const outletParentPath = storedParentPaths.get(v.outletId);
|
|
1194
|
+
const result = v.reactElement ? matchComponent$1(v.reactElement, pathname, false, outletParentPath) : null;
|
|
1039
1195
|
if (!result) {
|
|
1040
|
-
const indexMatch = resolveIndexRouteMatch(v, pathname,
|
|
1196
|
+
const indexMatch = resolveIndexRouteMatch(v, pathname, outletParentPath);
|
|
1041
1197
|
if (indexMatch) {
|
|
1042
1198
|
match = indexMatch;
|
|
1043
1199
|
viewItem = v;
|
|
1044
1200
|
return true;
|
|
1045
1201
|
}
|
|
1202
|
+
// Empty path routes (path="") should match when the pathname matches the
|
|
1203
|
+
// outlet's parent path exactly (no remaining segments). matchComponent doesn't
|
|
1204
|
+
// handle this because it lacks parent path context. Without this check, a
|
|
1205
|
+
// catch-all * view item (which matches any pathname) would be incorrectly
|
|
1206
|
+
// returned instead of the empty path route on back navigation.
|
|
1207
|
+
if (viewItemPath === '' && !isIndexRoute && outletParentPath) {
|
|
1208
|
+
const normalizedParent = normalizePathnameForComparison(outletParentPath);
|
|
1209
|
+
const normalizedPathname = normalizePathnameForComparison(pathname);
|
|
1210
|
+
if (normalizedPathname === normalizedParent) {
|
|
1211
|
+
match = createDefaultMatch(pathname, v.routeData.childProps);
|
|
1212
|
+
viewItem = v;
|
|
1213
|
+
return true;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1046
1216
|
}
|
|
1047
1217
|
if (result) {
|
|
1048
1218
|
const hasParams = result.params && Object.keys(result.params).length > 0;
|
|
@@ -1099,7 +1269,8 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
1099
1269
|
const isDefaultRoute = childProps.path === undefined || childProps.path === '';
|
|
1100
1270
|
const isIndexRoute = !!childProps.index;
|
|
1101
1271
|
if (isIndexRoute) {
|
|
1102
|
-
const
|
|
1272
|
+
const outletParentPath = storedParentPaths.get(v.outletId);
|
|
1273
|
+
const indexMatch = resolveIndexRouteMatch(v, pathname, outletParentPath);
|
|
1103
1274
|
if (indexMatch) {
|
|
1104
1275
|
match = indexMatch;
|
|
1105
1276
|
viewItem = v;
|
|
@@ -1136,11 +1307,22 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
1136
1307
|
/**
|
|
1137
1308
|
* Utility to apply matchPath to a React element and return its match state.
|
|
1138
1309
|
*/
|
|
1139
|
-
function matchComponent$1(node, pathname, allowFallback = false) {
|
|
1310
|
+
function matchComponent$1(node, pathname, allowFallback = false, parentPath) {
|
|
1140
1311
|
var _a;
|
|
1141
1312
|
const routeProps = (_a = node === null || node === void 0 ? void 0 : node.props) !== null && _a !== void 0 ? _a : {};
|
|
1142
1313
|
const routePath = routeProps.path;
|
|
1143
|
-
|
|
1314
|
+
let pathnameToMatch;
|
|
1315
|
+
if (parentPath && routePath && !routePath.startsWith('/')) {
|
|
1316
|
+
// When parent path is known, compute exact relative pathname
|
|
1317
|
+
// instead of using the tail-slice heuristic
|
|
1318
|
+
const relative = pathname.startsWith(parentPath)
|
|
1319
|
+
? pathname.slice(parentPath.length).replace(/^\//, '')
|
|
1320
|
+
: pathname;
|
|
1321
|
+
pathnameToMatch = relative;
|
|
1322
|
+
}
|
|
1323
|
+
else {
|
|
1324
|
+
pathnameToMatch = derivePathnameToMatch(pathname, routePath);
|
|
1325
|
+
}
|
|
1144
1326
|
const match = matchPath({
|
|
1145
1327
|
pathname: pathnameToMatch,
|
|
1146
1328
|
componentProps: routeProps,
|
|
@@ -1191,11 +1373,13 @@ function clonePageElement(leavingViewHtml) {
|
|
|
1191
1373
|
*/
|
|
1192
1374
|
const VIEW_UNMOUNT_DELAY_MS = 250;
|
|
1193
1375
|
/**
|
|
1194
|
-
* Delay
|
|
1195
|
-
*
|
|
1376
|
+
* Delay (ms) to wait for an IonPage to mount before proceeding with a
|
|
1377
|
+
* page transition. Only container routes (nested outlets with no direct
|
|
1378
|
+
* IonPage) actually hit this timeout; normal routes clear it early via
|
|
1379
|
+
* registerIonPage, so a larger value here doesn't affect the happy path.
|
|
1196
1380
|
*/
|
|
1197
|
-
const ION_PAGE_WAIT_TIMEOUT_MS =
|
|
1198
|
-
const isViewVisible = (el) => !el.classList.contains('ion-page-invisible') && !el.classList.contains('ion-page-hidden') && el.style.
|
|
1381
|
+
const ION_PAGE_WAIT_TIMEOUT_MS = 300;
|
|
1382
|
+
const isViewVisible = (el) => !el.classList.contains('ion-page-invisible') && !el.classList.contains('ion-page-hidden') && el.style.visibility !== 'hidden';
|
|
1199
1383
|
const hideIonPageElement = (element) => {
|
|
1200
1384
|
if (element) {
|
|
1201
1385
|
element.classList.add('ion-page-hidden');
|
|
@@ -1204,7 +1388,7 @@ const hideIonPageElement = (element) => {
|
|
|
1204
1388
|
};
|
|
1205
1389
|
const showIonPageElement = (element) => {
|
|
1206
1390
|
if (element) {
|
|
1207
|
-
element.style.removeProperty('
|
|
1391
|
+
element.style.removeProperty('visibility');
|
|
1208
1392
|
element.classList.remove('ion-page-hidden');
|
|
1209
1393
|
element.removeAttribute('aria-hidden');
|
|
1210
1394
|
}
|
|
@@ -1218,7 +1402,16 @@ class StackManager extends React.PureComponent {
|
|
|
1218
1402
|
};
|
|
1219
1403
|
this.pendingPageTransition = false;
|
|
1220
1404
|
this.waitingForIonPage = false;
|
|
1405
|
+
/** Tracks whether the component is mounted to guard async transition paths. */
|
|
1406
|
+
this._isMounted = false;
|
|
1407
|
+
/** In-flight requestAnimationFrame IDs from transitionPage, cancelled on unmount. */
|
|
1408
|
+
this.transitionRafIds = [];
|
|
1221
1409
|
this.outletMountPath = undefined;
|
|
1410
|
+
/**
|
|
1411
|
+
* Whether this outlet is at the root level (no parent route matches).
|
|
1412
|
+
* Derived from UNSAFE_RouteContext in render() — empty matches means root.
|
|
1413
|
+
*/
|
|
1414
|
+
this.isRootOutlet = true;
|
|
1222
1415
|
this.registerIonPage = this.registerIonPage.bind(this);
|
|
1223
1416
|
this.transitionPage = this.transitionPage.bind(this);
|
|
1224
1417
|
this.handlePageTransition = this.handlePageTransition.bind(this);
|
|
@@ -1228,20 +1421,30 @@ class StackManager extends React.PureComponent {
|
|
|
1228
1421
|
}
|
|
1229
1422
|
/**
|
|
1230
1423
|
* Determines the parent path for nested routing in React Router 6.
|
|
1231
|
-
*
|
|
1424
|
+
*
|
|
1425
|
+
* When the mount path is known (seeded from UNSAFE_RouteContext), returns
|
|
1426
|
+
* it directly — no iterative discovery needed. The computeParentPath
|
|
1427
|
+
* fallback only runs for root outlets where RouteContext doesn't provide
|
|
1428
|
+
* a parent match.
|
|
1232
1429
|
*/
|
|
1233
1430
|
getParentPath() {
|
|
1234
1431
|
const currentPathname = this.props.routeInfo.pathname;
|
|
1235
|
-
// Prevent out-of-scope outlets from adopting unrelated routes
|
|
1236
|
-
|
|
1432
|
+
// Prevent out-of-scope outlets from adopting unrelated routes.
|
|
1433
|
+
// Uses segment-aware comparison: /tabs-secondary must NOT match /tabs scope.
|
|
1434
|
+
if (this.outletMountPath && !isPathnameInScope(currentPathname, this.outletMountPath)) {
|
|
1237
1435
|
return undefined;
|
|
1238
1436
|
}
|
|
1437
|
+
// Fast path: mount path is known from RouteContext. The parent path IS the
|
|
1438
|
+
// mount path — no need to run the iterative computeParentPath algorithm.
|
|
1439
|
+
if (this.outletMountPath && !this.isRootOutlet) {
|
|
1440
|
+
return this.outletMountPath;
|
|
1441
|
+
}
|
|
1442
|
+
// Fallback: root outlet or mount path not yet seeded. Run the full
|
|
1443
|
+
// computeParentPath algorithm to discover the parent depth.
|
|
1239
1444
|
if (this.ionRouterOutlet) {
|
|
1240
1445
|
const routeChildren = extractRouteChildren(this.ionRouterOutlet.props.children);
|
|
1241
1446
|
const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);
|
|
1242
|
-
|
|
1243
|
-
const needsParentPath = !isRootOutlet || hasRelativeRoutes || hasIndexRoute;
|
|
1244
|
-
if (needsParentPath) {
|
|
1447
|
+
if (!this.isRootOutlet || hasRelativeRoutes || hasIndexRoute) {
|
|
1245
1448
|
const result = computeParentPath({
|
|
1246
1449
|
currentPathname,
|
|
1247
1450
|
outletMountPath: this.outletMountPath,
|
|
@@ -1350,12 +1553,11 @@ class StackManager extends React.PureComponent {
|
|
|
1350
1553
|
*/
|
|
1351
1554
|
handleOutOfContextNestedOutlet(parentPath, leavingViewItem) {
|
|
1352
1555
|
var _a;
|
|
1353
|
-
|
|
1354
|
-
if (isRootOutlet || parentPath !== undefined || !this.ionRouterOutlet) {
|
|
1556
|
+
if (this.isRootOutlet || parentPath !== undefined || !this.ionRouterOutlet) {
|
|
1355
1557
|
return false;
|
|
1356
1558
|
}
|
|
1357
1559
|
const routesChildren = (_a = getRoutesChildren(this.ionRouterOutlet.props.children)) !== null && _a !== void 0 ? _a : this.ionRouterOutlet.props.children;
|
|
1358
|
-
const routeChildren = React.Children.toArray(routesChildren).filter((child) => React.isValidElement(child) && child.type === Route);
|
|
1560
|
+
const routeChildren = React.Children.toArray(routesChildren).filter((child) => React.isValidElement(child) && (child.type === Route || child.type === IonRoute));
|
|
1359
1561
|
const hasRelativeRoutes = routeChildren.some((route) => {
|
|
1360
1562
|
const path = route.props.path;
|
|
1361
1563
|
return path && !path.startsWith('/') && path !== '*';
|
|
@@ -1374,8 +1576,7 @@ class StackManager extends React.PureComponent {
|
|
|
1374
1576
|
* Handles nested outlet with no matching route. Returns true to abort.
|
|
1375
1577
|
*/
|
|
1376
1578
|
handleNoMatchingRoute(enteringRoute, enteringViewItem, leavingViewItem) {
|
|
1377
|
-
|
|
1378
|
-
if (isRootOutlet || enteringRoute || enteringViewItem) {
|
|
1579
|
+
if (this.isRootOutlet || enteringRoute || enteringViewItem) {
|
|
1379
1580
|
return false;
|
|
1380
1581
|
}
|
|
1381
1582
|
hideIonPageElement(leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
|
|
@@ -1397,7 +1598,7 @@ class StackManager extends React.PureComponent {
|
|
|
1397
1598
|
// When entering === leaving, the view is already visible - skip transition to prevent flash
|
|
1398
1599
|
if (enteringViewItem === leavingViewItem) {
|
|
1399
1600
|
if (isParameterizedRoute || isWildcardContainerRoute) {
|
|
1400
|
-
const updatedMatch = matchComponent(enteringViewItem.reactElement, routeInfo.pathname, true);
|
|
1601
|
+
const updatedMatch = matchComponent(enteringViewItem.reactElement, routeInfo.pathname, true, this.outletMountPath);
|
|
1401
1602
|
if (updatedMatch) {
|
|
1402
1603
|
enteringViewItem.routeData.match = updatedMatch;
|
|
1403
1604
|
}
|
|
@@ -1420,7 +1621,7 @@ class StackManager extends React.PureComponent {
|
|
|
1420
1621
|
const currentInContainer = routeInfo.pathname.startsWith(containerBase + '/') || routeInfo.pathname === containerBase;
|
|
1421
1622
|
const previousInContainer = routeInfo.lastPathname.startsWith(containerBase + '/') || routeInfo.lastPathname === containerBase;
|
|
1422
1623
|
if (currentInContainer && previousInContainer) {
|
|
1423
|
-
const updatedMatch = matchComponent(enteringViewItem.reactElement, routeInfo.pathname, true);
|
|
1624
|
+
const updatedMatch = matchComponent(enteringViewItem.reactElement, routeInfo.pathname, true, this.outletMountPath);
|
|
1424
1625
|
if (updatedMatch) {
|
|
1425
1626
|
enteringViewItem.routeData.match = updatedMatch;
|
|
1426
1627
|
}
|
|
@@ -1589,17 +1790,19 @@ class StackManager extends React.PureComponent {
|
|
|
1589
1790
|
* nested scrollbars (each page has its own IonContent). Top-level outlets
|
|
1590
1791
|
* are unaffected and animate normally.
|
|
1591
1792
|
*
|
|
1592
|
-
* Uses inline
|
|
1593
|
-
* beforeTransition() removes ion-page-hidden via setPageHidden().
|
|
1594
|
-
* Inline
|
|
1595
|
-
* until React unmounts it after ionViewDidLeave fires.
|
|
1793
|
+
* Uses inline visibility:hidden rather than ion-page-hidden class because
|
|
1794
|
+
* core's beforeTransition() removes ion-page-hidden via setPageHidden().
|
|
1795
|
+
* Inline visibility:hidden survives that removal, keeping the page hidden
|
|
1796
|
+
* until React unmounts it after ionViewDidLeave fires. Unlike display:none,
|
|
1797
|
+
* visibility:hidden preserves element geometry so commit() animations
|
|
1798
|
+
* can resolve normally.
|
|
1596
1799
|
*/
|
|
1597
1800
|
applySkipAnimationIfNeeded(enteringViewItem, leavingViewItem) {
|
|
1598
1801
|
var _a;
|
|
1599
1802
|
const isNestedOutlet = !!((_a = this.routerOutletElement) === null || _a === void 0 ? void 0 : _a.closest('.ion-page'));
|
|
1600
1803
|
const shouldSkip = isNestedOutlet && !!leavingViewItem && enteringViewItem !== leavingViewItem;
|
|
1601
1804
|
if (shouldSkip && (leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement)) {
|
|
1602
|
-
leavingViewItem.ionPageElement.style.setProperty('
|
|
1805
|
+
leavingViewItem.ionPageElement.style.setProperty('visibility', 'hidden');
|
|
1603
1806
|
leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
|
|
1604
1807
|
}
|
|
1605
1808
|
return shouldSkip;
|
|
@@ -1676,6 +1879,20 @@ class StackManager extends React.PureComponent {
|
|
|
1676
1879
|
}
|
|
1677
1880
|
});
|
|
1678
1881
|
this.forceUpdate();
|
|
1882
|
+
// Safety net: after forceUpdate triggers a React render cycle, check if
|
|
1883
|
+
// any pages in this outlet are stuck with ion-page-invisible. This can
|
|
1884
|
+
// happen when view lookup fails (e.g., wildcard-to-index transitions
|
|
1885
|
+
// where the view item gets corrupted). The forceUpdate above causes
|
|
1886
|
+
// React to render the correct component, but ion-page-invisible may
|
|
1887
|
+
// persist if no transition runs for that page.
|
|
1888
|
+
setTimeout(() => {
|
|
1889
|
+
if (!this._isMounted || !this.routerOutletElement)
|
|
1890
|
+
return;
|
|
1891
|
+
const stuckPages = this.routerOutletElement.querySelectorAll(':scope > .ion-page-invisible');
|
|
1892
|
+
stuckPages.forEach((page) => {
|
|
1893
|
+
page.classList.remove('ion-page-invisible');
|
|
1894
|
+
});
|
|
1895
|
+
}, ION_PAGE_WAIT_TIMEOUT_MS);
|
|
1679
1896
|
}
|
|
1680
1897
|
}, ION_PAGE_WAIT_TIMEOUT_MS);
|
|
1681
1898
|
this.forceUpdate();
|
|
@@ -1691,6 +1908,7 @@ class StackManager extends React.PureComponent {
|
|
|
1691
1908
|
: { pathname: routeInfo.pushedByRoute || '' };
|
|
1692
1909
|
}
|
|
1693
1910
|
componentDidMount() {
|
|
1911
|
+
this._isMounted = true;
|
|
1694
1912
|
if (this.clearOutletTimeout) {
|
|
1695
1913
|
/**
|
|
1696
1914
|
* The clearOutlet integration with React Router is a bit hacky.
|
|
@@ -1721,6 +1939,17 @@ class StackManager extends React.PureComponent {
|
|
|
1721
1939
|
}
|
|
1722
1940
|
}
|
|
1723
1941
|
componentWillUnmount() {
|
|
1942
|
+
this._isMounted = false;
|
|
1943
|
+
// Cancel any in-flight transition rAFs
|
|
1944
|
+
for (const id of this.transitionRafIds) {
|
|
1945
|
+
cancelAnimationFrame(id);
|
|
1946
|
+
}
|
|
1947
|
+
this.transitionRafIds = [];
|
|
1948
|
+
// Disconnect any in-flight MutationObserver from waitForComponentsReady
|
|
1949
|
+
if (this.transitionObserver) {
|
|
1950
|
+
this.transitionObserver.disconnect();
|
|
1951
|
+
this.transitionObserver = undefined;
|
|
1952
|
+
}
|
|
1724
1953
|
if (this.ionPageWaitTimeout) {
|
|
1725
1954
|
clearTimeout(this.ionPageWaitTimeout);
|
|
1726
1955
|
this.ionPageWaitTimeout = undefined;
|
|
@@ -2052,13 +2281,25 @@ class StackManager extends React.PureComponent {
|
|
|
2052
2281
|
enteringEl.classList.add('ion-page-invisible');
|
|
2053
2282
|
}
|
|
2054
2283
|
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2284
|
+
const commitDuration = skipTransition || skipAnimation || directionToUse === undefined ? 0 : undefined;
|
|
2285
|
+
// Race commit against a timeout to recover from hangs
|
|
2286
|
+
const commitPromise = routerOutlet.commit(enteringEl, leavingEl, {
|
|
2287
|
+
duration: commitDuration,
|
|
2057
2288
|
direction: directionToUse,
|
|
2058
2289
|
showGoBack: !!routeInfo.pushedByRoute,
|
|
2059
2290
|
progressAnimation,
|
|
2060
2291
|
animationBuilder: routeInfo.routeAnimation,
|
|
2061
2292
|
});
|
|
2293
|
+
const timeoutMs = 5000;
|
|
2294
|
+
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve('timeout'), timeoutMs));
|
|
2295
|
+
const result = await Promise.race([commitPromise.then(() => 'done'), timeoutPromise]);
|
|
2296
|
+
if (result === 'timeout') {
|
|
2297
|
+
// Force entering page visible even though commit hung
|
|
2298
|
+
enteringEl.classList.remove('ion-page-invisible');
|
|
2299
|
+
}
|
|
2300
|
+
if (!progressAnimation) {
|
|
2301
|
+
enteringEl.classList.remove('ion-page-invisible');
|
|
2302
|
+
}
|
|
2062
2303
|
};
|
|
2063
2304
|
const routerOutlet = this.routerOutletElement;
|
|
2064
2305
|
const routeInfoFallbackDirection = routeInfo.routeDirection === 'none' || routeInfo.routeDirection === 'root' ? undefined : routeInfo.routeDirection;
|
|
@@ -2066,7 +2307,7 @@ class StackManager extends React.PureComponent {
|
|
|
2066
2307
|
if (enteringViewItem && enteringViewItem.ionPageElement && this.routerOutletElement) {
|
|
2067
2308
|
if (leavingViewItem && leavingViewItem.ionPageElement && enteringViewItem === leavingViewItem) {
|
|
2068
2309
|
// Clone page for same-view transitions (e.g., /user/1 → /user/2)
|
|
2069
|
-
const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname);
|
|
2310
|
+
const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname, undefined, this.outletMountPath);
|
|
2070
2311
|
if (match) {
|
|
2071
2312
|
const newLeavingElement = clonePageElement(leavingViewItem.ionPageElement.outerHTML);
|
|
2072
2313
|
if (newLeavingElement) {
|
|
@@ -2087,21 +2328,21 @@ class StackManager extends React.PureComponent {
|
|
|
2087
2328
|
const isNonAnimatedTransition = directionToUse === undefined && !progressAnimation;
|
|
2088
2329
|
if (isNonAnimatedTransition && leavingEl) {
|
|
2089
2330
|
/**
|
|
2090
|
-
*
|
|
2091
|
-
*
|
|
2092
|
-
*
|
|
2093
|
-
*
|
|
2094
|
-
*
|
|
2331
|
+
* Skip commit() for non-animated transitions (like tab switches).
|
|
2332
|
+
* commit() runs animation logic that can cause intermediate paints
|
|
2333
|
+
* even with duration: 0. Instead, swap visibility synchronously.
|
|
2334
|
+
*
|
|
2335
|
+
* Synchronous DOM class changes are batched into a single browser
|
|
2336
|
+
* paint, so there's no gap frame where neither page is visible and
|
|
2337
|
+
* no overlap frame where both pages are visible.
|
|
2095
2338
|
*/
|
|
2096
2339
|
const enteringEl = enteringViewItem.ionPageElement;
|
|
2097
2340
|
// Ensure entering element has proper base classes
|
|
2098
2341
|
enteringEl.classList.add('ion-page');
|
|
2099
|
-
//
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
enteringEl.classList.remove('ion-page-hidden');
|
|
2104
|
-
enteringEl.removeAttribute('aria-hidden');
|
|
2342
|
+
// Clear ALL hidden state from entering element. showIonPageElement
|
|
2343
|
+
// removes visibility:hidden (from applySkipAnimationIfNeeded),
|
|
2344
|
+
// ion-page-hidden, and aria-hidden in one call.
|
|
2345
|
+
showIonPageElement(enteringEl);
|
|
2105
2346
|
// Handle can-go-back class since we're skipping commit() which normally sets this
|
|
2106
2347
|
if (routeInfo.pushedByRoute) {
|
|
2107
2348
|
enteringEl.classList.add('can-go-back');
|
|
@@ -2131,9 +2372,17 @@ class StackManager extends React.PureComponent {
|
|
|
2131
2372
|
if (!resolved && checkReady()) {
|
|
2132
2373
|
resolved = true;
|
|
2133
2374
|
observer.disconnect();
|
|
2375
|
+
if (this.transitionObserver === observer) {
|
|
2376
|
+
this.transitionObserver = undefined;
|
|
2377
|
+
}
|
|
2134
2378
|
resolve();
|
|
2135
2379
|
}
|
|
2136
2380
|
});
|
|
2381
|
+
// Disconnect any previous observer before tracking the new one
|
|
2382
|
+
if (this.transitionObserver) {
|
|
2383
|
+
this.transitionObserver.disconnect();
|
|
2384
|
+
}
|
|
2385
|
+
this.transitionObserver = observer;
|
|
2137
2386
|
observer.observe(enteringEl, {
|
|
2138
2387
|
subtree: true,
|
|
2139
2388
|
attributes: true,
|
|
@@ -2143,28 +2392,25 @@ class StackManager extends React.PureComponent {
|
|
|
2143
2392
|
if (!resolved) {
|
|
2144
2393
|
resolved = true;
|
|
2145
2394
|
observer.disconnect();
|
|
2395
|
+
if (this.transitionObserver === observer) {
|
|
2396
|
+
this.transitionObserver = undefined;
|
|
2397
|
+
}
|
|
2146
2398
|
resolve();
|
|
2147
2399
|
}
|
|
2148
2400
|
}, 100);
|
|
2149
2401
|
});
|
|
2150
2402
|
};
|
|
2151
2403
|
await waitForComponentsReady();
|
|
2152
|
-
//
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
leavingEl.setAttribute('aria-hidden', 'true');
|
|
2160
|
-
resolve();
|
|
2161
|
-
});
|
|
2162
|
-
});
|
|
2163
|
-
});
|
|
2404
|
+
// Bail out if the component unmounted during waitForComponentsReady
|
|
2405
|
+
if (!this._isMounted)
|
|
2406
|
+
return;
|
|
2407
|
+
// Swap visibility synchronously - show entering, hide leaving
|
|
2408
|
+
enteringEl.classList.remove('ion-page-invisible');
|
|
2409
|
+
leavingEl.classList.add('ion-page-hidden');
|
|
2410
|
+
leavingEl.setAttribute('aria-hidden', 'true');
|
|
2164
2411
|
}
|
|
2165
2412
|
else {
|
|
2166
2413
|
await runCommit(enteringViewItem.ionPageElement, leavingEl);
|
|
2167
|
-
// For animated transitions, hide leaving element after commit completes
|
|
2168
2414
|
if (leavingEl && !progressAnimation) {
|
|
2169
2415
|
leavingEl.classList.add('ion-page-hidden');
|
|
2170
2416
|
leavingEl.setAttribute('aria-hidden', 'true');
|
|
@@ -2178,33 +2424,89 @@ class StackManager extends React.PureComponent {
|
|
|
2178
2424
|
const ionRouterOutlet = React.Children.only(children);
|
|
2179
2425
|
// Store reference for use in getParentPath() and handlePageTransition()
|
|
2180
2426
|
this.ionRouterOutlet = ionRouterOutlet;
|
|
2181
|
-
|
|
2182
|
-
//
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2427
|
+
return (React.createElement(UNSAFE_RouteContext.Consumer, null, (parentContext) => {
|
|
2428
|
+
// Derive the outlet's mount path from React Router's matched route context.
|
|
2429
|
+
// This eliminates the need for heuristic-based mount path discovery in
|
|
2430
|
+
// computeParentPath, since React Router already knows the matched base path.
|
|
2431
|
+
const parentMatches = parentContext === null || parentContext === void 0 ? void 0 : parentContext.matches;
|
|
2432
|
+
const parentPathnameBase = parentMatches && parentMatches.length > 0
|
|
2433
|
+
? parentMatches[parentMatches.length - 1].pathnameBase
|
|
2434
|
+
: undefined;
|
|
2435
|
+
// Derive isRootOutlet from RouteContext: empty matches means root.
|
|
2436
|
+
this.isRootOutlet = !parentMatches || parentMatches.length === 0;
|
|
2437
|
+
// Seed StackManager's mount path from the parent route context
|
|
2438
|
+
if (parentPathnameBase && !this.outletMountPath) {
|
|
2439
|
+
this.outletMountPath = parentPathnameBase;
|
|
2440
|
+
}
|
|
2441
|
+
const components = this.context.getChildrenToRender(this.id, this.ionRouterOutlet, this.props.routeInfo, () => {
|
|
2442
|
+
// Callback triggers re-render when view items are modified during getChildrenToRender
|
|
2443
|
+
this.forceUpdate();
|
|
2444
|
+
}, parentPathnameBase);
|
|
2445
|
+
return (React.createElement(StackContext.Provider, { value: this.stackContextValue }, React.cloneElement(ionRouterOutlet, {
|
|
2446
|
+
ref: (node) => {
|
|
2447
|
+
if (ionRouterOutlet.props.setRef) {
|
|
2448
|
+
// Needed to handle external refs from devs.
|
|
2449
|
+
ionRouterOutlet.props.setRef(node);
|
|
2450
|
+
}
|
|
2451
|
+
if (ionRouterOutlet.props.forwardedRef) {
|
|
2452
|
+
// Needed to handle external refs from devs.
|
|
2453
|
+
ionRouterOutlet.props.forwardedRef.current = node;
|
|
2454
|
+
}
|
|
2455
|
+
this.routerOutletElement = node;
|
|
2456
|
+
const { ref } = ionRouterOutlet;
|
|
2457
|
+
// Check for legacy refs.
|
|
2458
|
+
if (typeof ref === 'function') {
|
|
2459
|
+
ref(node);
|
|
2460
|
+
}
|
|
2461
|
+
},
|
|
2462
|
+
}, components)));
|
|
2463
|
+
}));
|
|
2203
2464
|
}
|
|
2204
2465
|
static get contextType() {
|
|
2205
2466
|
return RouteManagerContext;
|
|
2206
2467
|
}
|
|
2207
2468
|
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Converts React Route elements to RouteObject format for use with matchRoutes().
|
|
2471
|
+
* Filters out pathless routes (which are handled by fallback logic separately).
|
|
2472
|
+
*
|
|
2473
|
+
* When a basename is provided, absolute route paths are relativized by stripping
|
|
2474
|
+
* the basename prefix. This is necessary because matchRoutes() strips the basename
|
|
2475
|
+
* from the LOCATION pathname but not from route paths — absolute paths must be
|
|
2476
|
+
* made relative to the basename for matching to work correctly.
|
|
2477
|
+
*
|
|
2478
|
+
* @param routeChildren The flat array of Route/IonRoute elements from the outlet.
|
|
2479
|
+
* @param basename The resolved parent path (without trailing slash or `/*`) used to relativize absolute paths.
|
|
2480
|
+
*/
|
|
2481
|
+
function routeElementsToRouteObjects(routeChildren, basename) {
|
|
2482
|
+
return routeChildren
|
|
2483
|
+
.filter((child) => child.props.path != null || child.props.index)
|
|
2484
|
+
.map((child) => {
|
|
2485
|
+
const handle = { _element: child };
|
|
2486
|
+
let path = child.props.path;
|
|
2487
|
+
// Relativize absolute paths by stripping the basename prefix
|
|
2488
|
+
if (path && path.startsWith('/') && basename) {
|
|
2489
|
+
if (path === basename) {
|
|
2490
|
+
path = '';
|
|
2491
|
+
}
|
|
2492
|
+
else if (path.startsWith(basename + '/')) {
|
|
2493
|
+
path = path.slice(basename.length + 1);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
if (child.props.index) {
|
|
2497
|
+
return {
|
|
2498
|
+
index: true,
|
|
2499
|
+
handle,
|
|
2500
|
+
caseSensitive: child.props.caseSensitive || undefined,
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
return {
|
|
2504
|
+
path,
|
|
2505
|
+
handle,
|
|
2506
|
+
caseSensitive: child.props.caseSensitive || undefined,
|
|
2507
|
+
};
|
|
2508
|
+
});
|
|
2509
|
+
}
|
|
2208
2510
|
/**
|
|
2209
2511
|
* Finds the `<Route />` node matching the current route info.
|
|
2210
2512
|
* If no `<Route />` can be matched, a fallback node is returned.
|
|
@@ -2215,137 +2517,62 @@ class StackManager extends React.PureComponent {
|
|
|
2215
2517
|
* @param parentPath The parent path that was matched by the parent outlet (for nested routing)
|
|
2216
2518
|
*/
|
|
2217
2519
|
function findRouteByRouteInfo(node, routeInfo, parentPath) {
|
|
2218
|
-
var _a;
|
|
2520
|
+
var _a, _b, _c;
|
|
2219
2521
|
let matchedNode;
|
|
2220
2522
|
let fallbackNode;
|
|
2221
2523
|
// `<Route />` nodes are rendered inside of a <Routes /> node
|
|
2222
2524
|
const routesChildren = (_a = getRoutesChildren(node)) !== null && _a !== void 0 ? _a : node;
|
|
2223
2525
|
// Collect all route children
|
|
2224
|
-
const routeChildren = React.Children.toArray(routesChildren).filter((child) => React.isValidElement(child) && child.type === Route);
|
|
2225
|
-
//
|
|
2226
|
-
const
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
return -1;
|
|
2239
|
-
if (aIsWildcardOnly && !bIsWildcardOnly)
|
|
2240
|
-
return 1;
|
|
2241
|
-
// Exact matches (no wildcards/params) come before wildcard/param routes
|
|
2242
|
-
const aHasWildcard = pathA.includes('*') || pathA.includes(':');
|
|
2243
|
-
const bHasWildcard = pathB.includes('*') || pathB.includes(':');
|
|
2244
|
-
if (!aHasWildcard && bHasWildcard)
|
|
2245
|
-
return -1;
|
|
2246
|
-
if (aHasWildcard && !bHasWildcard)
|
|
2247
|
-
return 1;
|
|
2248
|
-
// Among routes with same wildcard status, longer paths are more specific
|
|
2249
|
-
if (pathA.length !== pathB.length) {
|
|
2250
|
-
return pathB.length - pathA.length;
|
|
2251
|
-
}
|
|
2252
|
-
return 0;
|
|
2253
|
-
});
|
|
2254
|
-
// For nested routes in React Router 6, we need to extract the relative path
|
|
2255
|
-
// that this outlet should be responsible for matching
|
|
2256
|
-
const originalPathname = routeInfo.pathname;
|
|
2257
|
-
let relativePathnameToMatch = routeInfo.pathname;
|
|
2258
|
-
// Check if we have relative routes (routes that don't start with '/')
|
|
2259
|
-
const hasRelativeRoutes = sortedRoutes.some((r) => r.props.path && !r.props.path.startsWith('/'));
|
|
2260
|
-
const hasIndexRoute = sortedRoutes.some((r) => r.props.index);
|
|
2261
|
-
// SIMPLIFIED: Trust React Router 6's matching more, compute relative path when parent is known
|
|
2262
|
-
if ((hasRelativeRoutes || hasIndexRoute) && parentPath) {
|
|
2263
|
-
const parentPrefix = parentPath.replace('/*', '');
|
|
2264
|
-
// Normalize both paths to start with '/' for consistent comparison
|
|
2265
|
-
const normalizedParent = stripTrailingSlash(parentPrefix.startsWith('/') ? parentPrefix : `/${parentPrefix}`);
|
|
2266
|
-
const normalizedPathname = stripTrailingSlash(routeInfo.pathname);
|
|
2267
|
-
// Only compute relative path if pathname is within parent scope
|
|
2268
|
-
if (normalizedPathname.startsWith(normalizedParent + '/') || normalizedPathname === normalizedParent) {
|
|
2269
|
-
const pathSegments = routeInfo.pathname.split('/').filter(Boolean);
|
|
2270
|
-
const parentSegments = normalizedParent.split('/').filter(Boolean);
|
|
2271
|
-
const relativeSegments = pathSegments.slice(parentSegments.length);
|
|
2272
|
-
relativePathnameToMatch = relativeSegments.join('/'); // Empty string is valid for index routes
|
|
2273
|
-
}
|
|
2274
|
-
}
|
|
2275
|
-
// Find the first matching route
|
|
2276
|
-
for (const child of sortedRoutes) {
|
|
2277
|
-
const childPath = child.props.path;
|
|
2278
|
-
const isAbsoluteRoute = childPath && childPath.startsWith('/');
|
|
2279
|
-
// Determine which pathname to match against:
|
|
2280
|
-
// - For absolute routes: use the original full pathname
|
|
2281
|
-
// - For relative routes with a parent: use the computed relative pathname
|
|
2282
|
-
// - For relative routes at root level (no parent): use the original pathname
|
|
2283
|
-
// (matchPath will handle the relative-to-absolute normalization)
|
|
2284
|
-
const pathnameToMatch = isAbsoluteRoute ? originalPathname : relativePathnameToMatch;
|
|
2285
|
-
// Determine the path portion to match:
|
|
2286
|
-
// - For absolute routes: use derivePathnameToMatch
|
|
2287
|
-
// - For relative routes at root level (no parent): use original pathname
|
|
2288
|
-
// directly since matchPath normalizes both path and pathname
|
|
2289
|
-
// - For relative routes with parent: use derivePathnameToMatch for wildcards,
|
|
2290
|
-
// or the computed relative pathname for non-wildcards
|
|
2291
|
-
let pathForMatch;
|
|
2292
|
-
if (isAbsoluteRoute) {
|
|
2293
|
-
pathForMatch = derivePathnameToMatch(pathnameToMatch, childPath);
|
|
2294
|
-
}
|
|
2295
|
-
else if (!parentPath && childPath) {
|
|
2296
|
-
// Root-level relative route: use the full pathname and let matchPath
|
|
2297
|
-
// handle the normalization (it adds '/' to both path and pathname)
|
|
2298
|
-
pathForMatch = originalPathname;
|
|
2299
|
-
}
|
|
2300
|
-
else if (childPath && childPath.includes('*')) {
|
|
2301
|
-
// Relative wildcard route with parent path: use derivePathnameToMatch
|
|
2302
|
-
pathForMatch = derivePathnameToMatch(pathnameToMatch, childPath);
|
|
2526
|
+
const routeChildren = React.Children.toArray(routesChildren).filter((child) => React.isValidElement(child) && (child.type === Route || child.type === IonRoute));
|
|
2527
|
+
// Delegate route matching to RR6's matchRoutes(), which handles specificity ranking internally.
|
|
2528
|
+
const basename = parentPath ? stripTrailingSlash(parentPath.replace('/*', '')) : undefined;
|
|
2529
|
+
const routeObjects = routeElementsToRouteObjects(routeChildren, basename);
|
|
2530
|
+
const matches = matchRoutes(routeObjects, { pathname: routeInfo.pathname }, basename);
|
|
2531
|
+
if (matches && matches.length > 0) {
|
|
2532
|
+
const bestMatch = matches[matches.length - 1];
|
|
2533
|
+
matchedNode = (_c = (_b = bestMatch.route.handle) === null || _b === void 0 ? void 0 : _b._element) !== null && _c !== void 0 ? _c : undefined;
|
|
2534
|
+
}
|
|
2535
|
+
// Fallback: try pathless routes, but only if pathname is within scope.
|
|
2536
|
+
if (!matchedNode) {
|
|
2537
|
+
let pathnameInScope = true;
|
|
2538
|
+
if (parentPath) {
|
|
2539
|
+
pathnameInScope = isPathnameInScope(routeInfo.pathname, parentPath);
|
|
2303
2540
|
}
|
|
2304
2541
|
else {
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
break;
|
|
2314
|
-
}
|
|
2315
|
-
}
|
|
2316
|
-
if (matchedNode) {
|
|
2317
|
-
return matchedNode;
|
|
2318
|
-
}
|
|
2319
|
-
// If we haven't found a node, try to find one that doesn't have a path prop (fallback route)
|
|
2320
|
-
// BUT only return the fallback if the current pathname is within the outlet's scope.
|
|
2321
|
-
// For outlets with absolute paths, compute the common prefix to determine scope.
|
|
2322
|
-
const absolutePathRoutes = routeChildren.filter((r) => r.props.path && r.props.path.startsWith('/'));
|
|
2323
|
-
// Determine if pathname is within scope before returning fallback
|
|
2324
|
-
let isPathnameInScope = true;
|
|
2325
|
-
if (absolutePathRoutes.length > 0) {
|
|
2326
|
-
// Find common prefix of all absolute paths to determine outlet scope
|
|
2327
|
-
const absolutePaths = absolutePathRoutes.map((r) => r.props.path);
|
|
2328
|
-
const commonPrefix = computeCommonPrefix(absolutePaths);
|
|
2329
|
-
// If we have a common prefix, check if the current pathname is within that scope
|
|
2330
|
-
if (commonPrefix && commonPrefix !== '/') {
|
|
2331
|
-
isPathnameInScope = routeInfo.pathname.startsWith(commonPrefix);
|
|
2542
|
+
const absolutePathRoutes = routeChildren.filter((r) => r.props.path && r.props.path.startsWith('/'));
|
|
2543
|
+
if (absolutePathRoutes.length > 0) {
|
|
2544
|
+
const absolutePaths = absolutePathRoutes.map((r) => r.props.path);
|
|
2545
|
+
const commonPrefix = computeCommonPrefix(absolutePaths);
|
|
2546
|
+
if (commonPrefix && commonPrefix !== '/') {
|
|
2547
|
+
pathnameInScope = routeInfo.pathname.startsWith(commonPrefix);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2332
2550
|
}
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
break;
|
|
2551
|
+
if (pathnameInScope) {
|
|
2552
|
+
for (const child of routeChildren) {
|
|
2553
|
+
if (!child.props.path) {
|
|
2554
|
+
fallbackNode = child;
|
|
2555
|
+
break;
|
|
2556
|
+
}
|
|
2340
2557
|
}
|
|
2341
2558
|
}
|
|
2342
2559
|
}
|
|
2343
2560
|
return matchedNode !== null && matchedNode !== void 0 ? matchedNode : fallbackNode;
|
|
2344
2561
|
}
|
|
2345
|
-
function matchComponent(node, pathname, forceExact) {
|
|
2562
|
+
function matchComponent(node, pathname, forceExact, parentPath) {
|
|
2346
2563
|
var _a;
|
|
2347
2564
|
const routePath = (_a = node === null || node === void 0 ? void 0 : node.props) === null || _a === void 0 ? void 0 : _a.path;
|
|
2348
|
-
|
|
2565
|
+
let pathnameToMatch;
|
|
2566
|
+
if (parentPath && routePath && !routePath.startsWith('/')) {
|
|
2567
|
+
// When parent path is known, compute exact relative pathname
|
|
2568
|
+
const relative = pathname.startsWith(parentPath)
|
|
2569
|
+
? pathname.slice(parentPath.length).replace(/^\//, '')
|
|
2570
|
+
: pathname;
|
|
2571
|
+
pathnameToMatch = relative;
|
|
2572
|
+
}
|
|
2573
|
+
else {
|
|
2574
|
+
pathnameToMatch = derivePathnameToMatch(pathname, routePath);
|
|
2575
|
+
}
|
|
2349
2576
|
return matchPath({
|
|
2350
2577
|
pathname: pathnameToMatch,
|
|
2351
2578
|
componentProps: Object.assign(Object.assign({}, node.props), { end: forceExact }),
|
|
@@ -2426,6 +2653,16 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
2426
2653
|
// for future navigations once React has committed the mount. This avoids
|
|
2427
2654
|
// duplicate entries when React StrictMode runs an extra render pre-commit.
|
|
2428
2655
|
locationHistory.current.add(routeInfo);
|
|
2656
|
+
// If IonTabBar already called handleSetCurrentTab during render (before this
|
|
2657
|
+
// effect), the tab was stored in currentTab.current but the history entry was
|
|
2658
|
+
// not yet seeded. Apply the pending tab to the seed entry now.
|
|
2659
|
+
if (currentTab.current) {
|
|
2660
|
+
const ri = Object.assign({}, locationHistory.current.current());
|
|
2661
|
+
if (ri.tab !== currentTab.current) {
|
|
2662
|
+
ri.tab = currentTab.current;
|
|
2663
|
+
locationHistory.current.update(ri);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2429
2666
|
registerHistoryListener(handleHistoryChange);
|
|
2430
2667
|
didMountRef.current = true;
|
|
2431
2668
|
}, []);
|
|
@@ -2484,15 +2721,19 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
2484
2721
|
leavingLocationInfo = locationHistory.current.current();
|
|
2485
2722
|
}
|
|
2486
2723
|
const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
|
|
2487
|
-
if (leavingUrl !== location.pathname) {
|
|
2724
|
+
if (leavingUrl !== location.pathname + location.search) {
|
|
2488
2725
|
if (!incomingRouteParams.current) {
|
|
2489
|
-
//
|
|
2490
|
-
//
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
if (!
|
|
2495
|
-
currentTab.current
|
|
2726
|
+
// Use history-based tab detection instead of URL-pattern heuristics,
|
|
2727
|
+
// so tab routes work with any URL structure (not just paths containing "/tabs").
|
|
2728
|
+
// Fall back to currentTab.current only when the destination is within the
|
|
2729
|
+
// current tab's path hierarchy (prevents non-tab routes from inheriting a tab).
|
|
2730
|
+
let tabToUse = locationHistory.current.findTabForPathname(location.pathname);
|
|
2731
|
+
if (!tabToUse && currentTab.current) {
|
|
2732
|
+
const tabFirstRoute = locationHistory.current.getFirstRouteInfoForTab(currentTab.current);
|
|
2733
|
+
const tabRootPath = tabFirstRoute === null || tabFirstRoute === void 0 ? void 0 : tabFirstRoute.pathname;
|
|
2734
|
+
if (tabRootPath && (location.pathname === tabRootPath || location.pathname.startsWith(tabRootPath + '/'))) {
|
|
2735
|
+
tabToUse = currentTab.current;
|
|
2736
|
+
}
|
|
2496
2737
|
}
|
|
2497
2738
|
/**
|
|
2498
2739
|
* A `REPLACE` action can be triggered by React Router's
|
|
@@ -2532,6 +2773,8 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
2532
2773
|
}
|
|
2533
2774
|
else {
|
|
2534
2775
|
// It's a non-linear history path like a direct link.
|
|
2776
|
+
// Still push the current location key so browser forward is detectable.
|
|
2777
|
+
forwardStack.current.push(currentLocationKeyRef.current);
|
|
2535
2778
|
incomingRouteParams.current = {
|
|
2536
2779
|
routeAction: 'pop',
|
|
2537
2780
|
routeDirection: 'none',
|
|
@@ -2556,7 +2799,7 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
2556
2799
|
}
|
|
2557
2800
|
let routeInfo;
|
|
2558
2801
|
// If we're navigating away from tabs to a non-tab route, clear the current tab
|
|
2559
|
-
if (
|
|
2802
|
+
if (!locationHistory.current.findTabForPathname(location.pathname) && currentTab.current) {
|
|
2560
2803
|
currentTab.current = undefined;
|
|
2561
2804
|
}
|
|
2562
2805
|
/**
|
|
@@ -2711,7 +2954,13 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
2711
2954
|
*/
|
|
2712
2955
|
const handleSetCurrentTab = (tab) => {
|
|
2713
2956
|
currentTab.current = tab;
|
|
2714
|
-
const
|
|
2957
|
+
const current = locationHistory.current.current();
|
|
2958
|
+
if (!current) {
|
|
2959
|
+
// locationHistory not yet seeded (e.g., called during initial render
|
|
2960
|
+
// before mount effect). The mount effect will seed the correct entry.
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
const ri = Object.assign({}, current);
|
|
2715
2964
|
if (ri.tab !== tab) {
|
|
2716
2965
|
ri.tab = tab;
|
|
2717
2966
|
locationHistory.current.update(ri);
|
|
@@ -2755,7 +3004,7 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
2755
3004
|
* e.g., `/home` → `/settings` → back to `/home`
|
|
2756
3005
|
*/
|
|
2757
3006
|
const condition1 = routeInfo.lastPathname === routeInfo.pushedByRoute;
|
|
2758
|
-
const condition2 = prevInfo.pathname === routeInfo.pushedByRoute && routeInfo.tab
|
|
3007
|
+
const condition2 = prevInfo.pathname === routeInfo.pushedByRoute && !routeInfo.tab && !prevInfo.tab;
|
|
2759
3008
|
if (condition1 || condition2) {
|
|
2760
3009
|
// Record the current location key so browser forward is detectable
|
|
2761
3010
|
forwardStack.current.push(currentLocationKeyRef.current);
|
|
@@ -2809,26 +3058,29 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
2809
3058
|
// is also within tabs. If not, we should clear the tab context.
|
|
2810
3059
|
let navigationTab = tab;
|
|
2811
3060
|
// If no explicit tab is provided and we're in a tab context,
|
|
2812
|
-
// check if the destination path is outside of the current tab context
|
|
3061
|
+
// check if the destination path is outside of the current tab context.
|
|
3062
|
+
// Uses history-based tab detection instead of URL pattern matching,
|
|
3063
|
+
// so it works with any tab URL structure.
|
|
2813
3064
|
if (!tab && currentTab.current && path) {
|
|
2814
|
-
//
|
|
2815
|
-
const
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
if (
|
|
2826
|
-
|
|
2827
|
-
navigationTab =
|
|
3065
|
+
// Check if destination was previously visited in a tab context
|
|
3066
|
+
const destinationTab = locationHistory.current.findTabForPathname(path);
|
|
3067
|
+
if (destinationTab) {
|
|
3068
|
+
// Previously visited as a tab route - use the known tab
|
|
3069
|
+
navigationTab = destinationTab;
|
|
3070
|
+
}
|
|
3071
|
+
else {
|
|
3072
|
+
// New destination - check if it's a child of the current tab's root path
|
|
3073
|
+
const tabFirstRoute = locationHistory.current.getFirstRouteInfoForTab(currentTab.current);
|
|
3074
|
+
if (tabFirstRoute) {
|
|
3075
|
+
const tabRootPath = tabFirstRoute.pathname;
|
|
3076
|
+
if (path === tabRootPath || path.startsWith(tabRootPath + '/')) {
|
|
3077
|
+
// Still within the current tab's path hierarchy
|
|
3078
|
+
navigationTab = currentTab.current;
|
|
2828
3079
|
}
|
|
2829
3080
|
else {
|
|
2830
|
-
//
|
|
2831
|
-
|
|
3081
|
+
// Destination is outside the current tab context
|
|
3082
|
+
currentTab.current = undefined;
|
|
3083
|
+
navigationTab = undefined;
|
|
2832
3084
|
}
|
|
2833
3085
|
}
|
|
2834
3086
|
}
|
|
@@ -2914,9 +3166,9 @@ const RouterContent$1 = ({ children }) => {
|
|
|
2914
3166
|
const location = useLocation$1();
|
|
2915
3167
|
const navigationType = useNavigationType$1();
|
|
2916
3168
|
const historyListenHandler = useRef();
|
|
2917
|
-
const registerHistoryListener = (cb) => {
|
|
3169
|
+
const registerHistoryListener = useCallback((cb) => {
|
|
2918
3170
|
historyListenHandler.current = cb;
|
|
2919
|
-
};
|
|
3171
|
+
}, []);
|
|
2920
3172
|
/**
|
|
2921
3173
|
* Processes navigation changes within the application.
|
|
2922
3174
|
*
|
|
@@ -2929,14 +3181,14 @@ const RouterContent$1 = ({ children }) => {
|
|
|
2929
3181
|
* @param action The type of navigation action ('PUSH', 'POP', or
|
|
2930
3182
|
* 'REPLACE').
|
|
2931
3183
|
*/
|
|
2932
|
-
const handleHistoryChange = (
|
|
3184
|
+
const handleHistoryChange = useCallback((loc, act) => {
|
|
2933
3185
|
if (historyListenHandler.current) {
|
|
2934
|
-
historyListenHandler.current(
|
|
3186
|
+
historyListenHandler.current(loc, act);
|
|
2935
3187
|
}
|
|
2936
|
-
};
|
|
3188
|
+
}, []);
|
|
2937
3189
|
useEffect(() => {
|
|
2938
3190
|
handleHistoryChange(location, navigationType);
|
|
2939
|
-
}, [location, navigationType]);
|
|
3191
|
+
}, [location, navigationType, handleHistoryChange]);
|
|
2940
3192
|
return React.createElement(IonRouter, { registerHistoryListener: registerHistoryListener }, children);
|
|
2941
3193
|
};
|
|
2942
3194
|
const IonReactMemoryRouter = (_a) => {
|
|
@@ -2953,9 +3205,9 @@ const RouterContent = ({ children }) => {
|
|
|
2953
3205
|
const location = useLocation();
|
|
2954
3206
|
const navigationType = useNavigationType();
|
|
2955
3207
|
const historyListenHandler = useRef();
|
|
2956
|
-
const registerHistoryListener = (cb) => {
|
|
3208
|
+
const registerHistoryListener = useCallback((cb) => {
|
|
2957
3209
|
historyListenHandler.current = cb;
|
|
2958
|
-
};
|
|
3210
|
+
}, []);
|
|
2959
3211
|
/**
|
|
2960
3212
|
* Processes navigation changes within the application.
|
|
2961
3213
|
*
|
|
@@ -2968,14 +3220,14 @@ const RouterContent = ({ children }) => {
|
|
|
2968
3220
|
* @param action The type of navigation action ('PUSH', 'POP', or
|
|
2969
3221
|
* 'REPLACE').
|
|
2970
3222
|
*/
|
|
2971
|
-
const handleHistoryChange = (
|
|
3223
|
+
const handleHistoryChange = useCallback((loc, act) => {
|
|
2972
3224
|
if (historyListenHandler.current) {
|
|
2973
|
-
historyListenHandler.current(
|
|
3225
|
+
historyListenHandler.current(loc, act);
|
|
2974
3226
|
}
|
|
2975
|
-
};
|
|
3227
|
+
}, []);
|
|
2976
3228
|
useEffect(() => {
|
|
2977
3229
|
handleHistoryChange(location, navigationType);
|
|
2978
|
-
}, [location, navigationType]);
|
|
3230
|
+
}, [location, navigationType, handleHistoryChange]);
|
|
2979
3231
|
return React.createElement(IonRouter, { registerHistoryListener: registerHistoryListener }, children);
|
|
2980
3232
|
};
|
|
2981
3233
|
const IonReactHashRouter = (_a) => {
|