@ionic/react-router 8.8.1-dev.11774384072.1e807ca8 → 8.8.1-dev.11774636992.1511ce70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1406,6 +1406,12 @@ class StackManager extends React.PureComponent {
1406
1406
  this._isMounted = false;
1407
1407
  /** In-flight requestAnimationFrame IDs from transitionPage, cancelled on unmount. */
1408
1408
  this.transitionRafIds = [];
1409
+ /**
1410
+ * Monotonically increasing counter incremented at the start of each transitionPage call.
1411
+ * Used to detect when an async commit() resolves after a newer transition has already run,
1412
+ * preventing the stale commit from hiding an element that the newer transition made visible.
1413
+ */
1414
+ this.transitionGeneration = 0;
1409
1415
  this.outletMountPath = undefined;
1410
1416
  /**
1411
1417
  * Whether this outlet is at the root level (no parent route matches).
@@ -1993,7 +1999,15 @@ class StackManager extends React.PureComponent {
1993
1999
  this._isMounted = true;
1994
2000
  if (this.routerOutletElement) {
1995
2001
  this.setupRouterOutlet(this.routerOutletElement);
1996
- this.handlePageTransition(this.props.routeInfo);
2002
+ // Defer to a microtask to avoid calling forceUpdate() synchronously during
2003
+ // React 19's reappearLayoutEffects phase, which re-runs componentDidMount
2004
+ // without a preceding componentWillUnmount and causes "Maximum update depth exceeded".
2005
+ const routeInfo = this.props.routeInfo;
2006
+ queueMicrotask(() => {
2007
+ if (this._isMounted && this.props.routeInfo.pathname === routeInfo.pathname) {
2008
+ this.handlePageTransition(routeInfo);
2009
+ }
2010
+ });
1997
2011
  }
1998
2012
  }
1999
2013
  componentDidUpdate(prevProps) {
@@ -2326,6 +2340,7 @@ class StackManager extends React.PureComponent {
2326
2340
  * overlapping content during the transition. Defaults to `false`.
2327
2341
  */
2328
2342
  async transitionPage(routeInfo, enteringViewItem, leavingViewItem, direction, progressAnimation = false, skipAnimation = false) {
2343
+ const myGeneration = ++this.transitionGeneration;
2329
2344
  const runCommit = async (enteringEl, leavingEl) => {
2330
2345
  const skipTransition = this.skipTransition;
2331
2346
  /**
@@ -2375,10 +2390,22 @@ class StackManager extends React.PureComponent {
2375
2390
  const timeoutMs = 5000;
2376
2391
  const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve('timeout'), timeoutMs));
2377
2392
  const result = await Promise.race([commitPromise.then(() => 'done'), timeoutPromise]);
2393
+ // Bail out if the component unmounted during the commit animation
2394
+ if (!this._isMounted)
2395
+ return;
2378
2396
  if (result === 'timeout') {
2379
2397
  // Force entering page visible even though commit hung
2380
2398
  enteringEl.classList.remove('ion-page-invisible');
2381
2399
  }
2400
+ /**
2401
+ * If a newer transitionPage call ran while this commit was in-flight (e.g., a tab
2402
+ * switch fired during a forward animation), the core commit may have applied
2403
+ * ion-page-hidden to leavingEl even though the newer transition already made it
2404
+ * visible. Undo that stale hide so the newer transition's DOM state wins.
2405
+ */
2406
+ if (myGeneration !== this.transitionGeneration && leavingEl && leavingEl === this.transitionEnteringElement) {
2407
+ showIonPageElement(leavingEl);
2408
+ }
2382
2409
  if (!progressAnimation) {
2383
2410
  enteringEl.classList.remove('ion-page-invisible');
2384
2411
  }
@@ -2387,6 +2414,7 @@ class StackManager extends React.PureComponent {
2387
2414
  const routeInfoFallbackDirection = routeInfo.routeDirection === 'none' || routeInfo.routeDirection === 'root' ? undefined : routeInfo.routeDirection;
2388
2415
  const directionToUse = direction !== null && direction !== void 0 ? direction : routeInfoFallbackDirection;
2389
2416
  if (enteringViewItem && enteringViewItem.ionPageElement && this.routerOutletElement) {
2417
+ this.transitionEnteringElement = enteringViewItem.ionPageElement;
2390
2418
  if (leavingViewItem && leavingViewItem.ionPageElement && enteringViewItem === leavingViewItem) {
2391
2419
  // Clone page for same-view transitions (e.g., /user/1 → /user/2)
2392
2420
  const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname, undefined, this.outletMountPath);
@@ -2487,15 +2515,22 @@ class StackManager extends React.PureComponent {
2487
2515
  if (!this._isMounted)
2488
2516
  return;
2489
2517
  // Swap visibility synchronously - show entering, hide leaving
2518
+ // Skip hiding if a newer transition already made leavingEl the entering view
2490
2519
  enteringEl.classList.remove('ion-page-invisible');
2491
- leavingEl.classList.add('ion-page-hidden');
2492
- leavingEl.setAttribute('aria-hidden', 'true');
2520
+ if (myGeneration === this.transitionGeneration || leavingEl !== this.transitionEnteringElement) {
2521
+ leavingEl.classList.add('ion-page-hidden');
2522
+ leavingEl.setAttribute('aria-hidden', 'true');
2523
+ }
2493
2524
  }
2494
2525
  else {
2495
2526
  await runCommit(enteringViewItem.ionPageElement, leavingEl);
2496
2527
  if (leavingEl && !progressAnimation) {
2497
- leavingEl.classList.add('ion-page-hidden');
2498
- leavingEl.setAttribute('aria-hidden', 'true');
2528
+ // Skip hiding if a newer transition already made leavingEl the entering view
2529
+ // runCommit's generation check has already restored its visibility in that case
2530
+ if (myGeneration === this.transitionGeneration || leavingEl !== this.transitionEnteringElement) {
2531
+ leavingEl.classList.add('ion-page-hidden');
2532
+ leavingEl.setAttribute('aria-hidden', 'true');
2533
+ }
2499
2534
  }
2500
2535
  }
2501
2536
  }