@ionic/react-router 8.7.13-dev.11765829391.14bc580c → 8.7.13-dev.11765907916.16a61ecf

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
@@ -13,10 +13,13 @@ const IonRouteInner = ({ path, element }) => {
13
13
  * @see https://reactrouter.com/v6/utils/match-path
14
14
  */
15
15
  const matchPath = ({ pathname, componentProps }) => {
16
- var _a, _b;
17
16
  const { path, index } = componentProps, restProps = __rest(componentProps, ["path", "index"]);
18
- // Handle index routes - they match when pathname is empty or just "/"
17
+ // Handle index routes
19
18
  if (index && !path) {
19
+ // Index routes match when there's no additional path after the parent route
20
+ // For example, in a nested outlet at /routing/*, the index route matches
21
+ // when the relative path is empty (i.e., we're exactly at /routing)
22
+ // If pathname is empty or just "/", it should match the index route
20
23
  if (pathname === '' || pathname === '/') {
21
24
  return {
22
25
  params: {},
@@ -29,25 +32,14 @@ const matchPath = ({ pathname, componentProps }) => {
29
32
  },
30
33
  };
31
34
  }
35
+ // Otherwise, index routes don't match when there's additional path
32
36
  return null;
33
37
  }
34
- // Handle empty path routes - they match when pathname is also empty or just "/"
35
- if (path === '' || path === undefined) {
36
- if (pathname === '' || pathname === '/') {
37
- return {
38
- params: {},
39
- pathname: pathname,
40
- pathnameBase: pathname || '/',
41
- pattern: {
42
- path: '',
43
- caseSensitive: (_a = restProps.caseSensitive) !== null && _a !== void 0 ? _a : false,
44
- end: (_b = restProps.end) !== null && _b !== void 0 ? _b : true,
45
- },
46
- };
47
- }
38
+ if (!path) {
48
39
  return null;
49
40
  }
50
- // For relative paths (don't start with '/'), normalize both path and pathname for matching
41
+ // For relative paths in nested routes (those that don't start with '/'),
42
+ // use React Router's matcher against a normalized path.
51
43
  if (!path.startsWith('/')) {
52
44
  const matchOptions = Object.assign({ path: `/${path}` }, restProps);
53
45
  if ((matchOptions === null || matchOptions === void 0 ? void 0 : matchOptions.end) === undefined) {
@@ -59,6 +51,7 @@ const matchPath = ({ pathname, componentProps }) => {
59
51
  // Adjust the match to remove the leading '/' we added
60
52
  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
53
  }
54
+ // No match found
62
55
  return null;
63
56
  }
64
57
  // For absolute paths, use React Router's matcher directly.
@@ -78,16 +71,12 @@ const matchPath = ({ pathname, componentProps }) => {
78
71
  */
79
72
  const derivePathnameToMatch = (fullPathname, routePath) => {
80
73
  var _a;
81
- // For absolute or empty routes, use the full pathname as-is
82
74
  if (!routePath || routePath === '' || routePath.startsWith('/')) {
83
75
  return fullPathname;
84
76
  }
85
77
  const trimmedPath = fullPathname.startsWith('/') ? fullPathname.slice(1) : fullPathname;
86
78
  if (!trimmedPath) {
87
- // For root-level relative routes (pathname is "/" and routePath is relative),
88
- // return the full pathname so matchPath can normalize both.
89
- // This allows routes like <Route path="foo/*" .../> at root level to work correctly.
90
- return fullPathname;
79
+ return '';
91
80
  }
92
81
  const fullSegments = trimmedPath.split('/').filter(Boolean);
93
82
  if (fullSegments.length === 0) {
@@ -163,36 +152,24 @@ const computeCommonPrefix = (paths) => {
163
152
  return commonSegments.length > 0 ? '/' + commonSegments.join('/') : '';
164
153
  };
165
154
  /**
166
- * Checks if a route path is a "splat-only" route (just `*` or `/*`).
167
- */
168
- const isSplatOnlyRoute = (routePath) => {
169
- return routePath === '*' || routePath === '/*';
170
- };
171
- /**
172
- * Checks if a route has an embedded wildcard (e.g., "tab1/*" but not "*" or "/*").
173
- */
174
- const hasEmbeddedWildcard = (routePath) => {
175
- return !!routePath && routePath.includes('*') && !isSplatOnlyRoute(routePath);
176
- };
177
- /**
178
- * Checks if a route with an embedded wildcard matches a pathname.
179
- */
180
- const matchesEmbeddedWildcardRoute = (route, pathname) => {
181
- const routePath = route.props.path;
182
- if (!hasEmbeddedWildcard(routePath)) {
183
- return false;
184
- }
185
- return !!matchPath({ pathname, componentProps: route.props });
186
- };
187
- /**
188
- * Checks if a route is a specific match (not wildcard-only or index).
155
+ * Checks if a route is a specific match (not wildcard or index).
156
+ *
157
+ * @param route The route element to check.
158
+ * @param remainingPath The remaining path to match against.
159
+ * @returns True if the route specifically matches the remaining path.
189
160
  */
190
161
  const isSpecificRouteMatch = (route, remainingPath) => {
191
162
  const routePath = route.props.path;
192
- if (route.props.index || isSplatOnlyRoute(routePath)) {
163
+ const isWildcardOnly = routePath === '*' || routePath === '/*';
164
+ const isIndex = route.props.index;
165
+ // Skip wildcards and index routes
166
+ if (isIndex || isWildcardOnly) {
193
167
  return false;
194
168
  }
195
- return !!matchPath({ pathname: remainingPath, componentProps: route.props });
169
+ return !!matchPath({
170
+ pathname: remainingPath,
171
+ componentProps: route.props,
172
+ });
196
173
  };
197
174
  /**
198
175
  * Analyzes route children to determine their characteristics.
@@ -237,13 +214,11 @@ const computeParentPath = (options) => {
237
214
  let firstSpecificMatch = undefined;
238
215
  let firstWildcardMatch = undefined;
239
216
  let indexMatchAtMount = undefined;
240
- // Start at i = 1 (normal case: strip at least one segment for parent path)
241
217
  for (let i = 1; i <= segments.length; i++) {
242
218
  const parentPath = '/' + segments.slice(0, i).join('/');
243
219
  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));
220
+ // Check for specific (non-wildcard, non-index) route matches
221
+ const hasSpecificMatch = routeChildren.some((route) => isSpecificRouteMatch(route, remainingPath));
247
222
  if (hasSpecificMatch && !firstSpecificMatch) {
248
223
  firstSpecificMatch = parentPath;
249
224
  // Found a specific match - this is our answer for non-index routes
@@ -290,16 +265,6 @@ const computeParentPath = (options) => {
290
265
  }
291
266
  }
292
267
  }
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.
296
- if (!firstSpecificMatch) {
297
- const fullRemainingPath = segments.join('/');
298
- const hasRootLevelMatch = routeChildren.some((route) => matchesEmbeddedWildcardRoute(route, fullRemainingPath));
299
- if (hasRootLevelMatch) {
300
- firstSpecificMatch = '/';
301
- }
302
- }
303
268
  // Determine the best parent path:
304
269
  // 1. Specific match (routes like tabs/*, favorites) - highest priority
305
270
  // 2. Wildcard match (route path="*") - catches unmatched segments
@@ -775,36 +740,27 @@ class ReactRouterViewStack extends ViewStacks {
775
740
  const combinedParams = Object.assign(Object.assign({}, accumulatedParentParams), ((_c = routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.params) !== null && _c !== void 0 ? _c : {}));
776
741
  // For relative route paths, we need to compute an absolute pathnameBase
777
742
  // by combining the parent's pathnameBase with the matched portion
743
+ let absolutePathnameBase = (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathnameBase) || routeInfo.pathname;
778
744
  const routePath = routeElement.props.path;
779
745
  const isRelativePath = routePath && !routePath.startsWith('/');
780
746
  const isIndexRoute = !!routeElement.props.index;
781
- const isSplatOnlyRoute = routePath === '*' || routePath === '/*';
782
- // Get parent's pathnameBase for relative path resolution
783
- 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;
747
+ if (isRelativePath || isIndexRoute) {
748
+ // Get the parent's pathnameBase to build the absolute path
749
+ const parentPathnameBase = parentMatches.length > 0 ? parentMatches[parentMatches.length - 1].pathnameBase : '/';
750
+ // For relative paths, the matchPath returns a relative pathnameBase
751
+ // We need to make it absolute by prepending the parent's base
752
+ if ((routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathnameBase) && isRelativePath) {
753
+ // Strip leading slash if present in the relative match
754
+ const relativeBase = routeMatch.pathnameBase.startsWith('/')
755
+ ? routeMatch.pathnameBase.slice(1)
756
+ : routeMatch.pathnameBase;
757
+ absolutePathnameBase =
758
+ parentPathnameBase === '/' ? `/${relativeBase}` : `${parentPathnameBase}/${relativeBase}`;
759
+ }
760
+ else if (isIndexRoute) {
761
+ // Index routes should use the parent's base as their base
762
+ absolutePathnameBase = parentPathnameBase;
763
+ }
808
764
  }
809
765
  const contextMatches = [
810
766
  ...parentMatches,
@@ -848,9 +804,7 @@ class ReactRouterViewStack extends ViewStacks {
848
804
  let parentPath = undefined;
849
805
  try {
850
806
  // Only attempt parent path computation for non-root outlets
851
- // Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
852
- const isRootOutlet = outletId.startsWith('routerOutlet');
853
- if (!isRootOutlet) {
807
+ if (outletId !== 'routerOutlet') {
854
808
  const routeChildren = extractRouteChildren(ionRouterOutlet.props.children);
855
809
  const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);
856
810
  if (hasRelativeRoutes || hasIndexRoute) {
@@ -1092,7 +1046,7 @@ class ReactRouterViewStack extends ViewStacks {
1092
1046
  * Matches a view with no path prop (default fallback route) or index route.
1093
1047
  */
1094
1048
  function matchDefaultRoute(v) {
1095
- var _a, _b, _c;
1049
+ var _a;
1096
1050
  const childProps = v.routeData.childProps;
1097
1051
  const isDefaultRoute = childProps.path === undefined || childProps.path === '';
1098
1052
  const isIndexRoute = !!childProps.index;
@@ -1105,22 +1059,14 @@ class ReactRouterViewStack extends ViewStacks {
1105
1059
  }
1106
1060
  return false;
1107
1061
  }
1108
- // For empty path routes, only match if we're at the same level as when the view was created.
1109
- // This prevents an empty path view item from being reused for different routes.
1110
1062
  if (isDefaultRoute) {
1111
- const previousPathnameBase = ((_b = (_a = v.routeData) === null || _a === void 0 ? void 0 : _a.match) === null || _b === void 0 ? void 0 : _b.pathnameBase) || '';
1112
- const normalizedBase = normalizePathnameForComparison(previousPathnameBase);
1113
- const normalizedPathname = normalizePathnameForComparison(pathname);
1114
- if (normalizedPathname !== normalizedBase) {
1115
- return false;
1116
- }
1117
1063
  match = {
1118
1064
  params: {},
1119
1065
  pathname,
1120
1066
  pathnameBase: pathname === '' ? '/' : pathname,
1121
1067
  pattern: {
1122
1068
  path: '',
1123
- caseSensitive: (_c = childProps.caseSensitive) !== null && _c !== void 0 ? _c : false,
1069
+ caseSensitive: (_a = childProps.caseSensitive) !== null && _a !== void 0 ? _a : false,
1124
1070
  end: true,
1125
1071
  },
1126
1072
  };
@@ -1245,30 +1191,24 @@ class StackManager extends React.PureComponent {
1245
1191
  if (this.outletMountPath && !currentPathname.startsWith(this.outletMountPath)) {
1246
1192
  return undefined;
1247
1193
  }
1248
- // Check if this outlet has route children to analyze
1249
- if (this.ionRouterOutlet) {
1194
+ // If this is a nested outlet (has an explicit ID like "main"),
1195
+ // we need to figure out what part of the path was already matched
1196
+ if (this.id !== 'routerOutlet' && this.ionRouterOutlet) {
1250
1197
  const routeChildren = extractRouteChildren(this.ionRouterOutlet.props.children);
1251
1198
  const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);
1252
- // Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
1253
- // But even outlets with auto-generated IDs may need parent path computation
1254
- // if they have relative routes (indicating they're nested outlets)
1255
- const isRootOutlet = this.id.startsWith('routerOutlet');
1256
- const needsParentPath = !isRootOutlet || hasRelativeRoutes || hasIndexRoute;
1257
- if (needsParentPath) {
1258
- const result = computeParentPath({
1259
- currentPathname,
1260
- outletMountPath: this.outletMountPath,
1261
- routeChildren,
1262
- hasRelativeRoutes,
1263
- hasIndexRoute,
1264
- hasWildcardRoute,
1265
- });
1266
- // Update the outlet mount path if it was set
1267
- if (result.outletMountPath && !this.outletMountPath) {
1268
- this.outletMountPath = result.outletMountPath;
1269
- }
1270
- return result.parentPath;
1199
+ const result = computeParentPath({
1200
+ currentPathname,
1201
+ outletMountPath: this.outletMountPath,
1202
+ routeChildren,
1203
+ hasRelativeRoutes,
1204
+ hasIndexRoute,
1205
+ hasWildcardRoute,
1206
+ });
1207
+ // Update the outlet mount path if it was set
1208
+ if (result.outletMountPath && !this.outletMountPath) {
1209
+ this.outletMountPath = result.outletMountPath;
1271
1210
  }
1211
+ return result.parentPath;
1272
1212
  }
1273
1213
  return this.outletMountPath;
1274
1214
  }
@@ -1316,39 +1256,12 @@ class StackManager extends React.PureComponent {
1316
1256
  * Determines if the leaving view item should be unmounted after a transition.
1317
1257
  */
1318
1258
  shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem) {
1319
- var _a, _b, _c, _d;
1320
1259
  if (!leavingViewItem) {
1321
1260
  return false;
1322
1261
  }
1323
1262
  if (routeInfo.routeAction === 'replace') {
1324
- // For replace actions, decide whether to unmount the leaving view.
1325
- // The key question is: are these routes in the same navigation context?
1326
- const enteringRoutePath = (_b = (_a = enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
1327
- const leavingRoutePath = (_d = (_c = leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
1328
- // Never unmount the root path "/" - it's the main entry point for back navigation
1329
- if (leavingRoutePath === '/' || leavingRoutePath === '') {
1330
- return false;
1331
- }
1332
- if (enteringRoutePath && leavingRoutePath) {
1333
- // Get parent paths to check if routes share a common parent
1334
- const getParentPath = (path) => {
1335
- const normalized = path.replace(/\/\*$/, ''); // Remove trailing /*
1336
- const lastSlash = normalized.lastIndexOf('/');
1337
- return lastSlash > 0 ? normalized.substring(0, lastSlash) : '/';
1338
- };
1339
- const enteringParent = getParentPath(enteringRoutePath);
1340
- const leavingParent = getParentPath(leavingRoutePath);
1341
- // Unmount if:
1342
- // 1. Routes are siblings (same parent, e.g., /page1 and /page2, or /foo/page1 and /foo/page2)
1343
- // 2. Entering is a child of leaving (redirect, e.g., /tabs -> /tabs/tab1)
1344
- const areSiblings = enteringParent === leavingParent && enteringParent !== '/';
1345
- const isChildRedirect = enteringRoutePath.startsWith(leavingRoutePath) ||
1346
- (leavingRoutePath.endsWith('/*') && enteringRoutePath.startsWith(leavingRoutePath.slice(0, -2)));
1347
- return areSiblings || isChildRedirect;
1348
- }
1349
- return false;
1263
+ return true;
1350
1264
  }
1351
- // For non-replace actions, only unmount for back navigation (not forward push)
1352
1265
  const isForwardPush = routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'forward';
1353
1266
  if (!isForwardPush && routeInfo.routeDirection !== 'none' && enteringViewItem !== leavingViewItem) {
1354
1267
  return true;
@@ -1384,9 +1297,7 @@ class StackManager extends React.PureComponent {
1384
1297
  */
1385
1298
  handleOutOfContextNestedOutlet(parentPath, leavingViewItem) {
1386
1299
  var _a;
1387
- // Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
1388
- const isRootOutlet = this.id.startsWith('routerOutlet');
1389
- if (isRootOutlet || parentPath !== undefined || !this.ionRouterOutlet) {
1300
+ if (this.id === 'routerOutlet' || parentPath !== undefined || !this.ionRouterOutlet) {
1390
1301
  return false;
1391
1302
  }
1392
1303
  const routesChildren = (_a = getRoutesChildren(this.ionRouterOutlet.props.children)) !== null && _a !== void 0 ? _a : this.ionRouterOutlet.props.children;
@@ -1411,9 +1322,7 @@ class StackManager extends React.PureComponent {
1411
1322
  * Returns true if the transition should be aborted.
1412
1323
  */
1413
1324
  handleNoMatchingRoute(enteringRoute, enteringViewItem, leavingViewItem) {
1414
- // Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
1415
- const isRootOutlet = this.id.startsWith('routerOutlet');
1416
- if (isRootOutlet || enteringRoute || enteringViewItem) {
1325
+ if (this.id === 'routerOutlet' || enteringRoute || enteringViewItem) {
1417
1326
  return false;
1418
1327
  }
1419
1328
  // Hide any visible views in this outlet since it has no matching route
@@ -1429,6 +1338,8 @@ class StackManager extends React.PureComponent {
1429
1338
  */
1430
1339
  handleReadyEnteringView(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem) {
1431
1340
  var _a, _b;
1341
+ // Ensure the entering view is not hidden from previous navigations
1342
+ showIonPageElement(enteringViewItem.ionPageElement);
1432
1343
  // Handle same view item case (e.g., parameterized route changes)
1433
1344
  if (enteringViewItem === leavingViewItem) {
1434
1345
  const routePath = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
@@ -1452,76 +1363,24 @@ class StackManager extends React.PureComponent {
1452
1363
  if (!leavingViewItem && this.props.routeInfo.prevRouteLastPathname) {
1453
1364
  leavingViewItem = this.context.findViewItemByPathname(this.props.routeInfo.prevRouteLastPathname, this.id);
1454
1365
  }
1455
- // Ensure the entering view is marked as mounted.
1456
- // This is critical for views that were previously unmounted (e.g., navigating back to home).
1457
- // When mount=false, the ViewLifeCycleManager doesn't render the IonPage, so the
1458
- // ionPageElement reference becomes stale. By setting mount=true, we ensure the view
1459
- // gets re-rendered and a new IonPage is created.
1460
- if (!enteringViewItem.mount) {
1461
- enteringViewItem.mount = true;
1462
- }
1463
- // Check visibility state BEFORE showing the entering view.
1464
- // This must be done before showIonPageElement to get accurate visibility state.
1465
- const enteringWasVisible = enteringViewItem.ionPageElement && isViewVisible(enteringViewItem.ionPageElement);
1466
- const leavingIsHidden = leavingViewItem !== undefined && leavingViewItem.ionPageElement && !isViewVisible(leavingViewItem.ionPageElement);
1366
+ // Skip transition if entering view is visible and leaving view is not
1367
+ if (enteringViewItem.ionPageElement &&
1368
+ isViewVisible(enteringViewItem.ionPageElement) &&
1369
+ leavingViewItem !== undefined &&
1370
+ leavingViewItem.ionPageElement &&
1371
+ !isViewVisible(leavingViewItem.ionPageElement)) {
1372
+ return;
1373
+ }
1467
1374
  // Check for duplicate transition
1468
1375
  const currentTransition = {
1469
1376
  enteringId: enteringViewItem.id,
1470
1377
  leavingId: leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.id,
1471
1378
  };
1472
- const isDuplicateTransition = leavingViewItem &&
1379
+ if (leavingViewItem &&
1473
1380
  this.lastTransition &&
1474
1381
  this.lastTransition.leavingId &&
1475
1382
  this.lastTransition.enteringId === currentTransition.enteringId &&
1476
- this.lastTransition.leavingId === currentTransition.leavingId;
1477
- // Skip transition if entering view was ALREADY visible and leaving view is not visible.
1478
- // This indicates the transition has already been performed (e.g., via swipe gesture).
1479
- // IMPORTANT: Only skip if both ionPageElements are the same as when the transition was last done.
1480
- // If the leaving view's ionPageElement changed (e.g., component re-rendered with different IonPage),
1481
- // we should NOT skip because the DOM state is inconsistent.
1482
- if (enteringWasVisible && leavingIsHidden && isDuplicateTransition) {
1483
- // For swipe-to-go-back, the transition animation was handled by the gesture.
1484
- // We still need to set mount=false so React unmounts the leaving view.
1485
- // Only do this when skipTransition is set (indicating gesture completion).
1486
- if (this.skipTransition &&
1487
- shouldUnmountLeavingViewItem &&
1488
- leavingViewItem &&
1489
- enteringViewItem !== leavingViewItem) {
1490
- leavingViewItem.mount = false;
1491
- // Call transitionPage with duration 0 to trigger ionViewDidLeave lifecycle
1492
- // which is needed for ViewLifeCycleManager to remove the view.
1493
- this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back');
1494
- }
1495
- // Clear skipTransition since we're not calling transitionPage which normally clears it
1496
- this.skipTransition = false;
1497
- // Must call forceUpdate to trigger re-render after mount state change
1498
- this.forceUpdate();
1499
- return;
1500
- }
1501
- // Ensure the entering view is not hidden from previous navigations
1502
- // This must happen AFTER the visibility check above
1503
- showIonPageElement(enteringViewItem.ionPageElement);
1504
- // Skip if this is a duplicate transition (but visibility state didn't match above)
1505
- // OR if skipTransition is set (swipe gesture already handled the animation)
1506
- if (isDuplicateTransition || this.skipTransition) {
1507
- // For swipe-to-go-back, we still need to handle unmounting even if visibility
1508
- // conditions aren't fully met (animation might still be in progress)
1509
- if (this.skipTransition &&
1510
- shouldUnmountLeavingViewItem &&
1511
- leavingViewItem &&
1512
- enteringViewItem !== leavingViewItem) {
1513
- leavingViewItem.mount = false;
1514
- // For swipe-to-go-back, we need to call transitionPage with duration 0 to
1515
- // trigger the ionViewDidLeave lifecycle event. The ViewLifeCycleManager
1516
- // uses componentCanBeDestroyed callback to remove the view, which is
1517
- // only called from ionViewDidLeave. Since the gesture animation already
1518
- // completed before mount=false was set, we need to re-fire the lifecycle.
1519
- this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back');
1520
- }
1521
- // Clear skipTransition since we're not calling transitionPage which normally clears it
1522
- this.skipTransition = false;
1523
- // Must call forceUpdate to trigger re-render after mount state change
1524
- this.forceUpdate();
1383
+ this.lastTransition.leavingId === currentTransition.leavingId) {
1525
1384
  return;
1526
1385
  }
1527
1386
  this.lastTransition = currentTransition;
@@ -1533,28 +1392,14 @@ class StackManager extends React.PureComponent {
1533
1392
  }
1534
1393
  }
1535
1394
  /**
1536
- * Handles the delayed unmount of the leaving view item.
1537
- * For 'replace' actions: handles container route transitions specially.
1538
- * For back navigation: explicitly unmounts because the ionViewDidLeave lifecycle
1539
- * fires DURING transitionPage, but mount=false is set AFTER.
1540
- *
1541
- * @param routeInfo Current route information
1542
- * @param enteringViewItem The view being navigated to
1543
- * @param leavingViewItem The view being navigated from
1395
+ * Handles the delayed unmount of the leaving view item after a replace action.
1544
1396
  */
1545
1397
  handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem) {
1546
1398
  var _a, _b, _c, _d, _e, _f;
1547
- if (!leavingViewItem.ionPageElement) {
1548
- return;
1549
- }
1550
- // For push/pop actions, do NOT unmount - views are cached for navigation history.
1551
- // Push: Forward navigation caches views for back navigation
1552
- // Pop: Back navigation should not unmount the entering view's history
1553
- // Only 'replace' actions should actually unmount views since they replace history.
1554
- if (routeInfo.routeAction !== 'replace') {
1399
+ if (routeInfo.routeAction !== 'replace' || !leavingViewItem.ionPageElement) {
1555
1400
  return;
1556
1401
  }
1557
- // For replace actions, check if we should skip removal for nested outlet redirects
1402
+ // Check if we should skip removal for nested outlet redirects
1558
1403
  const enteringRoutePath = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
1559
1404
  const leavingRoutePath = (_d = (_c = leavingViewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
1560
1405
  const isEnteringContainerRoute = enteringRoutePath && enteringRoutePath.endsWith('/*');
@@ -1570,8 +1415,6 @@ class StackManager extends React.PureComponent {
1570
1415
  const viewToUnmount = leavingViewItem;
1571
1416
  setTimeout(() => {
1572
1417
  this.context.unMountViewItem(viewToUnmount);
1573
- // Trigger re-render to remove the view from DOM
1574
- this.forceUpdate();
1575
1418
  }, VIEW_UNMOUNT_DELAY_MS);
1576
1419
  }
1577
1420
  /**
@@ -1616,8 +1459,6 @@ class StackManager extends React.PureComponent {
1616
1459
  this.transitionPage(routeInfo, latestEnteringView, latestLeavingView !== null && latestLeavingView !== void 0 ? latestLeavingView : undefined);
1617
1460
  if (shouldUnmountLeavingViewItem && latestLeavingView && latestEnteringView !== latestLeavingView) {
1618
1461
  latestLeavingView.mount = false;
1619
- // Call handleLeavingViewUnmount to ensure the view is properly removed
1620
- this.handleLeavingViewUnmount(routeInfo, latestEnteringView, latestLeavingView);
1621
1462
  }
1622
1463
  this.forceUpdate();
1623
1464
  }
@@ -1741,12 +1582,7 @@ class StackManager extends React.PureComponent {
1741
1582
  this.context.addViewItem(enteringViewItem);
1742
1583
  }
1743
1584
  // Handle transition based on ion-page element availability
1744
- // Check if the ionPageElement is still in the document.
1745
- // If the view was previously unmounted (mount=false), the ViewLifeCycleManager
1746
- // removes the React component from the tree, which removes the IonPage from the DOM.
1747
- // The ionPageElement reference becomes stale and we need to wait for a new one.
1748
- const ionPageIsInDocument = (enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) && document.body.contains(enteringViewItem.ionPageElement);
1749
- if (enteringViewItem && ionPageIsInDocument) {
1585
+ if (enteringViewItem && enteringViewItem.ionPageElement) {
1750
1586
  // Clear waiting state
1751
1587
  if (this.waitingForIonPage) {
1752
1588
  this.waitingForIonPage = false;
@@ -1757,17 +1593,8 @@ class StackManager extends React.PureComponent {
1757
1593
  }
1758
1594
  this.handleReadyEnteringView(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem);
1759
1595
  }
1760
- else if (enteringViewItem && !ionPageIsInDocument) {
1596
+ else if (enteringViewItem && !enteringViewItem.ionPageElement) {
1761
1597
  // Wait for ion-page to mount
1762
- // This handles both: no ionPageElement, or stale ionPageElement (not in document)
1763
- // Clear stale reference if the element is no longer in the document
1764
- if (enteringViewItem.ionPageElement && !document.body.contains(enteringViewItem.ionPageElement)) {
1765
- enteringViewItem.ionPageElement = undefined;
1766
- }
1767
- // Ensure the view is marked as mounted so ViewLifeCycleManager renders the IonPage
1768
- if (!enteringViewItem.mount) {
1769
- enteringViewItem.mount = true;
1770
- }
1771
1598
  this.handleWaitingForIonPage(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem);
1772
1599
  return;
1773
1600
  }
@@ -1799,19 +1626,6 @@ class StackManager extends React.PureComponent {
1799
1626
  const foundView = this.context.findViewItemByRouteInfo(routeInfo, this.id);
1800
1627
  if (foundView) {
1801
1628
  const oldPageElement = foundView.ionPageElement;
1802
- /**
1803
- * FIX for issue #28878: Reject orphaned IonPage registrations.
1804
- *
1805
- * When a component conditionally renders different IonPages (e.g., list vs empty state)
1806
- * using React keys, and state changes simultaneously with navigation, the new IonPage
1807
- * tries to register for a route we're navigating away from. This creates a stale view.
1808
- *
1809
- * Only reject if both pageIds exist and differ, to allow nested outlet registrations.
1810
- */
1811
- if (this.shouldRejectOrphanedPage(page, oldPageElement, routeInfo)) {
1812
- this.hideAndRemoveOrphanedPage(page);
1813
- return;
1814
- }
1815
1629
  foundView.ionPageElement = page;
1816
1630
  foundView.ionRoute = true;
1817
1631
  /**
@@ -1825,35 +1639,6 @@ class StackManager extends React.PureComponent {
1825
1639
  }
1826
1640
  this.handlePageTransition(routeInfo);
1827
1641
  }
1828
- /**
1829
- * Determines if a new IonPage registration should be rejected as orphaned.
1830
- * This happens when a component re-renders with a different IonPage while navigating away.
1831
- */
1832
- shouldRejectOrphanedPage(newPage, oldPageElement, routeInfo) {
1833
- if (!oldPageElement || oldPageElement === newPage) {
1834
- return false;
1835
- }
1836
- const newPageId = newPage.getAttribute('data-pageid');
1837
- const oldPageId = oldPageElement.getAttribute('data-pageid');
1838
- // Only reject if both pageIds exist and are different
1839
- if (!newPageId || !oldPageId || newPageId === oldPageId) {
1840
- return false;
1841
- }
1842
- // Reject only if we're navigating away from this route
1843
- return this.props.routeInfo.pathname !== routeInfo.pathname;
1844
- }
1845
- /**
1846
- * Hides an orphaned IonPage and schedules its removal from the DOM.
1847
- */
1848
- hideAndRemoveOrphanedPage(page) {
1849
- page.classList.add('ion-page-hidden');
1850
- page.setAttribute('aria-hidden', 'true');
1851
- setTimeout(() => {
1852
- if (page.parentElement) {
1853
- page.remove();
1854
- }
1855
- }, VIEW_UNMOUNT_DELAY_MS);
1856
- }
1857
1642
  /**
1858
1643
  * Configures the router outlet for the swipe-to-go-back gesture.
1859
1644
  *
@@ -1869,23 +1654,11 @@ class StackManager extends React.PureComponent {
1869
1654
  }
1870
1655
  const { routeInfo } = this.props;
1871
1656
  const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
1872
- // First try to find the view in the current outlet
1873
- let enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
1874
- // If not found in current outlet, search all outlets (for cross-outlet swipe back)
1875
- if (!enteringViewItem) {
1876
- enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, undefined, false);
1877
- }
1878
- // Check if the ionPageElement is still in the document.
1879
- // A view might have mount=false but still have its ionPageElement in the DOM
1880
- // (due to timing differences in unmounting).
1881
- const ionPageInDocument = Boolean((enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) && document.body.contains(enteringViewItem.ionPageElement));
1657
+ const enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
1882
1658
  const canStartSwipe = !!enteringViewItem &&
1883
- // Check if we can swipe to this view. Either:
1884
- // 1. The view is mounted (mount=true), OR
1885
- // 2. The view's ionPageElement is still in the document
1886
- // The second case handles views that have been marked for unmount but haven't
1887
- // actually been removed from the DOM yet.
1888
- (enteringViewItem.mount || ionPageInDocument) &&
1659
+ // The root url '/' is treated as the first view item (but is never mounted),
1660
+ // so we do not want to swipe back to the root url.
1661
+ enteringViewItem.mount &&
1889
1662
  // When on the first page it is possible for findViewItemByRouteInfo to
1890
1663
  // return the exact same view you are currently on.
1891
1664
  // Make sure that we are not swiping back to the same instances of a view.
@@ -1895,18 +1668,8 @@ class StackManager extends React.PureComponent {
1895
1668
  const onStart = async () => {
1896
1669
  const { routeInfo } = this.props;
1897
1670
  const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
1898
- // First try to find the view in the current outlet, then search all outlets
1899
- let enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
1900
- if (!enteringViewItem) {
1901
- enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, undefined, false);
1902
- }
1671
+ const enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
1903
1672
  const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
1904
- // Ensure the entering view is mounted so React keeps rendering it during the gesture.
1905
- // This is important when the view was previously marked for unmount but its
1906
- // ionPageElement is still in the DOM.
1907
- if (enteringViewItem && !enteringViewItem.mount) {
1908
- enteringViewItem.mount = true;
1909
- }
1910
1673
  // When the gesture starts, kick off a transition controlled via swipe gesture
1911
1674
  if (enteringViewItem && leavingViewItem) {
1912
1675
  await this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back', true);
@@ -1923,11 +1686,7 @@ class StackManager extends React.PureComponent {
1923
1686
  // Swipe gesture was aborted - re-hide the page that was going to enter
1924
1687
  const { routeInfo } = this.props;
1925
1688
  const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
1926
- // First try to find the view in the current outlet, then search all outlets
1927
- let enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
1928
- if (!enteringViewItem) {
1929
- enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, undefined, false);
1930
- }
1689
+ const enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
1931
1690
  const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
1932
1691
  // Don't hide if entering and leaving are the same (parameterized route edge case)
1933
1692
  if (enteringViewItem !== leavingViewItem && (enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) !== undefined) {
@@ -2110,59 +1869,27 @@ function findRouteByRouteInfo(node, routeInfo, parentPath) {
2110
1869
  });
2111
1870
  // For nested routes in React Router 6, we need to extract the relative path
2112
1871
  // that this outlet should be responsible for matching
2113
- const originalPathname = routeInfo.pathname;
2114
- let relativePathnameToMatch = routeInfo.pathname;
1872
+ let pathnameToMatch = routeInfo.pathname;
2115
1873
  // Check if we have relative routes (routes that don't start with '/')
2116
1874
  const hasRelativeRoutes = sortedRoutes.some((r) => r.props.path && !r.props.path.startsWith('/'));
2117
1875
  const hasIndexRoute = sortedRoutes.some((r) => r.props.index);
2118
1876
  // SIMPLIFIED: Trust React Router 6's matching more, compute relative path when parent is known
2119
1877
  if ((hasRelativeRoutes || hasIndexRoute) && parentPath) {
2120
1878
  const parentPrefix = parentPath.replace('/*', '');
2121
- // Normalize both paths to start with '/' for consistent comparison
2122
- const normalizedParent = stripTrailingSlash(parentPrefix.startsWith('/') ? parentPrefix : `/${parentPrefix}`);
1879
+ const normalizedParent = stripTrailingSlash(parentPrefix);
2123
1880
  const normalizedPathname = stripTrailingSlash(routeInfo.pathname);
2124
1881
  // Only compute relative path if pathname is within parent scope
2125
1882
  if (normalizedPathname.startsWith(normalizedParent + '/') || normalizedPathname === normalizedParent) {
2126
1883
  const pathSegments = routeInfo.pathname.split('/').filter(Boolean);
2127
1884
  const parentSegments = normalizedParent.split('/').filter(Boolean);
2128
1885
  const relativeSegments = pathSegments.slice(parentSegments.length);
2129
- relativePathnameToMatch = relativeSegments.join('/'); // Empty string is valid for index routes
1886
+ pathnameToMatch = relativeSegments.join('/'); // Empty string is valid for index routes
2130
1887
  }
2131
1888
  }
2132
1889
  // Find the first matching route
2133
1890
  for (const child of sortedRoutes) {
2134
- const childPath = child.props.path;
2135
- const isAbsoluteRoute = childPath && childPath.startsWith('/');
2136
- // Determine which pathname to match against:
2137
- // - For absolute routes: use the original full pathname
2138
- // - For relative routes with a parent: use the computed relative pathname
2139
- // - For relative routes at root level (no parent): use the original pathname
2140
- // (matchPath will handle the relative-to-absolute normalization)
2141
- const pathnameToMatch = isAbsoluteRoute ? originalPathname : relativePathnameToMatch;
2142
- // Determine the path portion to match:
2143
- // - For absolute routes: use derivePathnameToMatch
2144
- // - For relative routes at root level (no parent): use original pathname
2145
- // directly since matchPath normalizes both path and pathname
2146
- // - For relative routes with parent: use derivePathnameToMatch for wildcards,
2147
- // or the computed relative pathname for non-wildcards
2148
- let pathForMatch;
2149
- if (isAbsoluteRoute) {
2150
- pathForMatch = derivePathnameToMatch(pathnameToMatch, childPath);
2151
- }
2152
- else if (!parentPath && childPath) {
2153
- // Root-level relative route: use the full pathname and let matchPath
2154
- // handle the normalization (it adds '/' to both path and pathname)
2155
- pathForMatch = originalPathname;
2156
- }
2157
- else if (childPath && childPath.includes('*')) {
2158
- // Relative wildcard route with parent path: use derivePathnameToMatch
2159
- pathForMatch = derivePathnameToMatch(pathnameToMatch, childPath);
2160
- }
2161
- else {
2162
- pathForMatch = pathnameToMatch;
2163
- }
2164
1891
  const match = matchPath({
2165
- pathname: pathForMatch,
1892
+ pathname: pathnameToMatch,
2166
1893
  componentProps: child.props,
2167
1894
  });
2168
1895
  if (match) {