77import type { RouteInfo , StackContextState , ViewItem } from '@ionic/react' ;
88import { IonRoute , RouteManagerContext , StackContext , generateId , getConfig } from '@ionic/react' ;
99import React from 'react' ;
10- import { Route , UNSAFE_RouteContext as RouteContext } from 'react-router-dom' ;
10+ import type { RouteObject } from 'react-router-dom' ;
11+ import { Route , UNSAFE_RouteContext as RouteContext , matchRoutes } from 'react-router-dom' ;
1112
1213import { clonePageElement } from './clonePageElement' ;
1314import {
@@ -19,7 +20,6 @@ import {
1920import { derivePathnameToMatch , matchPath } from './utils/pathMatching' ;
2021import { stripTrailingSlash } from './utils/pathNormalization' ;
2122import { extractRouteChildren , getRoutesChildren , isNavigateElement } from './utils/routeElements' ;
22- import { compareRouteSpecificity } from './utils/viewItemUtils' ;
2323
2424/**
2525 * Delay in milliseconds before unmounting a view after a transition completes.
@@ -1320,7 +1320,7 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
13201320 // Derive the outlet's mount path from React Router's matched route context.
13211321 // This eliminates the need for heuristic-based mount path discovery in
13221322 // computeParentPath, since React Router already knows the matched base path.
1323- const parentMatches = parentContext ?. matches as Array < { pathnameBase : string } > | undefined ;
1323+ const parentMatches = parentContext ?. matches as { pathnameBase : string } [ ] | undefined ;
13241324 const parentPathnameBase =
13251325 parentMatches && parentMatches . length > 0
13261326 ? parentMatches [ parentMatches . length - 1 ] . pathnameBase
@@ -1383,6 +1383,49 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
13831383
13841384export default StackManager ;
13851385
1386+ /**
1387+ * Converts React Route elements to RouteObject format for use with matchRoutes().
1388+ * Filters out pathless routes (which are handled by fallback logic separately).
1389+ *
1390+ * When a basename is provided, absolute route paths are relativized by stripping
1391+ * the basename prefix. This is necessary because matchRoutes() strips the basename
1392+ * from the LOCATION pathname but not from route paths — absolute paths must be
1393+ * made relative to the basename for matching to work correctly.
1394+ *
1395+ * @param routeChildren The flat array of Route/IonRoute elements from the outlet.
1396+ * @param basename The resolved parent path (without trailing slash or `/*`) used to relativize absolute paths.
1397+ */
1398+ function routeElementsToRouteObjects ( routeChildren : React . ReactElement [ ] , basename ?: string ) : RouteObject [ ] {
1399+ return routeChildren
1400+ . filter ( ( child ) => child . props . path != null || child . props . index )
1401+ . map ( ( child ) : RouteObject => {
1402+ const handle = { _element : child } ;
1403+ let path = child . props . path as string | undefined ;
1404+
1405+ // Relativize absolute paths by stripping the basename prefix
1406+ if ( path && path . startsWith ( '/' ) && basename ) {
1407+ if ( path === basename ) {
1408+ path = '' ;
1409+ } else if ( path . startsWith ( basename + '/' ) ) {
1410+ path = path . slice ( basename . length + 1 ) ;
1411+ }
1412+ }
1413+
1414+ if ( child . props . index ) {
1415+ return {
1416+ index : true ,
1417+ handle,
1418+ caseSensitive : child . props . caseSensitive || undefined ,
1419+ } ;
1420+ }
1421+ return {
1422+ path,
1423+ handle,
1424+ caseSensitive : child . props . caseSensitive || undefined ,
1425+ } ;
1426+ } ) ;
1427+ }
1428+
13861429/**
13871430 * Finds the `<Route />` node matching the current route info.
13881431 * If no `<Route />` can be matched, a fallback node is returned.
@@ -1405,102 +1448,39 @@ function findRouteByRouteInfo(node: React.ReactNode, routeInfo: RouteInfo, paren
14051448 React . isValidElement ( child ) && ( child . type === Route || child . type === IonRoute )
14061449 ) ;
14071450
1408- // Sort routes by specificity (most specific first)
1409- const sortedRoutes = routeChildren . sort ( ( a , b ) =>
1410- compareRouteSpecificity (
1411- { path : a . props . path || '' , index : ! ! a . props . index } ,
1412- { path : b . props . path || '' , index : ! ! b . props . index }
1413- )
1414- ) ;
1415-
1416- // For nested routes in React Router 6, we need to extract the relative path
1417- // that this outlet should be responsible for matching
1418- const originalPathname = routeInfo . pathname ;
1419- let relativePathnameToMatch = routeInfo . pathname ;
1420-
1421- // Check if we have relative routes (routes that don't start with '/')
1422- const hasRelativeRoutes = sortedRoutes . some ( ( r ) => r . props . path && ! r . props . path . startsWith ( '/' ) ) ;
1423- const hasIndexRoute = sortedRoutes . some ( ( r ) => r . props . index ) ;
1424-
1425- // When parent path is known, compute the relative pathname for matching
1426- if ( ( hasRelativeRoutes || hasIndexRoute ) && parentPath ) {
1427- const parentPrefix = parentPath . replace ( '/*' , '' ) ;
1428- // Normalize both paths to start with '/' for consistent comparison
1429- const normalizedParent = stripTrailingSlash ( parentPrefix . startsWith ( '/' ) ? parentPrefix : `/${ parentPrefix } ` ) ;
1430- const normalizedPathname = stripTrailingSlash ( routeInfo . pathname ) ;
1431-
1432- // Only compute relative path if pathname is within parent scope
1433- if ( normalizedPathname . startsWith ( normalizedParent + '/' ) || normalizedPathname === normalizedParent ) {
1434- const pathSegments = routeInfo . pathname . split ( '/' ) . filter ( Boolean ) ;
1435- const parentSegments = normalizedParent . split ( '/' ) . filter ( Boolean ) ;
1436- const relativeSegments = pathSegments . slice ( parentSegments . length ) ;
1437- relativePathnameToMatch = relativeSegments . join ( '/' ) ; // Empty string is valid for index routes
1438- }
1439- }
1440-
1441- // Find the first matching route
1442- for ( const child of sortedRoutes ) {
1443- const childPath = child . props . path as string | undefined ;
1444- const isAbsoluteRoute = childPath && childPath . startsWith ( '/' ) ;
1445-
1446- // Determine which pathname to match against:
1447- // - For absolute routes: use the original full pathname
1448- // - For relative routes with a parent: use the computed relative pathname
1449- // - For relative routes at root level (no parent): use the original pathname
1450- const pathnameToMatch = isAbsoluteRoute ? originalPathname : relativePathnameToMatch ;
1451-
1452- // Determine the path portion to match
1453- let pathForMatch : string ;
1454- if ( isAbsoluteRoute ) {
1455- pathForMatch = derivePathnameToMatch ( pathnameToMatch , childPath ) ;
1456- } else if ( ! parentPath && childPath ) {
1457- // Root-level relative route: use the full pathname and let matchPath
1458- // handle the normalization (it adds '/' to both path and pathname)
1459- pathForMatch = originalPathname ;
1460- } else if ( childPath && childPath . includes ( '*' ) ) {
1461- // Relative wildcard route with parent path: the relative pathname was already
1462- // computed above using the parent path, so it's already correct
1463- pathForMatch = pathnameToMatch ;
1464- } else {
1465- pathForMatch = pathnameToMatch ;
1466- }
1467-
1468- const match = matchPath ( {
1469- pathname : pathForMatch ,
1470- componentProps : child . props ,
1471- } ) ;
1451+ // Delegate route matching to RR6's matchRoutes(), which handles specificity ranking internally.
1452+ const basename = parentPath ? stripTrailingSlash ( parentPath . replace ( '/*' , '' ) ) : undefined ;
1453+ const routeObjects = routeElementsToRouteObjects ( routeChildren , basename ) ;
1454+ const matches = matchRoutes ( routeObjects , { pathname : routeInfo . pathname } , basename ) ;
14721455
1473- if ( match ) {
1474- matchedNode = child ;
1475- break ;
1476- }
1477- }
1478-
1479- if ( matchedNode ) {
1480- return matchedNode ;
1456+ if ( matches && matches . length > 0 ) {
1457+ const bestMatch = matches [ matches . length - 1 ] ;
1458+ matchedNode = ( bestMatch . route as any ) . handle ?. _element ?? undefined ;
14811459 }
14821460
14831461 // Fallback: try pathless routes, but only if pathname is within scope.
1484- let pathnameInScope = true ;
1462+ if ( ! matchedNode ) {
1463+ let pathnameInScope = true ;
14851464
1486- if ( parentPath ) {
1487- pathnameInScope = isPathnameInScope ( routeInfo . pathname , parentPath ) ;
1488- } else {
1489- const absolutePathRoutes = routeChildren . filter ( ( r ) => r . props . path && r . props . path . startsWith ( '/' ) ) ;
1490- if ( absolutePathRoutes . length > 0 ) {
1491- const absolutePaths = absolutePathRoutes . map ( ( r ) => r . props . path as string ) ;
1492- const commonPrefix = computeCommonPrefix ( absolutePaths ) ;
1493- if ( commonPrefix && commonPrefix !== '/' ) {
1494- pathnameInScope = routeInfo . pathname . startsWith ( commonPrefix ) ;
1465+ if ( parentPath ) {
1466+ pathnameInScope = isPathnameInScope ( routeInfo . pathname , parentPath ) ;
1467+ } else {
1468+ const absolutePathRoutes = routeChildren . filter ( ( r ) => r . props . path && r . props . path . startsWith ( '/' ) ) ;
1469+ if ( absolutePathRoutes . length > 0 ) {
1470+ const absolutePaths = absolutePathRoutes . map ( ( r ) => r . props . path as string ) ;
1471+ const commonPrefix = computeCommonPrefix ( absolutePaths ) ;
1472+ if ( commonPrefix && commonPrefix !== '/' ) {
1473+ pathnameInScope = routeInfo . pathname . startsWith ( commonPrefix ) ;
1474+ }
14951475 }
14961476 }
1497- }
14981477
1499- if ( pathnameInScope ) {
1500- for ( const child of routeChildren ) {
1501- if ( ! child . props . path ) {
1502- fallbackNode = child ;
1503- break ;
1478+ if ( pathnameInScope ) {
1479+ for ( const child of routeChildren ) {
1480+ if ( ! child . props . path ) {
1481+ fallbackNode = child ;
1482+ break ;
1483+ }
15041484 }
15051485 }
15061486 }
0 commit comments