@ionic/react-router 8.7.13-dev.11765921002.107104c2 → 8.7.13-dev.11766069240.1ab3dde2
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 +248 -176
- package/dist/index.js.map +1 -1
- package/dist/types/ReactRouter/ReactRouterViewStack.d.ts +0 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { __rest } from 'tslib';
|
|
2
2
|
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
|
3
3
|
import { Route, matchPath as matchPath$1, Routes, Navigate, UNSAFE_RouteContext, useLocation, useNavigate, BrowserRouter, useNavigationType, HashRouter } from 'react-router-dom';
|
|
4
|
-
import { ViewStacks, IonRoute, ViewLifeCycleManager,
|
|
4
|
+
import { ViewStacks, generateId, IonRoute, 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
7
|
const IonRouteInner = ({ path, element }) => {
|
|
@@ -212,6 +212,77 @@ const analyzeRouteChildren = (routeChildren) => {
|
|
|
212
212
|
});
|
|
213
213
|
return { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute, routeChildren };
|
|
214
214
|
};
|
|
215
|
+
/**
|
|
216
|
+
* Checks if any route matches as a specific (non-wildcard, non-index) route.
|
|
217
|
+
*/
|
|
218
|
+
const findSpecificMatch = (routeChildren, remainingPath) => {
|
|
219
|
+
return routeChildren.some((route) => isSpecificRouteMatch(route, remainingPath) || matchesEmbeddedWildcardRoute(route, remainingPath));
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* Checks if any specific route could plausibly match the remaining path.
|
|
223
|
+
* Used to determine if we should fall back to a wildcard match.
|
|
224
|
+
*/
|
|
225
|
+
const couldSpecificRouteMatch = (routeChildren, remainingPath) => {
|
|
226
|
+
const remainingFirstSegment = remainingPath.split('/')[0];
|
|
227
|
+
return routeChildren.some((route) => {
|
|
228
|
+
const routePath = route.props.path;
|
|
229
|
+
if (!routePath || routePath === '*' || routePath === '/*')
|
|
230
|
+
return false;
|
|
231
|
+
if (route.props.index)
|
|
232
|
+
return false;
|
|
233
|
+
const routeFirstSegment = routePath.split('/')[0].replace(/[*:]/g, '');
|
|
234
|
+
if (!routeFirstSegment)
|
|
235
|
+
return false;
|
|
236
|
+
// Check for prefix overlap (either direction)
|
|
237
|
+
return (routeFirstSegment.startsWith(remainingFirstSegment.slice(0, 3)) ||
|
|
238
|
+
remainingFirstSegment.startsWith(routeFirstSegment.slice(0, 3)));
|
|
239
|
+
});
|
|
240
|
+
};
|
|
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
|
+
/**
|
|
257
|
+
* Determines the best parent path from the available matches.
|
|
258
|
+
* Priority: specific > wildcard > index
|
|
259
|
+
*/
|
|
260
|
+
const selectBestMatch = (specificMatch, wildcardMatch, indexMatch) => {
|
|
261
|
+
var _a;
|
|
262
|
+
return (_a = specificMatch !== null && specificMatch !== void 0 ? specificMatch : wildcardMatch) !== null && _a !== void 0 ? _a : indexMatch;
|
|
263
|
+
};
|
|
264
|
+
/**
|
|
265
|
+
* Handles outlets with only absolute routes by computing their common prefix.
|
|
266
|
+
*/
|
|
267
|
+
const computeAbsoluteRoutesParentPath = (routeChildren, currentPathname, outletMountPath) => {
|
|
268
|
+
const absolutePathRoutes = routeChildren.filter((route) => {
|
|
269
|
+
const path = route.props.path;
|
|
270
|
+
return path && path.startsWith('/');
|
|
271
|
+
});
|
|
272
|
+
if (absolutePathRoutes.length === 0) {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
const absolutePaths = absolutePathRoutes.map((r) => r.props.path);
|
|
276
|
+
const commonPrefix = computeCommonPrefix(absolutePaths);
|
|
277
|
+
if (!commonPrefix || commonPrefix === '/') {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
const newOutletMountPath = outletMountPath || commonPrefix;
|
|
281
|
+
if (!currentPathname.startsWith(commonPrefix)) {
|
|
282
|
+
return { parentPath: undefined, outletMountPath: newOutletMountPath };
|
|
283
|
+
}
|
|
284
|
+
return { parentPath: commonPrefix, outletMountPath: newOutletMountPath };
|
|
285
|
+
};
|
|
215
286
|
/**
|
|
216
287
|
* Computes the parent path for a nested outlet based on the current pathname
|
|
217
288
|
* and the outlet's route configuration.
|
|
@@ -224,129 +295,59 @@ const analyzeRouteChildren = (routeChildren) => {
|
|
|
224
295
|
*/
|
|
225
296
|
const computeParentPath = (options) => {
|
|
226
297
|
const { currentPathname, outletMountPath, routeChildren, hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = options;
|
|
227
|
-
// If
|
|
228
|
-
// pathname is outside of that scope, do not attempt to re-compute a new
|
|
229
|
-
// parent path.
|
|
298
|
+
// If pathname is outside the established mount path scope, skip computation
|
|
230
299
|
if (outletMountPath && !currentPathname.startsWith(outletMountPath)) {
|
|
231
300
|
return { parentPath: undefined, outletMountPath };
|
|
232
301
|
}
|
|
233
302
|
if ((hasRelativeRoutes || hasIndexRoute) && currentPathname.includes('/')) {
|
|
234
303
|
const segments = currentPathname.split('/').filter(Boolean);
|
|
235
304
|
if (segments.length >= 1) {
|
|
236
|
-
|
|
237
|
-
let
|
|
238
|
-
let
|
|
239
|
-
|
|
240
|
-
// Start at i = 1 (normal case: strip at least one segment for parent path)
|
|
305
|
+
let firstSpecificMatch;
|
|
306
|
+
let firstWildcardMatch;
|
|
307
|
+
let indexMatchAtMount;
|
|
308
|
+
// Iterate through path segments to find the shortest matching parent path
|
|
241
309
|
for (let i = 1; i <= segments.length; i++) {
|
|
242
310
|
const parentPath = '/' + segments.slice(0, i).join('/');
|
|
243
311
|
const remainingPath = segments.slice(i).join('/');
|
|
244
|
-
// Check for specific route
|
|
245
|
-
|
|
246
|
-
const hasSpecificMatch = routeChildren.some((route) => isSpecificRouteMatch(route, remainingPath) || matchesEmbeddedWildcardRoute(route, remainingPath));
|
|
247
|
-
if (hasSpecificMatch && !firstSpecificMatch) {
|
|
312
|
+
// Check for specific route match (highest priority)
|
|
313
|
+
if (!firstSpecificMatch && findSpecificMatch(routeChildren, remainingPath)) {
|
|
248
314
|
firstSpecificMatch = parentPath;
|
|
249
|
-
// Found a specific match - this is our answer for non-index routes
|
|
250
315
|
break;
|
|
251
316
|
}
|
|
252
|
-
// Check
|
|
253
|
-
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
const remainingFirstSegment = remainingPath.split('/')[0];
|
|
257
|
-
const couldAnyRouteMatch = routeChildren.some((route) => {
|
|
258
|
-
const routePath = route.props.path;
|
|
259
|
-
if (!routePath || routePath === '*' || routePath === '/*')
|
|
260
|
-
return false;
|
|
261
|
-
if (route.props.index)
|
|
262
|
-
return false;
|
|
263
|
-
const routeFirstSegment = routePath.split('/')[0].replace(/[*:]/g, '');
|
|
264
|
-
if (!routeFirstSegment)
|
|
265
|
-
return false;
|
|
266
|
-
// Check for prefix overlap (either direction)
|
|
267
|
-
return (routeFirstSegment.startsWith(remainingFirstSegment.slice(0, 3)) ||
|
|
268
|
-
remainingFirstSegment.startsWith(routeFirstSegment.slice(0, 3)));
|
|
269
|
-
});
|
|
270
|
-
// Only save wildcard match if no specific route could match
|
|
271
|
-
if (!couldAnyRouteMatch) {
|
|
317
|
+
// Check for wildcard match (only if remaining path is non-empty)
|
|
318
|
+
const hasNonEmptyRemaining = remainingPath !== '' && remainingPath !== '/';
|
|
319
|
+
if (!firstWildcardMatch && hasNonEmptyRemaining && hasWildcardRoute) {
|
|
320
|
+
if (!couldSpecificRouteMatch(routeChildren, remainingPath)) {
|
|
272
321
|
firstWildcardMatch = parentPath;
|
|
273
|
-
// Continue looking - might find a specific match at a longer path
|
|
274
322
|
}
|
|
275
323
|
}
|
|
276
|
-
// Check for index route match
|
|
277
|
-
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
// If we already have an outletMountPath, index should only match there
|
|
281
|
-
if (outletMountPath) {
|
|
282
|
-
if (parentPath === outletMountPath) {
|
|
283
|
-
indexMatchAtMount = parentPath;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
else {
|
|
287
|
-
// No mount path set yet - index would establish this as mount path
|
|
288
|
-
// But only if we haven't found a better match
|
|
289
|
-
indexMatchAtMount = parentPath;
|
|
290
|
-
}
|
|
324
|
+
// Check for index route match
|
|
325
|
+
const indexMatch = checkIndexMatch(parentPath, remainingPath, hasIndexRoute, outletMountPath);
|
|
326
|
+
if (indexMatch) {
|
|
327
|
+
indexMatchAtMount = indexMatch;
|
|
291
328
|
}
|
|
292
329
|
}
|
|
293
|
-
// Fallback: check
|
|
294
|
-
// This handles outlets inside root-level splat routes where routes like
|
|
295
|
-
// "tab1/*" need to match the full pathname.
|
|
330
|
+
// Fallback: check root level for embedded wildcard routes (e.g., "tab1/*")
|
|
296
331
|
if (!firstSpecificMatch) {
|
|
297
332
|
const fullRemainingPath = segments.join('/');
|
|
298
|
-
|
|
299
|
-
if (hasRootLevelMatch) {
|
|
333
|
+
if (routeChildren.some((route) => matchesEmbeddedWildcardRoute(route, fullRemainingPath))) {
|
|
300
334
|
firstSpecificMatch = '/';
|
|
301
335
|
}
|
|
302
336
|
}
|
|
303
|
-
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
// 3. Index match - only valid at the outlet's mount point, not deeper
|
|
307
|
-
let bestPath = undefined;
|
|
308
|
-
if (firstSpecificMatch) {
|
|
309
|
-
bestPath = firstSpecificMatch;
|
|
310
|
-
}
|
|
311
|
-
else if (firstWildcardMatch) {
|
|
312
|
-
bestPath = firstWildcardMatch;
|
|
313
|
-
}
|
|
314
|
-
else if (indexMatchAtMount) {
|
|
315
|
-
// Only use index match if no specific or wildcard matched
|
|
316
|
-
// This handles the case where pathname exactly matches the mount path
|
|
317
|
-
bestPath = indexMatchAtMount;
|
|
318
|
-
}
|
|
319
|
-
// Store the mount path when we first successfully match a route
|
|
320
|
-
let newOutletMountPath = outletMountPath;
|
|
321
|
-
if (!outletMountPath && bestPath) {
|
|
322
|
-
newOutletMountPath = bestPath;
|
|
323
|
-
}
|
|
324
|
-
// If we have a mount path, verify the current pathname is within scope
|
|
337
|
+
const bestPath = selectBestMatch(firstSpecificMatch, firstWildcardMatch, indexMatchAtMount);
|
|
338
|
+
// Establish mount path on first successful match
|
|
339
|
+
const newOutletMountPath = outletMountPath || bestPath;
|
|
325
340
|
if (newOutletMountPath && !currentPathname.startsWith(newOutletMountPath)) {
|
|
326
341
|
return { parentPath: undefined, outletMountPath: newOutletMountPath };
|
|
327
342
|
}
|
|
328
343
|
return { parentPath: bestPath, outletMountPath: newOutletMountPath };
|
|
329
344
|
}
|
|
330
345
|
}
|
|
331
|
-
// Handle outlets with
|
|
332
|
-
// Compute the common prefix of all absolute routes to determine the outlet's scope
|
|
346
|
+
// Handle outlets with only absolute routes
|
|
333
347
|
if (!hasRelativeRoutes && !hasIndexRoute) {
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
return
|
|
337
|
-
});
|
|
338
|
-
if (absolutePathRoutes.length > 0) {
|
|
339
|
-
const absolutePaths = absolutePathRoutes.map((r) => r.props.path);
|
|
340
|
-
const commonPrefix = computeCommonPrefix(absolutePaths);
|
|
341
|
-
if (commonPrefix && commonPrefix !== '/') {
|
|
342
|
-
// Set the mount path based on common prefix of absolute routes
|
|
343
|
-
const newOutletMountPath = outletMountPath || commonPrefix;
|
|
344
|
-
// Check if current pathname is within scope
|
|
345
|
-
if (!currentPathname.startsWith(commonPrefix)) {
|
|
346
|
-
return { parentPath: undefined, outletMountPath: newOutletMountPath };
|
|
347
|
-
}
|
|
348
|
-
return { parentPath: commonPrefix, outletMountPath: newOutletMountPath };
|
|
349
|
-
}
|
|
348
|
+
const result = computeAbsoluteRoutesParentPath(routeChildren, currentPathname, outletMountPath);
|
|
349
|
+
if (result) {
|
|
350
|
+
return result;
|
|
350
351
|
}
|
|
351
352
|
}
|
|
352
353
|
return { parentPath: outletMountPath, outletMountPath };
|
|
@@ -472,6 +473,72 @@ const NAVIGATE_REDIRECT_DELAY_MS = 100;
|
|
|
472
473
|
* This double-checks that the view is truly not needed before removal.
|
|
473
474
|
*/
|
|
474
475
|
const VIEW_CLEANUP_DELAY_MS = 200;
|
|
476
|
+
/**
|
|
477
|
+
* Computes the absolute pathnameBase for a route element based on its type.
|
|
478
|
+
* Handles relative paths, index routes, and splat routes differently.
|
|
479
|
+
*/
|
|
480
|
+
const computeAbsolutePathnameBase = (routeElement, routeMatch, parentPathnameBase, routeInfoPathname) => {
|
|
481
|
+
const routePath = routeElement.props.path;
|
|
482
|
+
const isRelativePath = routePath && !routePath.startsWith('/');
|
|
483
|
+
const isIndexRoute = !!routeElement.props.index;
|
|
484
|
+
const isSplatOnlyRoute = routePath === '*' || routePath === '/*';
|
|
485
|
+
if (isSplatOnlyRoute) {
|
|
486
|
+
// Splat routes should NOT contribute their matched portion to pathnameBase
|
|
487
|
+
// This aligns with React Router v7's v7_relativeSplatPath behavior
|
|
488
|
+
return parentPathnameBase;
|
|
489
|
+
}
|
|
490
|
+
if (isRelativePath && (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathnameBase)) {
|
|
491
|
+
const relativeBase = routeMatch.pathnameBase.startsWith('/')
|
|
492
|
+
? routeMatch.pathnameBase.slice(1)
|
|
493
|
+
: routeMatch.pathnameBase;
|
|
494
|
+
return parentPathnameBase === '/' ? `/${relativeBase}` : `${parentPathnameBase}/${relativeBase}`;
|
|
495
|
+
}
|
|
496
|
+
if (isIndexRoute) {
|
|
497
|
+
return parentPathnameBase;
|
|
498
|
+
}
|
|
499
|
+
return (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathnameBase) || routeInfoPathname;
|
|
500
|
+
};
|
|
501
|
+
/**
|
|
502
|
+
* Gets fallback params from view items in other outlets when parent context is empty.
|
|
503
|
+
* This handles cases where React context propagation doesn't work as expected.
|
|
504
|
+
*/
|
|
505
|
+
const getFallbackParamsFromViewItems = (allViewItems, currentOutletId, currentPathname) => {
|
|
506
|
+
var _a;
|
|
507
|
+
const params = {};
|
|
508
|
+
for (const otherViewItem of allViewItems) {
|
|
509
|
+
if (otherViewItem.outletId === currentOutletId)
|
|
510
|
+
continue;
|
|
511
|
+
const otherMatch = (_a = otherViewItem.routeData) === null || _a === void 0 ? void 0 : _a.match;
|
|
512
|
+
if ((otherMatch === null || otherMatch === void 0 ? void 0 : otherMatch.params) && Object.keys(otherMatch.params).length > 0) {
|
|
513
|
+
const matchedPathname = otherMatch.pathnameBase || otherMatch.pathname;
|
|
514
|
+
if (matchedPathname && currentPathname.startsWith(matchedPathname)) {
|
|
515
|
+
Object.assign(params, otherMatch.params);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return params;
|
|
520
|
+
};
|
|
521
|
+
/**
|
|
522
|
+
* Builds the matches array for RouteContext.
|
|
523
|
+
*/
|
|
524
|
+
const buildContextMatches = (parentMatches, combinedParams, routeMatch, routeInfoPathname, absolutePathnameBase, viewItem, routeElement, componentElement) => {
|
|
525
|
+
return [
|
|
526
|
+
...parentMatches,
|
|
527
|
+
{
|
|
528
|
+
params: combinedParams,
|
|
529
|
+
pathname: (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathname) || routeInfoPathname,
|
|
530
|
+
pathnameBase: absolutePathnameBase,
|
|
531
|
+
route: {
|
|
532
|
+
id: viewItem.id,
|
|
533
|
+
path: routeElement.props.path,
|
|
534
|
+
element: componentElement,
|
|
535
|
+
index: !!routeElement.props.index,
|
|
536
|
+
caseSensitive: routeElement.props.caseSensitive,
|
|
537
|
+
hasErrorBoundary: false,
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
];
|
|
541
|
+
};
|
|
475
542
|
const createDefaultMatch = (fullPathname, routeProps) => {
|
|
476
543
|
var _a, _b;
|
|
477
544
|
const isIndexRoute = !!routeProps.index;
|
|
@@ -529,7 +596,6 @@ const resolveIndexRouteMatch = (viewItem, pathname, parentPath) => {
|
|
|
529
596
|
class ReactRouterViewStack extends ViewStacks {
|
|
530
597
|
constructor() {
|
|
531
598
|
super();
|
|
532
|
-
this.viewItemCounter = 0;
|
|
533
599
|
/**
|
|
534
600
|
* Creates a new view item for the given outlet and react route element.
|
|
535
601
|
* Associates route props with the matched route path for further lookups.
|
|
@@ -609,8 +675,7 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
609
675
|
};
|
|
610
676
|
return existingViewItem;
|
|
611
677
|
}
|
|
612
|
-
|
|
613
|
-
const id = `${outletId}-${this.viewItemCounter}`;
|
|
678
|
+
const id = `${outletId}-${generateId(outletId)}`;
|
|
614
679
|
const viewItem = {
|
|
615
680
|
id,
|
|
616
681
|
outletId,
|
|
@@ -747,87 +812,19 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
747
812
|
}
|
|
748
813
|
const routeMatch = shouldSkipForDifferentParam ? (_g = viewItem.routeData) === null || _g === void 0 ? void 0 : _g.match : match || ((_h = viewItem.routeData) === null || _h === void 0 ? void 0 : _h.match);
|
|
749
814
|
return (React.createElement(UNSAFE_RouteContext.Consumer, { key: `view-context-${viewItem.id}` }, (parentContext) => {
|
|
750
|
-
var _a, _b
|
|
751
|
-
const parentMatches = (_a = parentContext === null || parentContext === void 0 ? void 0 : parentContext.matches) !== null && _a !== void 0 ? _a : [];
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
}, {});
|
|
755
|
-
// If parentMatches is empty, try to extract params from view items in other outlets.
|
|
756
|
-
// This handles cases where React context propagation doesn't work as expected
|
|
757
|
-
// for nested router outlets.
|
|
815
|
+
var _a, _b;
|
|
816
|
+
const parentMatches = ((_a = parentContext === null || parentContext === void 0 ? void 0 : parentContext.matches) !== null && _a !== void 0 ? _a : []);
|
|
817
|
+
// Accumulate params from parent matches, with fallback to other outlets
|
|
818
|
+
let accumulatedParentParams = parentMatches.reduce((acc, m) => (Object.assign(Object.assign({}, acc), m.params)), {});
|
|
758
819
|
if (parentMatches.length === 0 && Object.keys(accumulatedParentParams).length === 0) {
|
|
759
|
-
|
|
760
|
-
for (const otherViewItem of allViewItems) {
|
|
761
|
-
// Skip view items from the same outlet
|
|
762
|
-
if (otherViewItem.outletId === viewItem.outletId)
|
|
763
|
-
continue;
|
|
764
|
-
// Check if this view item's route could match the current pathname
|
|
765
|
-
const otherMatch = (_b = otherViewItem.routeData) === null || _b === void 0 ? void 0 : _b.match;
|
|
766
|
-
if (otherMatch && otherMatch.params && Object.keys(otherMatch.params).length > 0) {
|
|
767
|
-
// Check if the current pathname starts with this view item's matched pathname
|
|
768
|
-
const matchedPathname = otherMatch.pathnameBase || otherMatch.pathname;
|
|
769
|
-
if (matchedPathname && routeInfo.pathname.startsWith(matchedPathname)) {
|
|
770
|
-
accumulatedParentParams = Object.assign(Object.assign({}, accumulatedParentParams), otherMatch.params);
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
}
|
|
820
|
+
accumulatedParentParams = getFallbackParamsFromViewItems(this.getAllViewItems(), viewItem.outletId, routeInfo.pathname);
|
|
774
821
|
}
|
|
775
|
-
const combinedParams = Object.assign(Object.assign({}, accumulatedParentParams), ((
|
|
776
|
-
// For relative route paths, we need to compute an absolute pathnameBase
|
|
777
|
-
// by combining the parent's pathnameBase with the matched portion
|
|
778
|
-
const routePath = routeElement.props.path;
|
|
779
|
-
const isRelativePath = routePath && !routePath.startsWith('/');
|
|
780
|
-
const isIndexRoute = !!routeElement.props.index;
|
|
781
|
-
const isSplatOnlyRoute = routePath === '*' || routePath === '/*';
|
|
782
|
-
// Get parent's pathnameBase for relative path resolution
|
|
822
|
+
const combinedParams = Object.assign(Object.assign({}, accumulatedParentParams), ((_b = routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.params) !== null && _b !== void 0 ? _b : {}));
|
|
783
823
|
const parentPathnameBase = parentMatches.length > 0 ? parentMatches[parentMatches.length - 1].pathnameBase : '/';
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
let absolutePathnameBase;
|
|
787
|
-
if (isSplatOnlyRoute) {
|
|
788
|
-
// Splat routes should NOT contribute their matched portion to pathnameBase
|
|
789
|
-
// This aligns with React Router v7's v7_relativeSplatPath behavior
|
|
790
|
-
// Without this, relative links inside splat routes get double path segments
|
|
791
|
-
absolutePathnameBase = parentPathnameBase;
|
|
792
|
-
}
|
|
793
|
-
else if (isRelativePath && (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathnameBase)) {
|
|
794
|
-
// For relative paths with a pathnameBase, combine with parent
|
|
795
|
-
const relativeBase = routeMatch.pathnameBase.startsWith('/')
|
|
796
|
-
? routeMatch.pathnameBase.slice(1)
|
|
797
|
-
: routeMatch.pathnameBase;
|
|
798
|
-
absolutePathnameBase =
|
|
799
|
-
parentPathnameBase === '/' ? `/${relativeBase}` : `${parentPathnameBase}/${relativeBase}`;
|
|
800
|
-
}
|
|
801
|
-
else if (isIndexRoute) {
|
|
802
|
-
// Index routes should use the parent's base as their base
|
|
803
|
-
absolutePathnameBase = parentPathnameBase;
|
|
804
|
-
}
|
|
805
|
-
else {
|
|
806
|
-
// Default: use the match's pathnameBase or the current pathname
|
|
807
|
-
absolutePathnameBase = (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathnameBase) || routeInfo.pathname;
|
|
808
|
-
}
|
|
809
|
-
const contextMatches = [
|
|
810
|
-
...parentMatches,
|
|
811
|
-
{
|
|
812
|
-
params: combinedParams,
|
|
813
|
-
pathname: (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathname) || routeInfo.pathname,
|
|
814
|
-
pathnameBase: absolutePathnameBase,
|
|
815
|
-
route: {
|
|
816
|
-
id: viewItem.id,
|
|
817
|
-
path: routeElement.props.path,
|
|
818
|
-
element: componentElement,
|
|
819
|
-
index: !!routeElement.props.index,
|
|
820
|
-
caseSensitive: routeElement.props.caseSensitive,
|
|
821
|
-
hasErrorBoundary: false,
|
|
822
|
-
},
|
|
823
|
-
},
|
|
824
|
-
];
|
|
824
|
+
const absolutePathnameBase = computeAbsolutePathnameBase(routeElement, routeMatch, parentPathnameBase, routeInfo.pathname);
|
|
825
|
+
const contextMatches = buildContextMatches(parentMatches, combinedParams, routeMatch, routeInfo.pathname, absolutePathnameBase, viewItem, routeElement, componentElement);
|
|
825
826
|
const routeContextValue = parentContext
|
|
826
|
-
? Object.assign(Object.assign({}, parentContext), { matches: contextMatches }) : {
|
|
827
|
-
outlet: null,
|
|
828
|
-
matches: contextMatches,
|
|
829
|
-
isDataRoute: false,
|
|
830
|
-
};
|
|
827
|
+
? Object.assign(Object.assign({}, parentContext), { matches: contextMatches }) : { outlet: null, matches: contextMatches, isDataRoute: false };
|
|
831
828
|
return (React.createElement(ViewLifeCycleManager, { key: `view-${viewItem.id}`, mount: viewItem.mount, removeView: () => this.remove(viewItem) },
|
|
832
829
|
React.createElement(UNSAFE_RouteContext.Provider, { value: routeContextValue }, componentElement)));
|
|
833
830
|
}));
|
|
@@ -1529,6 +1526,10 @@ class StackManager extends React.PureComponent {
|
|
|
1529
1526
|
leavingViewItem.mount = false;
|
|
1530
1527
|
this.handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem);
|
|
1531
1528
|
}
|
|
1529
|
+
// Clean up any orphaned sibling views that are no longer reachable
|
|
1530
|
+
// This is important for replace actions (like redirects) where sibling views
|
|
1531
|
+
// that were pushed earlier become unreachable
|
|
1532
|
+
this.cleanupOrphanedSiblingViews(routeInfo, enteringViewItem, leavingViewItem);
|
|
1532
1533
|
}
|
|
1533
1534
|
/**
|
|
1534
1535
|
* Handles the delayed unmount of the leaving view item.
|
|
@@ -1561,7 +1562,8 @@ class StackManager extends React.PureComponent {
|
|
|
1561
1562
|
leavingRoutePath !== '*' &&
|
|
1562
1563
|
!leavingRoutePath.endsWith('/*') &&
|
|
1563
1564
|
!((_f = (_e = leavingViewItem.reactElement) === null || _e === void 0 ? void 0 : _e.props) === null || _f === void 0 ? void 0 : _f.index);
|
|
1564
|
-
// Skip removal
|
|
1565
|
+
// Skip removal for container-to-container transitions (e.g., /tabs/* → /settings/*).
|
|
1566
|
+
// These routes manage their own nested outlets; unmounting would disrupt child views.
|
|
1565
1567
|
if (isEnteringContainerRoute && !isLeavingSpecificRoute) {
|
|
1566
1568
|
return;
|
|
1567
1569
|
}
|
|
@@ -1572,6 +1574,74 @@ class StackManager extends React.PureComponent {
|
|
|
1572
1574
|
this.forceUpdate();
|
|
1573
1575
|
}, VIEW_UNMOUNT_DELAY_MS);
|
|
1574
1576
|
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Cleans up orphaned sibling views after a replace action.
|
|
1579
|
+
* When navigating via replace (e.g., through a redirect), sibling views that were
|
|
1580
|
+
* pushed earlier may become orphaned (unreachable via back navigation).
|
|
1581
|
+
* This method identifies and unmounts such views.
|
|
1582
|
+
*/
|
|
1583
|
+
cleanupOrphanedSiblingViews(routeInfo, enteringViewItem, leavingViewItem) {
|
|
1584
|
+
var _a, _b, _c, _d;
|
|
1585
|
+
// Only cleanup for replace actions
|
|
1586
|
+
if (routeInfo.routeAction !== 'replace') {
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
const enteringRoutePath = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
|
|
1590
|
+
if (!enteringRoutePath) {
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
// Get all views in this outlet
|
|
1594
|
+
const allViewsInOutlet = this.context.getViewItemsForOutlet ? this.context.getViewItemsForOutlet(this.id) : [];
|
|
1595
|
+
// Check if routes are "siblings" - direct children of the same outlet at the same level
|
|
1596
|
+
const areSiblingRoutes = (path1, path2) => {
|
|
1597
|
+
// Both are relative routes (don't start with /)
|
|
1598
|
+
const path1IsRelative = !path1.startsWith('/');
|
|
1599
|
+
const path2IsRelative = !path2.startsWith('/');
|
|
1600
|
+
// For relative routes at the outlet root level, they're siblings
|
|
1601
|
+
if (path1IsRelative && path2IsRelative) {
|
|
1602
|
+
// Check if they're at the same depth (no nested slashes, except for wildcards)
|
|
1603
|
+
const path1Depth = path1.replace(/\/\*$/, '').split('/').filter(Boolean).length;
|
|
1604
|
+
const path2Depth = path2.replace(/\/\*$/, '').split('/').filter(Boolean).length;
|
|
1605
|
+
return path1Depth === path2Depth && path1Depth <= 1;
|
|
1606
|
+
}
|
|
1607
|
+
// For absolute routes, check if they share the same parent
|
|
1608
|
+
const getParent = (path) => {
|
|
1609
|
+
const normalized = path.replace(/\/\*$/, '');
|
|
1610
|
+
const lastSlash = normalized.lastIndexOf('/');
|
|
1611
|
+
return lastSlash > 0 ? normalized.substring(0, lastSlash) : '/';
|
|
1612
|
+
};
|
|
1613
|
+
return getParent(path1) === getParent(path2);
|
|
1614
|
+
};
|
|
1615
|
+
for (const viewItem of allViewsInOutlet) {
|
|
1616
|
+
const viewRoutePath = (_d = (_c = viewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
|
|
1617
|
+
// Skip views that shouldn't be cleaned up:
|
|
1618
|
+
// - The entering view itself
|
|
1619
|
+
// - The immediate leaving view (handled separately by handleLeavingViewUnmount)
|
|
1620
|
+
// - Already unmounted views
|
|
1621
|
+
// - Views without a route path
|
|
1622
|
+
// - Container routes (ending in /*) when entering is also a container route
|
|
1623
|
+
const shouldSkip = viewItem.id === enteringViewItem.id ||
|
|
1624
|
+
(leavingViewItem && viewItem.id === leavingViewItem.id) ||
|
|
1625
|
+
!viewItem.mount ||
|
|
1626
|
+
!viewRoutePath ||
|
|
1627
|
+
(viewRoutePath.endsWith('/*') && enteringRoutePath.endsWith('/*'));
|
|
1628
|
+
if (shouldSkip) {
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
// Check if this is a sibling route that should be cleaned up
|
|
1632
|
+
if (areSiblingRoutes(enteringRoutePath, viewRoutePath)) {
|
|
1633
|
+
// Hide and unmount the orphaned view
|
|
1634
|
+
hideIonPageElement(viewItem.ionPageElement);
|
|
1635
|
+
viewItem.mount = false;
|
|
1636
|
+
// Schedule removal
|
|
1637
|
+
const viewToRemove = viewItem;
|
|
1638
|
+
setTimeout(() => {
|
|
1639
|
+
this.context.unMountViewItem(viewToRemove);
|
|
1640
|
+
this.forceUpdate();
|
|
1641
|
+
}, VIEW_UNMOUNT_DELAY_MS);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1575
1645
|
/**
|
|
1576
1646
|
* Handles the case when entering view has no ion-page element yet (waiting for render).
|
|
1577
1647
|
*/
|
|
@@ -2269,6 +2339,8 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
2269
2339
|
registerHistoryListener(handleHistoryChange);
|
|
2270
2340
|
didMountRef.current = true;
|
|
2271
2341
|
}, []);
|
|
2342
|
+
// Sync route params extracted by React Router's path matching back into routeInfo.
|
|
2343
|
+
// The view stack's match may contain params (e.g., :id) not present in the initial routeInfo.
|
|
2272
2344
|
useEffect(() => {
|
|
2273
2345
|
var _a;
|
|
2274
2346
|
const activeView = viewStack.current.findViewItemByRouteInfo(routeInfo, undefined, true);
|