@ionic/react-router 8.8.1-dev.11773873479.192398dc → 8.8.1-dev.11774029927.130994f5

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
@@ -1495,32 +1495,18 @@ class StackManager extends React.PureComponent {
1495
1495
  return { enteringViewItem, leavingViewItem };
1496
1496
  }
1497
1497
  shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem) {
1498
- var _a, _b, _c, _d;
1498
+ var _a, _b;
1499
1499
  if (!leavingViewItem) {
1500
1500
  return false;
1501
1501
  }
1502
1502
  if (routeInfo.routeAction === 'replace') {
1503
- const enteringRoutePath = (_b = (_a = enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
1504
- const leavingRoutePath = (_d = (_c = leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
1505
- // Never unmount root path - needed for back navigation
1506
- if (leavingRoutePath === '/' || leavingRoutePath === '') {
1503
+ const leavingRoutePath = (_b = (_a = leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
1504
+ // Never unmount root path or views without a path - needed for back navigation
1505
+ if (!leavingRoutePath || leavingRoutePath === '/' || leavingRoutePath === '') {
1507
1506
  return false;
1508
1507
  }
1509
- if (enteringRoutePath && leavingRoutePath) {
1510
- const getParentPath = (path) => {
1511
- const normalized = path.replace(/\/\*$/, '');
1512
- const lastSlash = normalized.lastIndexOf('/');
1513
- return lastSlash > 0 ? normalized.substring(0, lastSlash) : '/';
1514
- };
1515
- const enteringParent = getParentPath(enteringRoutePath);
1516
- const leavingParent = getParentPath(leavingRoutePath);
1517
- // Unmount if routes are siblings or entering is a child of leaving (redirect)
1518
- const areSiblings = enteringParent === leavingParent && enteringParent !== '/';
1519
- const isChildRedirect = enteringRoutePath.startsWith(leavingRoutePath) ||
1520
- (leavingRoutePath.endsWith('/*') && enteringRoutePath.startsWith(leavingRoutePath.slice(0, -2)));
1521
- return areSiblings || isChildRedirect;
1522
- }
1523
- return false;
1508
+ // Replace actions unmount the leaving view since it's being replaced in history.
1509
+ return true;
1524
1510
  }
1525
1511
  // For non-replace actions, only unmount for back navigation
1526
1512
  const isForwardPush = routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'forward';
@@ -1540,6 +1526,19 @@ class StackManager extends React.PureComponent {
1540
1526
  clearTimeout(this.outOfScopeUnmountTimeout);
1541
1527
  this.outOfScopeUnmountTimeout = undefined;
1542
1528
  }
1529
+ // Fire lifecycle events on any visible view before unmounting.
1530
+ // When navigating away from a tabbed section, the parent outlet fires
1531
+ // ionViewDidLeave on the tabs container, but the active tab child page
1532
+ // never receives its own lifecycle events because the core transition
1533
+ // dispatches events with bubbles:false. This ensures tab child pages
1534
+ // get ionViewWillLeave/ionViewDidLeave so useIonViewDidLeave fires.
1535
+ const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
1536
+ allViewsInOutlet.forEach((viewItem) => {
1537
+ if (viewItem.ionPageElement && isViewVisible(viewItem.ionPageElement)) {
1538
+ viewItem.ionPageElement.dispatchEvent(new CustomEvent('ionViewWillLeave', { bubbles: false, cancelable: false }));
1539
+ viewItem.ionPageElement.dispatchEvent(new CustomEvent('ionViewDidLeave', { bubbles: false, cancelable: false }));
1540
+ }
1541
+ });
1543
1542
  // Remove view items from the stack but do NOT apply ion-page-hidden.
1544
1543
  // ion-page-hidden sets display:none which immediately removes content
1545
1544
  // from the layout, causing the parent outlet's leaving page to flash
@@ -1551,7 +1550,6 @@ class StackManager extends React.PureComponent {
1551
1550
  // commit() captures the current DOM state (with content visible) before
1552
1551
  // React processes the removal. The compositor's cached layer is unaffected
1553
1552
  // by subsequent DOM changes during the animation.
1554
- const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
1555
1553
  allViewsInOutlet.forEach((viewItem) => {
1556
1554
  this.context.unMountViewItem(viewItem);
1557
1555
  });
@@ -1691,7 +1689,12 @@ class StackManager extends React.PureComponent {
1691
1689
  const shouldSkipAnimation = this.applySkipAnimationIfNeeded(enteringViewItem, leavingViewItem);
1692
1690
  this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, undefined, false, shouldSkipAnimation);
1693
1691
  if (shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem) {
1694
- leavingViewItem.mount = false;
1692
+ // For non-replace actions (back nav), set mount=false here to hide the view.
1693
+ // For replace actions, handleLeavingViewUnmount sets mount=false only after
1694
+ // its container-to-container guard passes, avoiding zombie state.
1695
+ if (routeInfo.routeAction !== 'replace') {
1696
+ leavingViewItem.mount = false;
1697
+ }
1695
1698
  this.handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem);
1696
1699
  }
1697
1700
  // Clean up orphaned sibling views after replace actions (redirects)
@@ -1702,13 +1705,19 @@ class StackManager extends React.PureComponent {
1702
1705
  */
1703
1706
  handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem) {
1704
1707
  var _a, _b, _c, _d, _e, _f;
1705
- if (!leavingViewItem.ionPageElement) {
1706
- return;
1707
- }
1708
1708
  // Only replace actions unmount views; push/pop cache for navigation history
1709
1709
  if (routeInfo.routeAction !== 'replace') {
1710
1710
  return;
1711
1711
  }
1712
+ if (!leavingViewItem.ionPageElement) {
1713
+ leavingViewItem.mount = false;
1714
+ const viewToUnmount = leavingViewItem;
1715
+ setTimeout(() => {
1716
+ this.context.unMountViewItem(viewToUnmount);
1717
+ this.forceUpdate();
1718
+ }, VIEW_UNMOUNT_DELAY_MS);
1719
+ return;
1720
+ }
1712
1721
  const enteringRoutePath = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
1713
1722
  const leavingRoutePath = (_d = (_c = leavingViewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
1714
1723
  const isEnteringContainerRoute = enteringRoutePath && enteringRoutePath.endsWith('/*');
@@ -1722,6 +1731,7 @@ class StackManager extends React.PureComponent {
1722
1731
  if (isEnteringContainerRoute && !isLeavingSpecificRoute) {
1723
1732
  return;
1724
1733
  }
1734
+ leavingViewItem.mount = false;
1725
1735
  const viewToUnmount = leavingViewItem;
1726
1736
  setTimeout(() => {
1727
1737
  this.context.unMountViewItem(viewToUnmount);
@@ -1764,10 +1774,24 @@ class StackManager extends React.PureComponent {
1764
1774
  }
1765
1775
  const getParent = (path) => {
1766
1776
  const normalized = path.replace(/\/\*$/, '');
1767
- const lastSlash = normalized.lastIndexOf('/');
1768
- return lastSlash > 0 ? normalized.substring(0, lastSlash) : '/';
1777
+ const segments = normalized.split('/').filter(Boolean);
1778
+ // Strip trailing parameter segments (e.g., :id) so that
1779
+ // sibling routes like /items/list/:id and /items/detail/:id
1780
+ // resolve to the same parent (/items).
1781
+ while (segments.length > 0 && segments[segments.length - 1].startsWith(':')) {
1782
+ segments.pop();
1783
+ }
1784
+ segments.pop();
1785
+ return segments.length > 0 ? '/' + segments.join('/') : '/';
1769
1786
  };
1770
- return getParent(path1) === getParent(path2);
1787
+ const parent = getParent(path1);
1788
+ // Exclude root-level routes from sibling detection to avoid unintended
1789
+ // cleanup of unrelated top-level routes. Also covers single-depth param
1790
+ // routes (e.g., /items/:id) which resolve to root after param stripping.
1791
+ if (parent === '/') {
1792
+ return false;
1793
+ }
1794
+ return parent === getParent(path2);
1771
1795
  };
1772
1796
  for (const viewItem of allViewsInOutlet) {
1773
1797
  const viewRoutePath = (_g = (_f = viewItem.reactElement) === null || _f === void 0 ? void 0 : _f.props) === null || _g === void 0 ? void 0 : _g.path;
@@ -1775,11 +1799,19 @@ class StackManager extends React.PureComponent {
1775
1799
  (leavingViewItem && viewItem.id === leavingViewItem.id) ||
1776
1800
  !viewItem.mount ||
1777
1801
  !viewRoutePath ||
1802
+ // Don't clean up container routes when entering a container route
1803
+ // (e.g., /tabs/* and /settings/* coexist for tab switching)
1778
1804
  (viewRoutePath.endsWith('/*') && enteringRoutePath.endsWith('/*'));
1779
1805
  if (shouldSkip) {
1780
1806
  continue;
1781
1807
  }
1782
- if (areSiblingRoutes(enteringRoutePath, viewRoutePath)) {
1808
+ const isOrphanedSpecificRoute = !viewRoutePath.endsWith('/*');
1809
+ // Clean up sibling non-container routes that are no longer reachable.
1810
+ let shouldCleanup = false;
1811
+ if ((isReplaceAction || isPushToContainer) && isOrphanedSpecificRoute) {
1812
+ shouldCleanup = areSiblingRoutes(enteringRoutePath, viewRoutePath);
1813
+ }
1814
+ if (shouldCleanup) {
1783
1815
  hideIonPageElement(viewItem.ionPageElement);
1784
1816
  viewItem.mount = false;
1785
1817
  const viewToRemove = viewItem;
@@ -1858,7 +1890,10 @@ class StackManager extends React.PureComponent {
1858
1890
  });
1859
1891
  // Don't unmount if entering and leaving are the same view item
1860
1892
  if (shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem) {
1861
- leavingViewItem.mount = false;
1893
+ if (routeInfo.routeAction !== 'replace') {
1894
+ leavingViewItem.mount = false;
1895
+ }
1896
+ this.handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem);
1862
1897
  }
1863
1898
  this.forceUpdate();
1864
1899
  return;
@@ -1884,8 +1919,9 @@ class StackManager extends React.PureComponent {
1884
1919
  const shouldSkipAnimation = this.applySkipAnimationIfNeeded(latestEnteringView, latestLeavingView !== null && latestLeavingView !== void 0 ? latestLeavingView : undefined);
1885
1920
  this.transitionPage(routeInfo, latestEnteringView, latestLeavingView !== null && latestLeavingView !== void 0 ? latestLeavingView : undefined, undefined, false, shouldSkipAnimation);
1886
1921
  if (shouldUnmountLeavingViewItem && latestLeavingView && latestEnteringView !== latestLeavingView) {
1887
- latestLeavingView.mount = false;
1888
- // Call handleLeavingViewUnmount to ensure the view is properly removed
1922
+ if (routeInfo.routeAction !== 'replace') {
1923
+ latestLeavingView.mount = false;
1924
+ }
1889
1925
  this.handleLeavingViewUnmount(routeInfo, latestEnteringView, latestLeavingView);
1890
1926
  }
1891
1927
  this.forceUpdate();
@@ -1935,18 +1971,6 @@ class StackManager extends React.PureComponent {
1935
1971
  }
1936
1972
  componentDidMount() {
1937
1973
  this._isMounted = true;
1938
- if (this.clearOutletTimeout) {
1939
- /**
1940
- * The clearOutlet integration with React Router is a bit hacky.
1941
- * It uses a timeout to clear the outlet after a transition.
1942
- * In React v18, components are mounted and unmounted in development mode
1943
- * to check for side effects.
1944
- *
1945
- * This clearTimeout prevents the outlet from being cleared when the component is re-mounted,
1946
- * which should only happen in development mode and as a result of a hot reload.
1947
- */
1948
- clearTimeout(this.clearOutletTimeout);
1949
- }
1950
1974
  if (this.routerOutletElement) {
1951
1975
  this.setupRouterOutlet(this.routerOutletElement);
1952
1976
  this.handlePageTransition(this.props.routeInfo);
@@ -1994,7 +2018,7 @@ class StackManager extends React.PureComponent {
1994
2018
  allViewsInOutlet.forEach((viewItem) => {
1995
2019
  hideIonPageElement(viewItem.ionPageElement);
1996
2020
  });
1997
- this.clearOutletTimeout = this.context.clearOutlet(this.id);
2021
+ this.context.clearOutlet(this.id);
1998
2022
  }
1999
2023
  /**
2000
2024
  * Sets the transition between pages within this router outlet.
@@ -2199,9 +2223,15 @@ class StackManager extends React.PureComponent {
2199
2223
  }
2200
2224
  // View might have mount=false but ionPageElement still in DOM
2201
2225
  const ionPageInDocument = Boolean((enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) && document.body.contains(enteringViewItem.ionPageElement));
2226
+ // For wildcard/parameterized routes, the pattern path (e.g. "/foo/*") will
2227
+ // never equal the resolved pathname (e.g. "/foo/bar"), so the pattern check
2228
+ // alone isn't sufficient. Also, verify the entering view's resolved pathname
2229
+ // differs from the current pathname — if they match, the entering and leaving
2230
+ // views are the same and the swipe gesture shouldn't start.
2202
2231
  const canStartSwipe = !!enteringViewItem &&
2203
2232
  (enteringViewItem.mount || ionPageInDocument) &&
2204
- enteringViewItem.routeData.match.pattern.path !== routeInfo.pathname;
2233
+ enteringViewItem.routeData.match.pattern.path !== routeInfo.pathname &&
2234
+ enteringViewItem.routeData.match.pathname !== routeInfo.pathname;
2205
2235
  return canStartSwipe;
2206
2236
  };
2207
2237
  const onStart = async () => {