@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 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, generateId, StackContext, RouteManagerContext, getConfig, LocationHistory, NavManager } from '@ionic/react';
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 this outlet previously established a mount path and the current
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
- // Find matches at each level, keeping track of the FIRST (shortest) match
237
- let firstSpecificMatch = undefined;
238
- let firstWildcardMatch = undefined;
239
- let indexMatchAtMount = undefined;
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 matches (non-wildcard-only, non-index)
245
- // Also check routes with embedded wildcards (e.g., "tab1/*")
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 if wildcard would match this remaining path
253
- // Only if remaining is non-empty (wildcard needs something to match)
254
- if (remainingPath !== '' && remainingPath !== '/' && hasWildcardRoute && !firstWildcardMatch) {
255
- // Check if any specific route could plausibly match this remaining path
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 when remaining path is empty
277
- // BUT only at the outlet's mount path level
278
- if ((remainingPath === '' || remainingPath === '/') && hasIndexRoute) {
279
- // Index route matches when current path exactly matches the mount path
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 at root level (i = 0) for embedded wildcard routes.
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
- const hasRootLevelMatch = routeChildren.some((route) => matchesEmbeddedWildcardRoute(route, fullRemainingPath));
299
- if (hasRootLevelMatch) {
333
+ if (routeChildren.some((route) => matchesEmbeddedWildcardRoute(route, fullRemainingPath))) {
300
334
  firstSpecificMatch = '/';
301
335
  }
302
336
  }
303
- // Determine the best parent path:
304
- // 1. Specific match (routes like tabs/*, favorites) - highest priority
305
- // 2. Wildcard match (route path="*") - catches unmatched segments
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 ONLY absolute routes (no relative routes or index routes)
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 absolutePathRoutes = routeChildren.filter((route) => {
335
- const path = route.props.path;
336
- return path && path.startsWith('/');
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
- this.viewItemCounter++;
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, _c;
751
- const parentMatches = (_a = parentContext === null || parentContext === void 0 ? void 0 : parentContext.matches) !== null && _a !== void 0 ? _a : [];
752
- let accumulatedParentParams = parentMatches.reduce((acc, match) => {
753
- return Object.assign(Object.assign({}, acc), match.params);
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
- const allViewItems = this.getAllViewItems();
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), ((_c = routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.params) !== null && _c !== void 0 ? _c : {}));
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
- // Start with the match's pathnameBase, falling back to routeInfo.pathname
785
- // BUT: splat-only routes should use parent's base (v7_relativeSplatPath behavior)
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 only for container-to-container transitions
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);