@ionic/react-router 8.8.1-dev.11773676615.1d6c4cf7 → 8.8.1-dev.11773873479.192398dc

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
@@ -1540,9 +1540,19 @@ class StackManager extends React.PureComponent {
1540
1540
  clearTimeout(this.outOfScopeUnmountTimeout);
1541
1541
  this.outOfScopeUnmountTimeout = undefined;
1542
1542
  }
1543
+ // Remove view items from the stack but do NOT apply ion-page-hidden.
1544
+ // ion-page-hidden sets display:none which immediately removes content
1545
+ // from the layout, causing the parent outlet's leaving page to flash
1546
+ // blank during its transition animation (issue #25477).
1547
+ //
1548
+ // Removing from the stack triggers React reconciliation via forceUpdate,
1549
+ // which removes the DOM elements. React batches this re-render after all
1550
+ // componentDidUpdate calls in the current cycle, so the parent outlet's
1551
+ // commit() captures the current DOM state (with content visible) before
1552
+ // React processes the removal. The compositor's cached layer is unaffected
1553
+ // by subsequent DOM changes during the animation.
1543
1554
  const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
1544
1555
  allViewsInOutlet.forEach((viewItem) => {
1545
- hideIonPageElement(viewItem.ionPageElement);
1546
1556
  this.context.unMountViewItem(viewItem);
1547
1557
  });
1548
1558
  this.forceUpdate();
@@ -1782,13 +1792,15 @@ class StackManager extends React.PureComponent {
1782
1792
  }
1783
1793
  /**
1784
1794
  * Determines whether to skip the transition animation and, if so, immediately
1785
- * hides the leaving view with inline `display:none`.
1795
+ * hides the leaving view with inline `visibility:hidden`.
1786
1796
  *
1787
- * Skips transitions in outlets nested inside a parent IonPage. These outlets
1788
- * render pages inside a parent page's content area. The MD animation shows
1789
- * both entering and leaving pages simultaneously, causing text overlap and
1790
- * nested scrollbars (each page has its own IonContent). Top-level outlets
1791
- * are unaffected and animate normally.
1797
+ * Skips transitions only for outlets nested inside a parent IonPage's content
1798
+ * area (i.e., an ion-content sits between the outlet and the .ion-page). These
1799
+ * outlets render child pages inside a parent page's scrollable area, and the MD
1800
+ * animation shows both entering and leaving pages simultaneously causing text
1801
+ * overlap and nested scrollbars. Standard page-level outlets (tabs, routing,
1802
+ * swipe-to-go-back) animate normally even though they sit inside a framework-
1803
+ * managed .ion-page wrapper from the parent outlet's view stack.
1792
1804
  *
1793
1805
  * Uses inline visibility:hidden rather than ion-page-hidden class because
1794
1806
  * core's beforeTransition() removes ion-page-hidden via setPageHidden().
@@ -1798,9 +1810,23 @@ class StackManager extends React.PureComponent {
1798
1810
  * can resolve normally.
1799
1811
  */
