@@ -262,9 +262,19 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
262262 this . outOfScopeUnmountTimeout = undefined ;
263263 }
264264
265+ // Remove view items from the stack but do NOT apply ion-page-hidden.
266+ // ion-page-hidden sets display:none which immediately removes content
267+ // from the layout, causing the parent outlet's leaving page to flash
268+ // blank during its transition animation (issue #25477).
269+ //
270+ // Removing from the stack triggers React reconciliation via forceUpdate,
271+ // which removes the DOM elements. React batches this re-render after all
272+ // componentDidUpdate calls in the current cycle, so the parent outlet's
273+ // commit() captures the current DOM state (with content visible) before
274+ // React processes the removal. The compositor's cached layer is unaffected
275+ // by subsequent DOM changes during the animation.
265276 const allViewsInOutlet = this . context . getViewItemsForOutlet ( this . id ) ;
266277 allViewsInOutlet . forEach ( ( viewItem ) => {
267- hideIonPageElement ( viewItem . ionPageElement ) ;
268278 this . context . unMountViewItem ( viewItem ) ;
269279 } ) ;
270280
@@ -580,13 +590,15 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
580590
581591 /**
582592 * Determines whether to skip the transition animation and, if so, immediately
583- * hides the leaving view with inline `display:none `.
593+ * hides the leaving view with inline `visibility:hidden `.
584594 *
585- * Skips transitions in outlets nested inside a parent IonPage. These outlets
586- * render pages inside a parent page's content area. The MD animation shows
587- * both entering and leaving pages simultaneously, causing text overlap and
588- * nested scrollbars (each page has its own IonContent). Top-level outlets
589- * are unaffected and animate normally.
595+ * Skips transitions only for outlets nested inside a parent IonPage's content
596+ * area (i.e., an ion-content sits between the outlet and the .ion-page). These
597+ * outlets render child pages inside a parent page's scrollable area, and the MD
598+ * animation shows both entering and leaving pages simultaneously — causing text
599+ * overlap and nested scrollbars. Standard page-level outlets (tabs, routing,
600+ * swipe-to-go-back) animate normally even though they sit inside a framework-
601+ * managed .ion-page wrapper from the parent outlet's view stack.
590602 *
591603 * Uses inline visibility:hidden rather than ion-page-hidden class because
592604 * core's beforeTransition() removes ion-page-hidden via setPageHidden().
@@ -599,8 +611,22 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
599611 enteringViewItem : ViewItem ,
600612 leavingViewItem : ViewItem | undefined
601613 ) : boolean {
602- const isNestedOutlet = ! ! this . routerOutletElement ?. closest ( '.ion-page' ) ;
603- const shouldSkip = isNestedOutlet && ! ! leavingViewItem && enteringViewItem !== leavingViewItem ;
614+ // Only skip for outlets genuinely nested inside a page's content area.
615+ // Walk from the outlet up to the nearest .ion-page; if an ion-content
616+ // sits in between, the outlet is inside scrollable page content and
617+ // animating would cause overlapping pages with duplicate scrollbars.
618+ let isInsidePageContent = false ;
619+ let el : HTMLElement | null = this . routerOutletElement ?. parentElement ?? null ;
620+ while ( el ) {
621+ if ( el . classList . contains ( 'ion-page' ) ) break ;
622+ if ( el . tagName === 'ION-CONTENT' ) {
623+ isInsidePageContent = true ;
624+ break ;
625+ }
626+ el = el . parentElement ;
627+ }
628+
629+ const shouldSkip = isInsidePageContent && ! ! leavingViewItem && enteringViewItem !== leavingViewItem ;
604630
605631 if ( shouldSkip && leavingViewItem ?. ionPageElement ) {
606632 leavingViewItem . ionPageElement . style . setProperty ( 'visibility' , 'hidden' ) ;
0 commit comments