@ionic/react-router 8.7.13-dev.11765426479.16a61ecf → 8.7.13-dev.11765477700.112ae0a3

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,13 +13,10 @@ 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;
16
17
  const { path, index } = componentProps, restProps = __rest(componentProps, ["path", "index"]);
17
- // Handle index routes
18
+ // Handle index routes - they match when pathname is empty or just "/"
18
19
  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
23
20
  if (pathname === '' || pathname === '/') {
24
21
  return {
25
22
  params: {},
@@ -32,14 +29,25 @@ const matchPath = ({ pathname, componentProps }) => {
32
29
  },
33
30
  };
34
31
  }
35
- // Otherwise, index routes don't match when there's additional path
36
32
  return null;
37
33
  }
38
- if (!path) {
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
+ }
39
48
  return null;
40
49
  }
41
- // For relative paths in nested routes (those that don't start with '/'),
42
- // use React Router's matcher against a normalized path.
50
+ // For relative paths (don't start with '/'), normalize both path and pathname for matching
43
51
  if (!path.startsWith('/')) {
44
52
  const matchOptions = Object.assign({ path: `/${path}` }, restProps);
45
53
  if ((matchOptions === null || matchOptions === void 0 ? void 0 : matchOptions.end) === undefined) {
@@ -51,7 +59,6 @@ const matchPath = ({ pathname, componentProps }) => {
51
59
  // Adjust the match to remove the leading '/' we added
52
60
  return Object.assign(Object.assign({}, match), { pathname: pathname, pathnameBase: match.pathnameBase === '/' ? '' : match.pathnameBase.slice(1), pattern: Object.assign(Object.assign({}, match.pattern), { path: path }) });
53
61
  }
54
- // No match found
55
62
  return null;
56
63
  }
57
64
  // For absolute paths, use React Router's matcher directly.
@@ -71,12 +78,16 @@ const matchPath = ({ pathname, componentProps }) => {
71
78
  */
72
79
  const derivePathnameToMatch = (fullPathname, routePath) => {
73
80
  var _a;
81
+ // For absolute or empty routes, use the full pathname as-is
74
82
  if (!routePath || routePath === '' || routePath.startsWith('/')) {
75
83
  return fullPathname;
76
84
  }
77
85
  const trimmedPath = fullPathname.startsWith('/') ? fullPathname.slice(1) : fullPathname;
78
86
  if (!trimmedPath) {
79
- return '';
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;
80
91
  }
81
92
  const fullSegments = trimmedPath.split('/').filter(Boolean);
82
93
  if (fullSegments.length === 0) {
@@ -152,24 +163,36 @@ const computeCommonPrefix = (paths) => {
152
163
  return commonSegments.length > 0 ? '/' + commonSegments.join('/') : '';
153
164
  };
154
165
  /**
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.
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).
160
189
  */
161
190
  const isSpecificRouteMatch = (route, remainingPath) => {
162
191
  const routePath = route.props.path;
163
- const isWildcardOnly = routePath === '*' || routePath === '/*';
164
- const isIndex = route.props.index;
165
- // Skip wildcards and index routes
166
- if (isIndex || isWildcardOnly) {
192
+ if (route.props.index || isSplatOnlyRoute(routePath)) {
167
193
  return false;
168
194
  }
169
- return !!matchPath({
170
- pathname: remainingPath,
171
- componentProps: route.props,
172
- });
195
+ return !!matchPath({ pathname: remainingPath, componentProps: route.props });
173
196
  };
174
197
  /**
175
198
  * Analyzes route children to determine their characteristics.
@@ -214,11 +237,13 @@ const computeParentPath = (options) => {
214
237
  let firstSpecificMatch = undefined;
215
238
  let firstWildcardMatch = undefined;
216
239
  let indexMatchAtMount = undefined;
240
+ // Start at i = 1 (normal case: strip at least one segment for parent path)
217
241
  for (let i = 1; i <= segments.length; i++) {
218
242
  const parentPath = '/' + segments.slice(0, i).join('/');
219
243
  const remainingPath = segments.slice(i).join('/');
220
- // Check for specific (non-wildcard, non-index) route matches
221
- const hasSpecificMatch = routeChildren.some((route) => isSpecificRouteMatch(route, remainingPath));
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));
222
247
  if (hasSpecificMatch && !firstSpecificMatch) {
223
248
  firstSpecificMatch = parentPath;
224
249
  // Found a specific match - this is our answer for non-index routes
@@ -265,6 +290,16 @@ const computeParentPath = (options) => {
265
290
  }
266
291
  }
267
292
  }
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
+ }
268
303
  // Determine the best parent path:
269
304
  // 1. Specific match (routes like tabs/*, favorites) - highest priority
270
305
  // 2. Wildcard match (route path="*") - catches unmatched segments
@@ -740,27 +775,36 @@ class ReactRouterViewStack extends ViewStacks {
740
775
  const combinedParams = Object.assign(Object.assign({}, accumulatedParentParams), ((_c = routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.params) !== null && _c !== void 0 ? _c : {}));
741
776
  // For relative route paths, we need to compute an absolute pathnameBase
742
777
  // by combining the parent's pathnameBase with the matched portion
743
- let absolutePathnameBase = (routeMatch === null || routeMatch === void 0 ? void 0 : routeMatch.pathnameBase) || routeInfo.pathname;
744
778
  const routePath = routeElement.props.path;
745
779
  const isRelativePath = routePath && !routePath.startsWith('/');
746
780
  const isIndexRoute = !!routeElement.props.index;
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
- }
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;
764
808
  }
765
809
  const contextMatches = [
766
810
  ...parentMatches,
@@ -804,7 +848,9 @@ class ReactRouterViewStack extends ViewStacks {
804
848
  let parentPath = undefined;
805
849
  try {
806
850
  // Only attempt parent path computation for non-root outlets
807
- if (outletId !== 'routerOutlet') {
851
+ // Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
852
+ const isRootOutlet = outletId.startsWith('routerOutlet');
853
+ if (!isRootOutlet) {
808
854
  const routeChildren = extractRouteChildren(ionRouterOutlet.props.children);
809
855
  const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);
810
856
  if (hasRelativeRoutes || hasIndexRoute) {
@@ -1046,7 +1092,7 @@ class ReactRouterViewStack extends ViewStacks {
1046
1092
  * Matches a view with no path prop (default fallback route) or index route.
1047
1093
  */
1048
1094
  function matchDefaultRoute(v) {
1049
- var _a;
1095
+ var _a, _b, _c;
1050
1096
  const childProps = v.routeData.childProps;
1051
1097
  const isDefaultRoute = childProps.path === undefined || childProps.path === '';
1052
1098
  const isIndexRoute = !!childProps.index;
@@ -1059,14 +1105,22 @@ class ReactRouterViewStack extends ViewStacks {
1059
1105
  }
1060
1106
  return false;
1061
1107
  }
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.
1062
1110
  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
+ }
1063
1117
  match = {
1064
1118
  params: {},
1065
1119
  pathname,
1066
1120
  pathnameBase: pathname === '' ? '/' : pathname,
1067
1121
  pattern: {
1068
1122
  path: '',
1069
- caseSensitive: (_a = childProps.caseSensitive) !== null && _a !== void 0 ? _a : false,
1123
+ caseSensitive: (_c = childProps.caseSensitive) !== null && _c !== void 0 ? _c : false,
1070
1124
  end: true,
1071
1125
  },
1072
1126
  };
@@ -1191,24 +1245,30 @@ class StackManager extends React.PureComponent {
1191
1245
  if (this.outletMountPath && !currentPathname.startsWith(this.outletMountPath)) {
1192
1246
  return undefined;
1193
1247
  }
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) {
1248
+ // Check if this outlet has route children to analyze
1249
+ if (this.ionRouterOutlet) {
1197
1250
  const routeChildren = extractRouteChildren(this.ionRouterOutlet.props.children);
1198
1251
  const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);
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;
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;
1210
1271
  }
1211
- return result.parentPath;
1212
1272
  }
1213
1273
  return this.outletMountPath;
1214
1274
  }
@@ -1297,7 +1357,9 @@ class StackManager extends React.PureComponent {
1297
1357
  */
1298
1358
  handleOutOfContextNestedOutlet(parentPath, leavingViewItem) {
1299
1359
  var _a;
1300
- if (this.id === 'routerOutlet' || parentPath !== undefined || !this.ionRouterOutlet) {
1360
+ // Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
1361
+ const isRootOutlet = this.id.startsWith('routerOutlet');
1362
+ if (isRootOutlet || parentPath !== undefined || !this.ionRouterOutlet) {
1301
1363
  return false;
1302
1364
  }
1303
1365
  const routesChildren = (_a = getRoutesChildren(this.ionRouterOutlet.props.children)) !== null && _a !== void 0 ? _a : this.ionRouterOutlet.props.children;
@@ -1322,7 +1384,9 @@ class StackManager extends React.PureComponent {
1322
1384
  * Returns true if the transition should be aborted.
1323
1385
  */
1324
1386
  handleNoMatchingRoute(enteringRoute, enteringViewItem, leavingViewItem) {
1325
- if (this.id === 'routerOutlet' || enteringRoute || enteringViewItem) {
1387
+ // Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
1388
+ const isRootOutlet = this.id.startsWith('routerOutlet');
1389
+ if (isRootOutlet || enteringRoute || enteringViewItem) {
1326
1390
  return false;
1327
1391
  }
1328
1392
  // Hide any visible views in this outlet since it has no matching route
@@ -1869,27 +1933,59 @@ function findRouteByRouteInfo(node, routeInfo, parentPath) {
1869
1933
  });
1870
1934
  // For nested routes in React Router 6, we need to extract the relative path
1871
1935
  // that this outlet should be responsible for matching
1872
- let pathnameToMatch = routeInfo.pathname;
1936
+ const originalPathname = routeInfo.pathname;
1937
+ let relativePathnameToMatch = routeInfo.pathname;
1873
1938
  // Check if we have relative routes (routes that don't start with '/')
1874
1939
  const hasRelativeRoutes = sortedRoutes.some((r) => r.props.path && !r.props.path.startsWith('/'));
1875
1940
  const hasIndexRoute = sortedRoutes.some((r) => r.props.index);
1876
1941
  // SIMPLIFIED: Trust React Router 6's matching more, compute relative path when parent is known
1877
1942
  if ((hasRelativeRoutes || hasIndexRoute) && parentPath) {
1878
1943
  const parentPrefix = parentPath.replace('/*', '');
1879
- const normalizedParent = stripTrailingSlash(parentPrefix);
1944
+ // Normalize both paths to start with '/' for consistent comparison
1945
+ const normalizedParent = stripTrailingSlash(parentPrefix.startsWith('/') ? parentPrefix : `/${parentPrefix}`);
1880
1946
  const normalizedPathname = stripTrailingSlash(routeInfo.pathname);
1881
1947
  // Only compute relative path if pathname is within parent scope
1882
1948
  if (normalizedPathname.startsWith(normalizedParent + '/') || normalizedPathname === normalizedParent) {
1883
1949
  const pathSegments = routeInfo.pathname.split('/').filter(Boolean);
1884
1950
  const parentSegments = normalizedParent.split('/').filter(Boolean);
1885
1951
  const relativeSegments = pathSegments.slice(parentSegments.length);
1886
- pathnameToMatch = relativeSegments.join('/'); // Empty string is valid for index routes
1952
+ relativePathnameToMatch = relativeSegments.join('/'); // Empty string is valid for index routes
1887
1953
  }
1888
1954
  }
1889
1955
  // Find the first matching route
1890
1956
  for (const child of sortedRoutes) {
1957
+ const childPath = child.props.path;
1958
+ const isAbsoluteRoute = childPath && childPath.startsWith('/');
1959
+ // Determine which pathname to match against:
1960
+ // - For absolute routes: use the original full pathname
1961
+ // - For relative routes with a parent: use the computed relative pathname
1962
+ // - For relative routes at root level (no parent): use the original pathname
1963
+ // (matchPath will handle the relative-to-absolute normalization)
1964
+ const pathnameToMatch = isAbsoluteRoute ? originalPathname : relativePathnameToMatch;
1965
+ // Determine the path portion to match:
1966
+ // - For absolute routes: use derivePathnameToMatch
1967
+ // - For relative routes at root level (no parent): use original pathname
1968
+ // directly since matchPath normalizes both path and pathname
1969
+ // - For relative routes with parent: use derivePathnameToMatch for wildcards,
1970
+ // or the computed relative pathname for non-wildcards
1971
+ let pathForMatch;
1972
+ if (isAbsoluteRoute) {
1973
+ pathForMatch = derivePathnameToMatch(pathnameToMatch, childPath);
1974
+ }
1975
+ else if (!parentPath && childPath) {
1976
+ // Root-level relative route: use the full pathname and let matchPath
1977
+ // handle the normalization (it adds '/' to both path and pathname)
1978
+ pathForMatch = originalPathname;
1979
+ }
1980
+ else if (childPath && childPath.includes('*')) {
1981
+ // Relative wildcard route with parent path: use derivePathnameToMatch
1982
+ pathForMatch = derivePathnameToMatch(pathnameToMatch, childPath);
1983
+ }
1984
+ else {
1985
+ pathForMatch = pathnameToMatch;
1986
+ }
1891
1987
  const match = matchPath({
1892
- pathname: pathnameToMatch,
1988
+ pathname: pathForMatch,
1893
1989
  componentProps: child.props,
1894
1990
  });
1895
1991
  if (match) {