1800
1812
  applySkipAnimationIfNeeded(enteringViewItem, leavingViewItem) {
1801
- var _a;
1802
- const isNestedOutlet = !!((_a = this.routerOutletElement) === null || _a === void 0 ? void 0 : _a.closest('.ion-page'));
1803
- const shouldSkip = isNestedOutlet && !!leavingViewItem && enteringViewItem !== leavingViewItem;
1813
+ var _a, _b;
1814
+ // Only skip for outlets genuinely nested inside a page's content area.
1815
+ // Walk from the outlet up to the nearest .ion-page; if an ion-content
1816
+ // sits in between, the outlet is inside scrollable page content and
1817
+ // animating would cause overlapping pages with duplicate scrollbars.
1818
+ let isInsidePageContent = false;
1819
+ let el = (_b = (_a = this.routerOutletElement) === null || _a === void 0 ? void 0 : _a.parentElement) !== null && _b !== void 0 ? _b : null;
1820
+ while (el) {
1821
+ if (el.classList.contains('ion-page'))
1822
+ break;
1823
+ if (el.tagName === 'ION-CONTENT') {
1824
+ isInsidePageContent = true;
1825
+ break;
1826
+ }
1827
+ el = el.parentElement;
1828
+ }
1829
+ const shouldSkip = isInsidePageContent && !!leavingViewItem && enteringViewItem !== leavingViewItem;
1804
1830
  if (shouldSkip && (leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement)) {
1805
1831
  leavingViewItem.ionPageElement.style.setProperty('visibility', 'hidden');
1806
1832
  leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
@@ -2596,6 +2622,27 @@ const filterUndefinedParams = (params) => {
2596
2622
  }
2597
2623
  return result;
2598
2624
  };
2625
+ /**
2626
+ * Checks if a POP event is a multi-step back navigation (navigate(-n) where n > 1).
2627
+ * Walks the pushedByRoute chain from prevInfo to verify the destination is an ancestor
2628
+ * in the same navigation chain. This distinguishes multi-step back from tab-crossing
2629
+ * back navigation where prevInfo.pathname also differs from the browser destination.
2630
+ */
2631
+ const checkIsMultiStepBack = (prevInfo, destinationPathname, history) => {
2632
+ if (!prevInfo || prevInfo.pathname === destinationPathname)
2633
+ return false;
2634
+ const visited = new Set();
2635
+ let walker = prevInfo;
2636
+ while (walker === null || walker === void 0 ? void 0 : walker.pushedByRoute) {
2637
+ if (visited.has(walker.id))
2638
+ break; // cycle guard
2639
+ visited.add(walker.id);
2640
+ if (walker.pushedByRoute === destinationPathname)
2641
+ return true;
2642
+ walker = history.findLastLocation(walker);
2643
+ }
2644
+ return false;
2645
+ };
2599
2646
  const areParamsEqual = (a, b) => {
2600
2647
  const paramsA = a || {};
2601
2648
  const paramsB = b || {};
@@ -2769,7 +2816,22 @@ const IonRouter = ({ children, registerHistoryListener }) => {
2769
2816
  // Back navigation. Record current location key for potential forward
2770
2817
  forwardStack.current.push(currentLocationKeyRef.current);
2771
2818
  const prevInfo = locationHistory.current.findLastLocation(currentRoute);
2772
- incomingRouteParams.current = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back' });
2819
+ const isMultiStepBack = checkIsMultiStepBack(prevInfo, location.pathname, locationHistory.current);
2820
+ if (isMultiStepBack) {
2821
+ const destinationInfo = locationHistory.current.findLastLocationByPathname(location.pathname);
2822
+ incomingRouteParams.current = Object.assign(Object.assign({}, (destinationInfo || {})), { routeAction: 'pop', routeDirection: 'back' });
2823
+ }
2824
+ else if (prevInfo && prevInfo.pathname !== location.pathname && currentRoute.tab) {
2825
+ // Browser POP destination differs from within-tab back target.
2826
+ // Sync URL via replace, like handleNavigateBack's non-linear path (#25141).
2827
+ incomingRouteParams.current = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back' });
2828
+ forwardStack.current = [];
2829
+ handleNavigate(prevInfo.pathname + (prevInfo.search || ''), 'pop', 'back', undefined, undefined, prevInfo.tab);
2830
+ return;
2831
+ }
2832
+ else {
2833
+ incomingRouteParams.current = Object.assign(Object.assign({}, prevInfo), { routeAction: 'pop', routeDirection: 'back' });
2834
+ }
2773
2835
  }
2774
2836
  else {
2775
2837
  // It's a non-linear history path like a direct link.
@@ -2825,9 +2887,19 @@ const IonRouter = ({ children, registerHistoryListener }) => {
2825
2887
  // This preserves tab context for same-tab navigation while allowing cross-tab navigation.
2826
2888
  routeInfo.tab = routeInfo.tab || leavingLocationInfo.tab;
2827
2889
  routeInfo.pushedByRoute = leavingLocationInfo.pathname;
2828
- // Triggered by a browser back button or handleNavigateBack.
2890
+ }
2891
+ else if (routeInfo.routeAction === 'push' &&
2892
+ routeInfo.routeDirection === 'none' &&
2893
+ routeInfo.tab === leavingLocationInfo.tab) {
2894
+ // Push with routerDirection="none" within the same tab (or non-tab) context.
2895
+ // Still needs pushedByRoute so the back button can navigate back correctly.
2896
+ // Cross-tab navigations with direction "none" are handled by the tab-switching
2897
+ // block below which has different pushedByRoute semantics.
2898
+ routeInfo.tab = routeInfo.tab || leavingLocationInfo.tab;
2899
+ routeInfo.pushedByRoute = leavingLocationInfo.pathname;
2829
2900
  }
2830
2901
  else if (routeInfo.routeAction === 'pop') {
2902
+ // Triggered by a browser back button or handleNavigateBack.
2831
2903
  // Find the route that pushed this one.
2832
2904
  const r = locationHistory.current.findLastLocation(routeInfo);
2833
2905
  routeInfo.pushedByRoute = r === null || r === void 0 ? void 0 : r.pushedByRoute;
@@ -2900,8 +2972,10 @@ const IonRouter = ({ children, registerHistoryListener }) => {
2900
2972
  const handleResetTab = (tab, originalHref, originalRouteOptions) => {
2901
2973
  const routeInfo = locationHistory.current.getFirstRouteInfoForTab(tab);
2902
2974
  if (routeInfo) {
2975
+ const [pathname, search] = originalHref.split('?');
2903
2976
  const newRouteInfo = Object.assign({}, routeInfo);
2904
- newRouteInfo.pathname = originalHref;
2977
+ newRouteInfo.pathname = pathname;
2978
+ newRouteInfo.search = search ? '?' + search : '';
2905
2979
  newRouteInfo.routeOptions = originalRouteOptions;
2906
2980
  incomingRouteParams.current = Object.assign(Object.assign({}, newRouteInfo), { routeAction: 'pop', routeDirection: 'back' });
2907
2981
  navigate(newRouteInfo.pathname + (newRouteInfo.search || ''));
@@ -2928,21 +3002,23 @@ const IonRouter = ({ children, registerHistoryListener }) => {
2928
3002
  * e.g., `/tabs/home` → `/tabs/home`
2929
3003
  */
2930
3004
  if (routeInfo.pathname === pathname) {
2931
- incomingRouteParams.current = Object.assign(Object.assign({}, routeParams), { routeOptions });
2932
- navigate(routeInfo.pathname + (routeInfo.search || ''));
3005
+ const newSearch = search ? '?' + search : routeInfo.search;
3006
+ incomingRouteParams.current = Object.assign(Object.assign({}, routeParams), { search: newSearch || '', routeOptions });
3007
+ navigate(routeInfo.pathname + (newSearch || ''));
2933
3008
  /**
2934
3009
  * User is navigating to a different tab.
2935
3010
  * e.g., `/tabs/home` → `/tabs/settings`
2936
3011
  */
2937
3012
  }
2938
3013
  else {
2939
- incomingRouteParams.current = Object.assign(Object.assign({}, routeParams), { pathname, search: search ? '?' + search : undefined, routeOptions });
3014
+ incomingRouteParams.current = Object.assign(Object.assign({}, routeParams), { pathname, search: search ? '?' + search : '', routeOptions });
2940
3015
  navigate(pathname + (search ? '?' + search : ''));
2941
3016
  }
2942
3017
  // User has not navigated to this tab before.
2943
3018
  }
2944
3019
  else {
2945
- handleNavigate(pathname, 'push', 'none', undefined, routeOptions, tab);
3020
+ const fullPath = pathname + (search ? '?' + search : '');
3021
+ handleNavigate(fullPath, 'push', 'none', undefined, routeOptions, tab);
2946
3022
  }
2947
3023
  };
2948
3024
  /**