@@ -470,6 +470,11 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
470470 leavingViewItem . mount = false ;
471471 this . handleLeavingViewUnmount ( routeInfo , enteringViewItem , leavingViewItem ) ;
472472 }
473+
474+ // Clean up any orphaned sibling views that are no longer reachable
475+ // This is important for replace actions (like redirects) where sibling views
476+ // that were pushed earlier become unreachable
477+ this . cleanupOrphanedSiblingViews ( routeInfo , enteringViewItem , leavingViewItem ) ;
473478 }
474479
475480 /**
@@ -520,6 +525,96 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
520525 } , VIEW_UNMOUNT_DELAY_MS ) ;
521526 }
522527
528+ /**
529+ * Cleans up orphaned sibling views after a replace action.
530+ * When navigating via replace (e.g., through a redirect), sibling views that were
531+ * pushed earlier may become orphaned (unreachable via back navigation).
532+ * This method identifies and unmounts such views.
533+ */
534+ private cleanupOrphanedSiblingViews (
535+ routeInfo : RouteInfo ,
536+ enteringViewItem : ViewItem ,
537+ leavingViewItem : ViewItem | undefined
538+ ) : void {
539+ // Only cleanup for replace actions
540+ if ( routeInfo . routeAction !== 'replace' ) {
541+ return ;
542+ }
543+
544+ const enteringRoutePath = enteringViewItem . reactElement ?. props ?. path as string | undefined ;
545+ if ( ! enteringRoutePath ) {
546+ return ;
547+ }
548+
549+ // Get all views in this outlet
550+ const allViewsInOutlet = this . context . getViewItemsForOutlet ? this . context . getViewItemsForOutlet ( this . id ) : [ ] ;
551+
552+ // Check if routes are "siblings" - direct children of the same outlet at the same level
553+ const areSiblingRoutes = ( path1 : string , path2 : string ) : boolean => {
554+ // Both are relative routes (don't start with /)
555+ const path1IsRelative = ! path1 . startsWith ( '/' ) ;
556+ const path2IsRelative = ! path2 . startsWith ( '/' ) ;
557+
558+ // For relative routes at the outlet root level, they're siblings
559+ if ( path1IsRelative && path2IsRelative ) {
560+ // Check if they're at the same depth (no nested slashes, except for wildcards)
561+ const path1Depth = path1 . replace ( / \/ \* $ / , '' ) . split ( '/' ) . filter ( Boolean ) . length ;
562+ const path2Depth = path2 . replace ( / \/ \* $ / , '' ) . split ( '/' ) . filter ( Boolean ) . length ;
563+ return path1Depth === path2Depth && path1Depth <= 1 ;
564+ }
565+
566+ // For absolute routes, check if they share the same parent
567+ const getParent = ( path : string ) => {
568+ const normalized = path . replace ( / \/ \* $ / , '' ) ;
569+ const lastSlash = normalized . lastIndexOf ( '/' ) ;
570+ return lastSlash > 0 ? normalized . substring ( 0 , lastSlash ) : '/' ;
571+ } ;
572+
573+ return getParent ( path1 ) === getParent ( path2 ) ;
574+ } ;
575+
576+ for ( const viewItem of allViewsInOutlet ) {
577+ // Skip the entering view
578+ if ( viewItem . id === enteringViewItem . id ) {
579+ continue ;
580+ }
581+
582+ // Skip the immediate leaving view (handled separately)
583+ if ( leavingViewItem && viewItem . id === leavingViewItem . id ) {
584+ continue ;
585+ }
586+
587+ // Skip if already unmounted
588+ if ( ! viewItem . mount ) {
589+ continue ;
590+ }
591+
592+ const viewRoutePath = viewItem . reactElement ?. props ?. path as string | undefined ;
593+ if ( ! viewRoutePath ) {
594+ continue ;
595+ }
596+
597+ // Skip container routes (ending in /*) - they manage their own children
598+ if ( viewRoutePath . endsWith ( '/*' ) && enteringRoutePath . endsWith ( '/*' ) ) {
599+ continue ;
600+ }
601+
602+ // Check if this is a sibling route that should be cleaned up
603+ if ( areSiblingRoutes ( enteringRoutePath , viewRoutePath ) ) {
604+ // Hide and unmount the orphaned view
605+ hideIonPageElement ( viewItem . ionPageElement ) ;
606+ viewItem . mount = false ;
607+
608+ // Schedule removal
609+ const viewToRemove = viewItem ;
610+ setTimeout ( ( ) => {
611+ this . context . unMountViewItem ( viewToRemove ) ;
612+ this . forceUpdate ( ) ;
613+ } , VIEW_UNMOUNT_DELAY_MS ) ;
614+ }
615+ }
616+ }
617+
523618 /**
524619 * Handles the case when entering view has no ion-page element yet (waiting for render).
525620 */
0 commit comments