@ionic/react-router 8.7.13-dev.11765569652.136c6b13 → 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
  }
@@ -1357,9 +1297,7 @@ class StackManager extends React.PureComponent {
1357
1297
  */
1358
1298
  handleOutOfContextNestedOutlet(parentPath, leavingViewItem) {
1359
1299
  var _a;
1360
- // Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
1361
- const isRootOutlet = this.id.startsWith('routerOutlet');
1362
- if (isRootOutlet || parentPath !== undefined || !this.ionRouterOutlet) {
1300
+ if (this.id === 'routerOutlet' || parentPath !== undefined || !this.ionRouterOutlet) {
1363
1301
  return false;
1364
1302
  }
1365
1303
  const routesChildren = (_a = getRoutesChildren(this.ionRouterOutlet.props.children)) !== null && _a !== void 0 ? _a : this.ionRouterOutlet.props.children;
@@ -1384,9 +1322,7 @@ class StackManager extends React.PureComponent {
1384
1322
  * Returns true if the transition should be aborted.
1385
1323
  */
1386
1324
  handleNoMatchingRoute(enteringRoute, enteringViewItem, leavingViewItem) {
1387
- // Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
1388
- const isRootOutlet = this.id.startsWith('routerOutlet');
1389
- if (isRootOutlet || enteringRoute || enteringViewItem) {
1325
+ if (this.id === 'routerOutlet' || enteringRoute || enteringViewItem) {
1390
1326
  return false;
1391
1327
  }
1392
1328
  // Hide any visible views in this outlet since it has no matching route
@@ -1690,19 +1626,6 @@ class StackManager extends React.PureComponent {
1690
1626
  const foundView = this.context.findViewItemByRouteInfo(routeInfo, this.id);
1691
1627
  if (foundView) {
1692
1628
  const oldPageElement = foundView.ionPageElement;
1693
- /**
1694
- * FIX for issue #28878: Reject orphaned IonPage registrations.
1695
- *
1696
- * When a component conditionally renders different IonPages (e.g., list vs empty state)
1697
- * using React keys, and state changes simultaneously with navigation, the new IonPage
1698
- * tries to register for a route we're navigating away from. This creates a stale view.
1699
- *
1700
- * Only reject if both pageIds exist and differ, to allow nested outlet registrations.
1701
- */
1702
- if (this.shouldRejectOrphanedPage(page, oldPageElement, routeInfo)) {
1703
- this.hideAndRemoveOrphanedPage(page);
1704
- return;
1705
- }
1706
1629
  foundView.ionPageElement = page;
1707
1630
  foundView.ionRoute = true;
1708
1631
  /**
@@ -1716,35 +1639,6 @@ class StackManager extends React.PureComponent {
1716
1639
  }
1717
1640
  this.handlePageTransition(routeInfo);
1718
1641
  }
1719
- /**
1720
- * Determines if a new IonPage registration should be rejected as orphaned.
1721
- * This happens when a component re-renders with a different IonPage while navigating away.
1722
- */
1723
- shouldRejectOrphanedPage(newPage, oldPageElement, routeInfo) {
1724
- if (!oldPageElement || oldPageElement === newPage) {
1725
- return false;
1726
- }
1727
- const newPageId = newPage.getAttribute('data-pageid');
1728
- const oldPageId = oldPageElement.getAttribute('data-pageid');
1729
- // Only reject if both pageIds exist and are different
1730
- if (!newPageId || !oldPageId || newPageId === oldPageId) {
1731
- return false;
1732
- }
1733
- // Reject only if we're navigating away from this route
1734
- return this.props.routeInfo.pathname !== routeInfo.pathname;
1735
- }
1736
- /**
1737
- * Hides an orphaned IonPage and schedules its removal from the DOM.
1738
- */
1739
- hideAndRemoveOrphanedPage(page) {
1740
- page.classList.add('ion-page-hidden');
1741
- page.setAttribute('aria-hidden', 'true');
1742
- setTimeout(() => {
1743
- if (page.parentElement) {
1744
- page.remove();
1745
- }
1746
- }, VIEW_UNMOUNT_DELAY_MS);
1747
- }
1748
1642
  /**
1749
1643
  * Configures the router outlet for the swipe-to-go-back gesture.
1750
1644
  *
@@ -1975,59 +1869,27 @@ function findRouteByRouteInfo(node, routeInfo, parentPath) {
1975
1869
  });
1976
1870
  // For nested routes in React Router 6, we need to extract the relative path
1977
1871
  // that this outlet should be responsible for matching
1978
- const originalPathname = routeInfo.pathname;
1979
- let relativePathnameToMatch = routeInfo.pathname;
1872
+ let pathnameToMatch = routeInfo.pathname;
1980
1873
  // Check if we have relative routes (routes that don't start with '/')
1981
1874
  const hasRelativeRoutes = sortedRoutes.some((r) => r.props.path && !r.props.path.startsWith('/'));
1982
1875
  const hasIndexRoute = sortedRoutes.some((r) => r.props.index);
1983
1876
  // SIMPLIFIED: Trust React Router 6's matching more, compute relative path when parent is known
1984
1877
  if ((hasRelativeRoutes || hasIndexRoute) && parentPath) {
1985
1878
  const parentPrefix = parentPath.replace('/*', '');
1986
- // Normalize both paths to start with '/' for consistent comparison
1987
- const normalizedParent = stripTrailingSlash(parentPrefix.startsWith('/') ? parentPrefix : `/${parentPrefix}`);
1879
+ const normalizedParent = stripTrailingSlash(parentPrefix);
1988
1880
  const normalizedPathname = stripTrailingSlash(routeInfo.pathname);
1989
1881
  // Only compute relative path if pathname is within parent scope
1990
1882
  if (normalizedPathname.startsWith(normalizedParent + '/') || normalizedPathname === normalizedParent) {
1991
1883
  const pathSegments = routeInfo.pathname.split('/').filter(Boolean);
1992
1884
  const parentSegments = normalizedParent.split('/').filter(Boolean);
1993
1885
  const relativeSegments = pathSegments.slice(parentSegments.length);
1994
- relativePathnameToMatch = relativeSegments.join('/'); // Empty string is valid for index routes
1886
+ pathnameToMatch = relativeSegments.join('/'); // Empty string is valid for index routes
1995
1887
  }
1996
1888
  }
1997
1889
  // Find the first matching route
1998
1890
  for (const child of sortedRoutes) {
1999
- const childPath = child.props.path;
2000
- const isAbsoluteRoute = childPath && childPath.startsWith('/');
2001
- // Determine which pathname to match against:
2002
- // - For absolute routes: use the original full pathname
2003
- // - For relative routes with a parent: use the computed relative pathname
2004
- // - For relative routes at root level (no parent): use the original pathname
2005
- // (matchPath will handle the relative-to-absolute normalization)
2006
- const pathnameToMatch = isAbsoluteRoute ? originalPathname : relativePathnameToMatch;
2007
- // Determine the path portion to match:
2008
- // - For absolute routes: use derivePathnameToMatch
2009
- // - For relative routes at root level (no parent): use original pathname
2010
- // directly since matchPath normalizes both path and pathname
2011
- // - For relative routes with parent: use derivePathnameToMatch for wildcards,
2012
- // or the computed relative pathname for non-wildcards
2013
- let pathForMatch;
2014
- if (isAbsoluteRoute) {
2015
- pathForMatch = derivePathnameToMatch(pathnameToMatch, childPath);
2016
- }
2017
- else if (!parentPath && childPath) {
2018
- // Root-level relative route: use the full pathname and let matchPath
2019
- // handle the normalization (it adds '/' to both path and pathname)
2020
- pathForMatch = originalPathname;
2021
- }
2022
- else if (childPath && childPath.includes('*')) {
2023
- // Relative wildcard route with parent path: use derivePathnameToMatch
2024
- pathForMatch = derivePathnameToMatch(pathnameToMatch, childPath);
2025
- }
2026
- else {
2027
- pathForMatch = pathnameToMatch;
2028
- }
2029
1891
  const match = matchPath({
2030
- pathname: pathForMatch,
1892
+ pathname: pathnameToMatch,
2031
1893
  componentProps: child.props,
2032
1894
  });
2033
1895
  if (match) {