@@ -226,6 +226,13 @@ export class ReactRouterViewStack extends ViewStacks {
226226 */
227227 private outletParentPaths = new Map < string , string > ( ) ;
228228
229+ /**
230+ * Stores the computed mount path for each outlet.
231+ * Fed back into computeParentPath on subsequent calls to stabilize
232+ * the parent path computation across navigations (mirrors StackManager.outletMountPath).
233+ */
234+ private outletMountPaths = new Map < string , string > ( ) ;
235+
229236 constructor ( ) {
230237 super ( ) ;
231238 }
@@ -440,11 +447,12 @@ export class ReactRouterViewStack extends ViewStacks {
440447 viewItem . routeData . match = match ;
441448 }
442449
443- // Deactivate wildcard routes and catch-all routes ( empty path) when we have specific route matches
444- // This prevents "Not found" or fallback pages from showing alongside valid routes
450+ // Deactivate wildcard ( catch-all) and empty- path (default) routes when a more- specific route matches.
451+ // This prevents "Not found" or fallback pages from showing alongside valid routes.
445452 if ( routePath === '*' || routePath === '' ) {
446453 // Check if any other view in this outlet has a match for the current route
447- const hasSpecificMatch = this . getViewItemsForOutlet ( viewItem . outletId ) . some ( ( v ) => {
454+ const outletViews = this . getViewItemsForOutlet ( viewItem . outletId ) ;
455+ let hasSpecificMatch = outletViews . some ( ( v ) => {
448456 if ( v . id === viewItem . id ) return false ; // Skip self
449457 const vRoutePath = v . reactElement ?. props ?. path || '' ;
450458 if ( vRoutePath === '*' || vRoutePath === '' ) return false ; // Skip other wildcard/empty routes
@@ -454,6 +462,28 @@ export class ReactRouterViewStack extends ViewStacks {
454462 return ! ! vMatch ;
455463 } ) ;
456464
465+ // For catch-all * routes, also deactivate when the pathname matches the outlet's
466+ // parent path exactly. This means there are no remaining segments for the wildcard
467+ // to catch, so the empty-path or index route should handle it instead.
468+ if ( ! hasSpecificMatch && routePath === '*' ) {
469+ const outletParentPath = this . outletParentPaths . get ( viewItem . outletId ) ;
470+ if ( outletParentPath ) {
471+ const normalizedParent = normalizePathnameForComparison ( outletParentPath ) ;
472+ const normalizedPathname = normalizePathnameForComparison ( routeInfo . pathname ) ;
473+ if ( normalizedPathname === normalizedParent ) {
474+ // Check if there's an empty-path or index view item that should handle this
475+ const hasDefaultRoute = outletViews . some ( ( v ) => {
476+ if ( v . id === viewItem . id ) return false ;
477+ const vRoutePath = v . reactElement ?. props ?. path ;
478+ return vRoutePath === '' || vRoutePath === undefined || ! ! v . routeData ?. childProps ?. index ;
479+ } ) ;
480+ if ( hasDefaultRoute ) {
481+ hasSpecificMatch = true ;
482+ }
483+ }
484+ }
485+ }
486+
457487 if ( hasSpecificMatch ) {
458488 viewItem . mount = false ;
459489 if ( viewItem . ionPageElement ) {
@@ -544,35 +574,42 @@ export class ReactRouterViewStack extends ViewStacks {
544574 ) => {
545575 const viewItems = this . getViewItemsForOutlet ( outletId ) ;
546576
547- // Determine parentPath for nested outlets to properly evaluate index routes
577+ // Determine parentPath for outlets with relative or index routes.
578+ // This populates outletParentPaths for findViewItemByPath's matchView
579+ // and the catch-all deactivation logic in renderViewItem.
548580 let parentPath : string | undefined = undefined ;
549581 try {
550- // Only attempt parent path computation for non-root outlets
551- // Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
552- const isRootOutlet = outletId . startsWith ( 'routerOutlet' ) ;
553- if ( ! isRootOutlet ) {
554- const routeChildren = extractRouteChildren ( ionRouterOutlet . props . children ) ;
555- const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren ( routeChildren ) ;
556-
557- if ( hasRelativeRoutes || hasIndexRoute ) {
558- const result = computeParentPath ( {
559- currentPathname : routeInfo . pathname ,
560- outletMountPath : undefined ,
561- routeChildren,
562- hasRelativeRoutes,
563- hasIndexRoute,
564- hasWildcardRoute,
565- } ) ;
566- parentPath = result . parentPath ;
582+ const routeChildren = extractRouteChildren ( ionRouterOutlet . props . children ) ;
583+ const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren ( routeChildren ) ;
584+
585+ if ( hasRelativeRoutes || hasIndexRoute ) {
586+ const result = computeParentPath ( {
587+ currentPathname : routeInfo . pathname ,
588+ outletMountPath : this . outletMountPaths . get ( outletId ) ,
589+ routeChildren,
590+ hasRelativeRoutes,
591+ hasIndexRoute,
592+ hasWildcardRoute,
593+ } ) ;
594+ parentPath = result . parentPath ;
595+
596+ // Persist the mount path for subsequent calls, mirroring StackManager.outletMountPath.
597+ // Unlike outletParentPaths (cleared when parentPath is undefined), the mount path is
598+ // intentionally sticky — it anchors the outlet's scope and is only removed in clear().
599+ if ( result . outletMountPath && ! this . outletMountPaths . has ( outletId ) ) {
600+ this . outletMountPaths . set ( outletId , result . outletMountPath ) ;
567601 }
568602 }
569603 } catch ( e ) {
570604 // Non-fatal: if we fail to compute parentPath, fall back to previous behavior
571605 }
572606
573- // Store the computed parentPath for use in findViewItemByPath
607+ // Store the computed parentPath for use in findViewItemByPath.
608+ // Clear stale entries when parentPath is undefined (e.g., navigated out of scope).
574609 if ( parentPath !== undefined ) {
575610 this . outletParentPaths . set ( outletId , parentPath ) ;
611+ } else if ( this . outletParentPaths . has ( outletId ) ) {
612+ this . outletParentPaths . delete ( outletId ) ;
576613 }
577614
578615 // Sync child elements with stored viewItems (e.g. to reflect new props)
@@ -731,6 +768,7 @@ export class ReactRouterViewStack extends ViewStacks {
731768 // a wildcard view, it should not be reused for subsequent navigations.
732769 // A fresh wildcard view will be created by createViewItem when needed.
733770 if ( ( viewItemPath === '*' || viewItemPath === '/*' ) && ! v . mount ) return false ;
771+
734772 const isIndexRoute = ! ! v . routeData . childProps . index ;
735773 const previousMatch = v . routeData ?. match ;
736774 const result = v . reactElement ? matchComponent ( v . reactElement , pathname ) : null ;
@@ -743,6 +781,21 @@ export class ReactRouterViewStack extends ViewStacks {
743781 viewItem = v ;
744782 return true ;
745783 }
784+
785+ // Empty path routes (path="") should match when the pathname matches the
786+ // outlet's parent path exactly (no remaining segments). matchComponent doesn't
787+ // handle this because it lacks parent path context. Without this check, a
788+ // catch-all * view item (which matches any pathname) would be incorrectly
789+ // returned instead of the empty path route on back navigation.
790+ if ( viewItemPath === '' && ! isIndexRoute && outletParentPath ) {
791+ const normalizedParent = normalizePathnameForComparison ( outletParentPath ) ;
792+ const normalizedPathname = normalizePathnameForComparison ( pathname ) ;
793+ if ( normalizedPathname === normalizedParent ) {
794+ match = createDefaultMatch ( pathname , v . routeData . childProps ) ;
795+ viewItem = v ;
796+ return true ;
797+ }
798+ }
746799 }
747800
748801 if ( result ) {
@@ -886,6 +939,7 @@ export class ReactRouterViewStack extends ViewStacks {
886939 */
887940 clear = ( outletId : string ) => {
888941 this . outletParentPaths . delete ( outletId ) ;
942+ this . outletMountPaths . delete ( outletId ) ;
889943 return super . clear ( outletId ) ;
890944 } ;
891945
0 commit comments