@@ -28,18 +28,20 @@ import { extractRouteChildren, getRoutesChildren, isNavigateElement } from './ut
2828const VIEW_UNMOUNT_DELAY_MS = 250 ;
2929
3030/**
31- * Delay in milliseconds to wait for an IonPage element to be mounted before
32- * proceeding with a page transition.
31+ * Delay (ms) to wait for an IonPage to mount before proceeding with a
32+ * page transition. Only container routes (nested outlets with no direct
33+ * IonPage) actually hit this timeout; normal routes clear it early via
34+ * registerIonPage, so a larger value here doesn't affect the happy path.
3335 */
34- const ION_PAGE_WAIT_TIMEOUT_MS = 50 ;
36+ const ION_PAGE_WAIT_TIMEOUT_MS = 300 ;
3537
3638interface StackManagerProps {
3739 routeInfo : RouteInfo ;
3840 id ?: string ;
3941}
4042
4143const isViewVisible = ( el : HTMLElement ) =>
42- ! el . classList . contains ( 'ion-page-invisible' ) && ! el . classList . contains ( 'ion-page-hidden' ) && el . style . display !== 'none ' ;
44+ ! el . classList . contains ( 'ion-page-invisible' ) && ! el . classList . contains ( 'ion-page-hidden' ) && el . style . visibility !== 'hidden ' ;
4345
4446const hideIonPageElement = ( element : HTMLElement | undefined ) : void => {
4547 if ( element ) {
@@ -50,7 +52,7 @@ const hideIonPageElement = (element: HTMLElement | undefined): void => {
5052
5153const showIonPageElement = ( element : HTMLElement | undefined ) : void => {
5254 if ( element ) {
53- element . style . removeProperty ( 'display ' ) ;
55+ element . style . removeProperty ( 'visibility ' ) ;
5456 element . classList . remove ( 'ion-page-hidden' ) ;
5557 element . removeAttribute ( 'aria-hidden' ) ;
5658 }
@@ -446,6 +448,8 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
446448
447449 const shouldSkipAnimation = this . applySkipAnimationIfNeeded ( enteringViewItem , leavingViewItem ) ;
448450
451+ console . log ( `[handleReadyEnteringView] outletId=${ this . id } pathname=${ routeInfo . pathname } lastPathname=${ routeInfo . lastPathname } action=${ routeInfo . routeAction } direction=${ routeInfo . routeDirection } skipAnimation=${ shouldSkipAnimation } entering=${ enteringViewItem . ionPageElement ?. getAttribute ( 'data-pageid' ) } enteringClasses=${ enteringViewItem . ionPageElement ?. className } leaving=${ leavingViewItem ?. ionPageElement ?. getAttribute ( 'data-pageid' ) } leavingClasses=${ leavingViewItem ?. ionPageElement ?. className } leavingVisibility=${ leavingViewItem ?. ionPageElement ?. style . visibility } shouldUnmount=${ shouldUnmountLeavingViewItem } ` ) ;
452+
449453 this . transitionPage ( routeInfo , enteringViewItem , leavingViewItem , undefined , false , shouldSkipAnimation ) ;
450454
451455 if ( shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem ) {
@@ -586,10 +590,12 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
586590 * nested scrollbars (each page has its own IonContent). Top-level outlets
587591 * are unaffected and animate normally.
588592 *
589- * Uses inline display:none rather than ion-page-hidden class because core's
590- * beforeTransition() removes ion-page-hidden via setPageHidden().
591- * Inline display:none survives that removal, keeping the page hidden
592- * until React unmounts it after ionViewDidLeave fires.
593+ * Uses inline visibility:hidden rather than ion-page-hidden class because
594+ * core's beforeTransition() removes ion-page-hidden via setPageHidden().
595+ * Inline visibility:hidden survives that removal, keeping the page hidden
596+ * until React unmounts it after ionViewDidLeave fires. Unlike display:none,
597+ * visibility:hidden preserves element geometry so commit() animations
598+ * can resolve normally.
593599 */
594600 private applySkipAnimationIfNeeded (
595601 enteringViewItem : ViewItem ,
@@ -599,7 +605,8 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
599605 const shouldSkip = isNestedOutlet && ! ! leavingViewItem && enteringViewItem !== leavingViewItem ;
600606
601607 if ( shouldSkip && leavingViewItem ?. ionPageElement ) {
602- leavingViewItem . ionPageElement . style . setProperty ( 'display' , 'none' ) ;
608+ console . log ( `[applySkipAnimation] hiding leaving=${ leavingViewItem . ionPageElement . getAttribute ( 'data-pageid' ) } via visibility:hidden, entering=${ enteringViewItem . ionPageElement ?. getAttribute ( 'data-pageid' ) } ` ) ;
609+ leavingViewItem . ionPageElement . style . setProperty ( 'visibility' , 'hidden' ) ;
603610 leavingViewItem . ionPageElement . setAttribute ( 'aria-hidden' , 'true' ) ;
604611 }
605612
@@ -660,13 +667,16 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
660667 this . ionPageWaitTimeout = undefined ;
661668
662669 if ( ! this . waitingForIonPage ) {
670+ console . log ( `[handleWaitingForIonPage] timeout fired but no longer waiting, outletId=${ this . id } ` ) ;
663671 return ;
664672 }
665673 this . waitingForIonPage = false ;
666674
667675 const latestEnteringView = this . context . findViewItemByRouteInfo ( routeInfo , this . id ) ?? enteringViewItem ;
668676 const latestLeavingView = this . context . findLeavingViewItemByRouteInfo ( routeInfo , this . id ) ?? leavingViewItem ;
669677
678+ console . log ( `[handleWaitingForIonPage] timeout fired outletId=${ this . id } pathname=${ routeInfo . pathname } hasIonPageEl=${ ! ! latestEnteringView ?. ionPageElement } entering=${ latestEnteringView ?. ionPageElement ?. getAttribute ( 'data-pageid' ) } leaving=${ latestLeavingView ?. ionPageElement ?. getAttribute ( 'data-pageid' ) } ` ) ;
679+
670680 if ( latestEnteringView ?. ionPageElement ) {
671681 const shouldSkipAnimation = this . applySkipAnimationIfNeeded ( latestEnteringView , latestLeavingView ?? undefined ) ;
672682 this . transitionPage ( routeInfo , latestEnteringView , latestLeavingView ?? undefined , undefined , false , shouldSkipAnimation ) ;
@@ -865,8 +875,10 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
865875 this . ionPageWaitTimeout = undefined ;
866876 }
867877
878+ console . log ( `[handlePageTransition] READY outletId=${ this . id } pathname=${ routeInfo . pathname } entering=${ enteringViewItem . ionPageElement ?. getAttribute ( 'data-pageid' ) } leaving=${ leavingViewItem ?. ionPageElement ?. getAttribute ( 'data-pageid' ) } ionPageInDoc=${ ionPageIsInDocument } ` ) ;
868879 this . handleReadyEnteringView ( routeInfo , enteringViewItem , leavingViewItem , shouldUnmountLeavingViewItem ) ;
869880 } else if ( enteringViewItem && ! ionPageIsInDocument ) {
881+ console . log ( `[handlePageTransition] WAITING outletId=${ this . id } pathname=${ routeInfo . pathname } enteringHasEl=${ ! ! enteringViewItem . ionPageElement } ionPageInDoc=${ ionPageIsInDocument } ` ) ;
870882 // Wait for ion-page to mount
871883 // This handles both: no ionPageElement, or stale ionPageElement (not in document)
872884 // Clear stale reference if the element is no longer in the document
@@ -961,6 +973,7 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
961973 return ;
962974 }
963975 }
976+ console . log ( `[registerIonPage] outletId=${ this . id } page=${ page . getAttribute ( 'data-pageid' ) } pageClasses="${ page . className } " pathname=${ routeInfo . pathname } ` ) ;
964977 this . handlePageTransition ( routeInfo ) ;
965978 }
966979
@@ -1145,13 +1158,35 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
11451158 }
11461159 }
11471160
1148- await routerOutlet . commit ( enteringEl , leavingEl , {
1149- duration : skipTransition || skipAnimation || directionToUse === undefined ? 0 : undefined ,
1161+ const commitDuration = skipTransition || skipAnimation || directionToUse === undefined ? 0 : undefined ;
1162+ console . log ( `[runCommit] BEFORE commit entering=${ enteringEl . getAttribute ( 'data-pageid' ) } enteringClasses="${ enteringEl . className } " leaving=${ leavingEl ?. getAttribute ( 'data-pageid' ) } leavingClasses="${ leavingEl ?. className } " leavingDisplay="${ leavingEl ?. style . display } " leavingVisibility="${ leavingEl ?. style . visibility } " duration=${ commitDuration } direction=${ directionToUse } skipTransition=${ skipTransition } skipAnimation=${ skipAnimation } ` ) ;
1163+ const commitStart = Date . now ( ) ;
1164+
1165+ // Race commit against a timeout to detect hangs
1166+ const commitPromise = routerOutlet . commit ( enteringEl , leavingEl , {
1167+ duration : commitDuration ,
11501168 direction : directionToUse ,
11511169 showGoBack : ! ! routeInfo . pushedByRoute ,
11521170 progressAnimation,
11531171 animationBuilder : routeInfo . routeAnimation ,
11541172 } ) ;
1173+
1174+ const timeoutMs = 5000 ;
1175+ const timeoutPromise = new Promise < 'timeout' > ( ( resolve ) => setTimeout ( ( ) => resolve ( 'timeout' ) , timeoutMs ) ) ;
1176+ const result = await Promise . race ( [ commitPromise . then ( ( ) => 'done' as const ) , timeoutPromise ] ) ;
1177+
1178+ if ( result === 'timeout' ) {
1179+ console . error ( `[runCommit] TIMEOUT commit hung for ${ timeoutMs } ms! entering=${ enteringEl . getAttribute ( 'data-pageid' ) } enteringClasses="${ enteringEl . className } " leaving=${ leavingEl ?. getAttribute ( 'data-pageid' ) } leavingClasses="${ leavingEl ?. className } " leavingDisplay="${ leavingEl ?. style . display } " leavingVisibility="${ leavingEl ?. style . visibility } "` ) ;
1180+ // Force entering page visible even though commit hung
1181+ enteringEl . classList . remove ( 'ion-page-invisible' ) ;
1182+ console . log ( `[runCommit] forced ion-page-invisible removal on ${ enteringEl . getAttribute ( 'data-pageid' ) } , classes now: "${ enteringEl . className } "` ) ;
1183+ } else {
1184+ console . log ( `[runCommit] AFTER commit resolved in ${ Date . now ( ) - commitStart } ms entering=${ enteringEl . getAttribute ( 'data-pageid' ) } enteringClasses="${ enteringEl . className } "` ) ;
1185+ }
1186+
1187+ if ( ! progressAnimation ) {
1188+ enteringEl . classList . remove ( 'ion-page-invisible' ) ;
1189+ }
11551190 } ;
11561191
11571192 const routerOutlet = this . routerOutletElement ! ;
@@ -1181,6 +1216,8 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
11811216 // flicker caused by commit() briefly unhiding the leaving page
11821217 const isNonAnimatedTransition = directionToUse === undefined && ! progressAnimation ;
11831218
1219+ console . log ( `[transitionPage] outletId=${ this . id } directionToUse=${ directionToUse } isNonAnimatedTransition=${ isNonAnimatedTransition } skipAnimation=${ skipAnimation } hasLeavingEl=${ ! ! leavingEl } entering=${ enteringViewItem . ionPageElement ?. getAttribute ( 'data-pageid' ) } leaving=${ leavingEl ?. getAttribute ( 'data-pageid' ) } ` ) ;
1220+
11841221 if ( isNonAnimatedTransition && leavingEl ) {
11851222 /**
11861223 * Flicker prevention for non-animated transitions:
@@ -1297,8 +1334,8 @@ export class StackManager extends React.PureComponent<StackManagerProps> {
12971334 this . transitionRafIds . push ( outerRafId ) ;
12981335 } ) ;
12991336 } else {
1337+ console . log ( `[transitionPage] taking commit() path for entering=${ enteringViewItem . ionPageElement ?. getAttribute ( 'data-pageid' ) } leaving=${ leavingEl ?. getAttribute ( 'data-pageid' ) } ` ) ;
13001338 await runCommit ( enteringViewItem . ionPageElement , leavingEl ) ;
1301- // For animated transitions, hide leaving element after commit completes
13021339 if ( leavingEl && ! progressAnimation ) {
13031340 leavingEl . classList . add ( 'ion-page-hidden' ) ;
13041341 leavingEl . setAttribute ( 'aria-hidden' , 'true' ) ;
0 commit comments