@ionic/react-router 8.8.1-dev.11773665268.1511a6c7 → 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
@@ -701,15 +701,9 @@ class ReactRouterViewStack extends ViewStacks {
701
701
  if (existingIsIndexRoute && newIsIndexRoute) {
702
702
  return true;
703
703
  }
704
- // Reuse empty-path routes (path="") that are not index routes.
705
- // These function like index routes and should be reused to prevent
706
- // duplicate views when navigating back to the default path.
707
- if (existingPath === '' && routePath === '' && !existingIsIndexRoute && !newIsIndexRoute) {
708
- return true;
709
- }
710
704
  // Reuse view items with the same path
711
705
  // Special case: reuse tabs/* and other specific wildcard routes
712
- // Don't reuse generic catch-all wildcards (*)
706
+ // Don't reuse index routes (empty path) or generic catch-all wildcards (*)
713
707
  if (existingPath === routePath && existingPath !== '' && existingPath !== '*') {
714
708
  // Parameterized routes need pathname matching to ensure /details/1 and /details/2
715
709
  // get separate view items. For wildcard routes (e.g., user/:userId/*), compare
@@ -1546,9 +1540,19 @@ class StackManager extends React.PureComponent {
1546
1540
  clearTimeout(this.outOfScopeUnmountTimeout);
1547
1541
  this.outOfScopeUnmountTimeout = undefined;
1548
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.
1549
1554
  const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
1550
1555
  allViewsInOutlet.forEach((viewItem) => {
1551
- hideIonPageElement(viewItem.ionPageElement);
1552
1556
  this.context.unMountViewItem(viewItem);
1553
1557
  });
1554
1558
  this.forceUpdate();
@@ -1788,13 +1792,15 @@ class StackManager extends React.PureComponent {
1788
1792
  }
1789
1793
  /**
1790
1794
  * Determines whether to skip the transition animation and, if so, immediately
1791
- * hides the leaving view with inline `display:none`.
1795
+ * hides the leaving view with inline `visibility:hidden`.
1792
1796
  *
1793
- * Skips transitions in outlets nested inside a parent IonPage. These outlets
1794
- * render pages inside a parent page's content area. The MD animation shows
1795
- * both entering and leaving pages simultaneously, causing text overlap and
1796
- * nested scrollbars (each page has its own IonContent). Top-level outlets
1797
- * 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.
1798
1804
  *
1799
1805
  * Uses inline visibility:hidden rather than ion-page-hidden class because
1800
1806
  * core's beforeTransition() removes ion-page-hidden via setPageHidden().
@@ -1804,9 +1810,23 @@ class StackManager extends React.PureComponent {
1804
1810
  * can resolve normally.
1805
1811
  */
1806
1812
  applySkipAnimationIfNeeded(enteringViewItem, leavingViewItem) {
1807
- var _a;
1808
- const isNestedOutlet = !!((_a = this.routerOutletElement) === null || _a === void 0 ? void 0 : _a.closest('.ion-page'));
1809
- 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;
1810
1830
  if (shouldSkip && (leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement)) {
1811
1831
  leavingViewItem.ionPageElement.style.setProperty('visibility', 'hidden');
1812
1832
  leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
@@ -2602,6 +2622,27 @@ const filterUndefinedParams = (params) => {
2602
2622
  }
2603
2623
  return result;
2604
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
+ };
2605
2646
  const areParamsEqual = (a, b) => {
2606
2647
  const paramsA = a || {};
2607
2648
  const paramsB = b || {};
@@ -2775,7 +2816,22 @@ const IonRouter = ({ children, registerHistoryListener }) => {
2775
2816
  // Back navigation. Record current location key for potential forward
2776
2817
  forwardStack.current.push(currentLocationKeyRef.current);
2777
2818
  const prevInfo = locationHistory.current.findLastLocation(currentRoute);
2778
- 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
+ }
2779
2835
  }
2780
2836
  else {
2781
2837
  // It's a non-linear history path like a direct link.
@@ -2831,9 +2887,19 @@ const IonRouter = ({ children, registerHistoryListener }) => {
2831
2887
  // This preserves tab context for same-tab navigation while allowing cross-tab navigation.
2832
2888
  routeInfo.tab = routeInfo.tab || leavingLocationInfo.tab;
2833
2889
  routeInfo.pushedByRoute = leavingLocationInfo.pathname;
2834
- // 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;
2835
2900
  }
2836
2901
  else if (routeInfo.routeAction === 'pop') {
2902
+ // Triggered by a browser back button or handleNavigateBack.
2837
2903
  // Find the route that pushed this one.
2838
2904
  const r = locationHistory.current.findLastLocation(routeInfo);
2839
2905
  routeInfo.pushedByRoute = r === null || r === void 0 ? void 0 : r.pushedByRoute;
@@ -2906,8 +2972,10 @@ const IonRouter = ({ children, registerHistoryListener }) => {
2906
2972
  const handleResetTab = (tab, originalHref, originalRouteOptions) => {
2907
2973
  const routeInfo = locationHistory.current.getFirstRouteInfoForTab(tab);
2908
2974
  if (routeInfo) {
2975
+ const [pathname, search] = originalHref.split('?');
2909
2976
  const newRouteInfo = Object.assign({}, routeInfo);
2910
- newRouteInfo.pathname = originalHref;
2977
+ newRouteInfo.pathname = pathname;
2978
+ newRouteInfo.search = search ? '?' + search : '';
2911
2979
  newRouteInfo.routeOptions = originalRouteOptions;
2912
2980
  incomingRouteParams.current = Object.assign(Object.assign({}, newRouteInfo), { routeAction: 'pop', routeDirection: 'back' });
2913
2981
  navigate(newRouteInfo.pathname + (newRouteInfo.search || ''));
@@ -2934,21 +3002,23 @@ const IonRouter = ({ children, registerHistoryListener }) => {
2934
3002
  * e.g., `/tabs/home` → `/tabs/home`
2935
3003
  */
2936
3004
  if (routeInfo.pathname === pathname) {
2937
- incomingRouteParams.current = Object.assign(Object.assign({}, routeParams), { routeOptions });
2938
- 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 || ''));
2939
3008
  /**
2940
3009
  * User is navigating to a different tab.
2941
3010
  * e.g., `/tabs/home` → `/tabs/settings`
2942
3011
  */
2943
3012
  }
2944
3013
  else {
2945
- 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 });
2946
3015
  navigate(pathname + (search ? '?' + search : ''));
2947
3016
  }
2948
3017
  // User has not navigated to this tab before.
2949
3018
  }
2950
3019
  else {
2951
- handleNavigate(pathname, 'push', 'none', undefined, routeOptions, tab);
3020
+ const fullPath = pathname + (search ? '?' + search : '');
3021
+ handleNavigate(fullPath, 'push', 'none', undefined, routeOptions, tab);
2952
3022
  }
2953
3023
  };
2954
3024
  /**