@ionic/react-router 8.7.13-dev.11765569652.136c6b13 → 8.7.13-dev.11765829391.14bc580c

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
@@ -1316,12 +1316,39 @@ class StackManager extends React.PureComponent {
1316
1316
  * Determines if the leaving view item should be unmounted after a transition.
1317
1317
  */
1318
1318
  shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem) {
1319
+ var _a, _b, _c, _d;
1319
1320
  if (!leavingViewItem) {
1320
1321
  return false;
1321
1322
  }
1322
1323
  if (routeInfo.routeAction === 'replace') {
1323
- return true;
1324
+ // For replace actions, decide whether to unmount the leaving view.
1325
+ // The key question is: are these routes in the same navigation context?
1326
+ 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;
1327
+ 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;
1328
+ // Never unmount the root path "/" - it's the main entry point for back navigation
1329
+ if (leavingRoutePath === '/' || leavingRoutePath === '') {
1330
+ return false;
1331
+ }
1332
+ if (enteringRoutePath && leavingRoutePath) {
1333
+ // Get parent paths to check if routes share a common parent
1334
+ const getParentPath = (path) => {
1335
+ const normalized = path.replace(/\/\*$/, ''); // Remove trailing /*
1336
+ const lastSlash = normalized.lastIndexOf('/');
1337
+ return lastSlash > 0 ? normalized.substring(0, lastSlash) : '/';
1338
+ };
1339
+ const enteringParent = getParentPath(enteringRoutePath);
1340
+ const leavingParent = getParentPath(leavingRoutePath);
1341
+ // Unmount if:
1342
+ // 1. Routes are siblings (same parent, e.g., /page1 and /page2, or /foo/page1 and /foo/page2)
1343
+ // 2. Entering is a child of leaving (redirect, e.g., /tabs -> /tabs/tab1)
1344
+ const areSiblings = enteringParent === leavingParent && enteringParent !== '/';
1345
+ const isChildRedirect = enteringRoutePath.startsWith(leavingRoutePath) ||
1346
+ (leavingRoutePath.endsWith('/*') && enteringRoutePath.startsWith(leavingRoutePath.slice(0, -2)));
1347
+ return areSiblings || isChildRedirect;
1348
+ }
1349
+ return false;
1324
1350
  }
1351
+ // For non-replace actions, only unmount for back navigation (not forward push)
1325
1352
  const isForwardPush = routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'forward';
1326
1353
  if (!isForwardPush && routeInfo.routeDirection !== 'none' && enteringViewItem !== leavingViewItem) {
1327
1354
  return true;
@@ -1402,8 +1429,6 @@ class StackManager extends React.PureComponent {
1402
1429
  */
1403
1430
  handleReadyEnteringView(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem) {
1404
1431
  var _a, _b;
1405
- // Ensure the entering view is not hidden from previous navigations
1406
- showIonPageElement(enteringViewItem.ionPageElement);
1407
1432
  // Handle same view item case (e.g., parameterized route changes)
1408
1433
  if (enteringViewItem === leavingViewItem) {
1409
1434
  const routePath = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
@@ -1427,24 +1452,76 @@ class StackManager extends React.PureComponent {
1427
1452
  if (!leavingViewItem && this.props.routeInfo.prevRouteLastPathname) {
1428
1453
  leavingViewItem = this.context.findViewItemByPathname(this.props.routeInfo.prevRouteLastPathname, this.id);
1429
1454
  }
1430
- // Skip transition if entering view is visible and leaving view is not
1431
- if (enteringViewItem.ionPageElement &&
1432
- isViewVisible(enteringViewItem.ionPageElement) &&
1433
- leavingViewItem !== undefined &&
1434
- leavingViewItem.ionPageElement &&
1435
- !isViewVisible(leavingViewItem.ionPageElement)) {
1436
- return;
1437
- }
1455
+ // Ensure the entering view is marked as mounted.
1456
+ // This is critical for views that were previously unmounted (e.g., navigating back to home).
1457
+ // When mount=false, the ViewLifeCycleManager doesn't render the IonPage, so the
1458
+ // ionPageElement reference becomes stale. By setting mount=true, we ensure the view
1459
+ // gets re-rendered and a new IonPage is created.
1460
+ if (!enteringViewItem.mount) {
1461
+ enteringViewItem.mount = true;
1462
+ }
1463
+ // Check visibility state BEFORE showing the entering view.
1464
+ // This must be done before showIonPageElement to get accurate visibility state.
1465
+ const enteringWasVisible = enteringViewItem.ionPageElement && isViewVisible(enteringViewItem.ionPageElement);
1466
+ const leavingIsHidden = leavingViewItem !== undefined && leavingViewItem.ionPageElement && !isViewVisible(leavingViewItem.ionPageElement);
1438
1467
  // Check for duplicate transition
1439
1468
  const currentTransition = {
1440
1469
  enteringId: enteringViewItem.id,
1441
1470
  leavingId: leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.id,
1442
1471
  };
1443
- if (leavingViewItem &&
1472
+ const isDuplicateTransition = leavingViewItem &&
1444
1473
  this.lastTransition &&
1445
1474
  this.lastTransition.leavingId &&
1446
1475
  this.lastTransition.enteringId === currentTransition.enteringId &&
1447
- this.lastTransition.leavingId === currentTransition.leavingId) {
1476
+ this.lastTransition.leavingId === currentTransition.leavingId;
1477
+ // Skip transition if entering view was ALREADY visible and leaving view is not visible.
1478
+ // This indicates the transition has already been performed (e.g., via swipe gesture).
1479
+ // IMPORTANT: Only skip if both ionPageElements are the same as when the transition was last done.
1480
+ // If the leaving view's ionPageElement changed (e.g., component re-rendered with different IonPage),
1481
+ // we should NOT skip because the DOM state is inconsistent.
1482
+ if (enteringWasVisible && leavingIsHidden && isDuplicateTransition) {
1483
+ // For swipe-to-go-back, the transition animation was handled by the gesture.
1484
+ // We still need to set mount=false so React unmounts the leaving view.
1485
+ // Only do this when skipTransition is set (indicating gesture completion).
1486
+ if (this.skipTransition &&
1487
+ shouldUnmountLeavingViewItem &&
1488
+ leavingViewItem &&
1489
+ enteringViewItem !== leavingViewItem) {
1490
+ leavingViewItem.mount = false;
1491
+ // Call transitionPage with duration 0 to trigger ionViewDidLeave lifecycle
1492
+ // which is needed for ViewLifeCycleManager to remove the view.
1493
+ this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back');
1494
+ }
1495
+ // Clear skipTransition since we're not calling transitionPage which normally clears it
1496
+ this.skipTransition = false;
1497
+ // Must call forceUpdate to trigger re-render after mount state change
1498
+ this.forceUpdate();
1499
+ return;
1500
+ }
1501
+ // Ensure the entering view is not hidden from previous navigations
1502
+ // This must happen AFTER the visibility check above
1503
+ showIonPageElement(enteringViewItem.ionPageElement);
1504
+ // Skip if this is a duplicate transition (but visibility state didn't match above)
1505
+ // OR if skipTransition is set (swipe gesture already handled the animation)
1506
+ if (isDuplicateTransition || this.skipTransition) {
1507
+ // For swipe-to-go-back, we still need to handle unmounting even if visibility
1508
+ // conditions aren't fully met (animation might still be in progress)
1509
+ if (this.skipTransition &&
1510
+ shouldUnmountLeavingViewItem &&
1511
+ leavingViewItem &&
1512
+ enteringViewItem !== leavingViewItem) {
1513
+ leavingViewItem.mount = false;
1514
+ // For swipe-to-go-back, we need to call transitionPage with duration 0 to
1515
+ // trigger the ionViewDidLeave lifecycle event. The ViewLifeCycleManager
1516
+ // uses componentCanBeDestroyed callback to remove the view, which is
1517
+ // only called from ionViewDidLeave. Since the gesture animation already
1518
+ // completed before mount=false was set, we need to re-fire the lifecycle.
1519
+ this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back');
1520
+ }
1521
+ // Clear skipTransition since we're not calling transitionPage which normally clears it
1522
+ this.skipTransition = false;
1523
+ // Must call forceUpdate to trigger re-render after mount state change
1524
+ this.forceUpdate();
1448
1525
  return;
1449
1526
  }
1450
1527
  this.lastTransition = currentTransition;
@@ -1456,14 +1533,28 @@ class StackManager extends React.PureComponent {
1456
1533
  }
1457
1534
  }
1458
1535
  /**
1459
- * Handles the delayed unmount of the leaving view item after a replace action.
1536
+ * Handles the delayed unmount of the leaving view item.
1537
+ * For 'replace' actions: handles container route transitions specially.
1538
+ * For back navigation: explicitly unmounts because the ionViewDidLeave lifecycle
1539
+ * fires DURING transitionPage, but mount=false is set AFTER.
1540
+ *
1541
+ * @param routeInfo Current route information
1542
+ * @param enteringViewItem The view being navigated to
1543
+ * @param leavingViewItem The view being navigated from
1460
1544
  */
1461
1545
  handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem) {
1462
1546
  var _a, _b, _c, _d, _e, _f;
1463
- if (routeInfo.routeAction !== 'replace' || !leavingViewItem.ionPageElement) {
1547
+ if (!leavingViewItem.ionPageElement) {
1548
+ return;
1549
+ }
1550
+ // For push/pop actions, do NOT unmount - views are cached for navigation history.
1551
+ // Push: Forward navigation caches views for back navigation
1552
+ // Pop: Back navigation should not unmount the entering view's history
1553
+ // Only 'replace' actions should actually unmount views since they replace history.
1554
+ if (routeInfo.routeAction !== 'replace') {
1464
1555
  return;
1465
1556
  }
1466
- // Check if we should skip removal for nested outlet redirects
1557
+ // For replace actions, check if we should skip removal for nested outlet redirects
1467
1558
  const enteringRoutePath = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
1468
1559
  const leavingRoutePath = (_d = (_c = leavingViewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
1469
1560
  const isEnteringContainerRoute = enteringRoutePath && enteringRoutePath.endsWith('/*');
@@ -1479,6 +1570,8 @@ class StackManager extends React.PureComponent {
1479
1570
  const viewToUnmount = leavingViewItem;
1480
1571
  setTimeout(() => {
1481
1572
  this.context.unMountViewItem(viewToUnmount);
1573
+ // Trigger re-render to remove the view from DOM
1574
+ this.forceUpdate();
1482
1575
  }, VIEW_UNMOUNT_DELAY_MS);
1483
1576
  }
1484
1577
  /**
@@ -1523,6 +1616,8 @@ class StackManager extends React.PureComponent {
1523
1616
  this.transitionPage(routeInfo, latestEnteringView, latestLeavingView !== null && latestLeavingView !== void 0 ? latestLeavingView : undefined);
1524
1617
  if (shouldUnmountLeavingViewItem && latestLeavingView && latestEnteringView !== latestLeavingView) {
1525
1618
  latestLeavingView.mount = false;
1619
+ // Call handleLeavingViewUnmount to ensure the view is properly removed
1620
+ this.handleLeavingViewUnmount(routeInfo, latestEnteringView, latestLeavingView);
1526
1621
  }
1527
1622
  this.forceUpdate();
1528
1623
  }
@@ -1646,7 +1741,12 @@ class StackManager extends React.PureComponent {
1646
1741
  this.context.addViewItem(enteringViewItem);
1647
1742
  }
1648
1743
  // Handle transition based on ion-page element availability
1649
- if (enteringViewItem && enteringViewItem.ionPageElement) {
1744
+ // Check if the ionPageElement is still in the document.
1745
+ // If the view was previously unmounted (mount=false), the ViewLifeCycleManager
1746
+ // removes the React component from the tree, which removes the IonPage from the DOM.
1747
+ // The ionPageElement reference becomes stale and we need to wait for a new one.
1748
+ const ionPageIsInDocument = (enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) && document.body.contains(enteringViewItem.ionPageElement);
1749
+ if (enteringViewItem && ionPageIsInDocument) {
1650
1750
  // Clear waiting state
1651
1751
  if (this.waitingForIonPage) {
1652
1752
  this.waitingForIonPage = false;
@@ -1657,8 +1757,17 @@ class StackManager extends React.PureComponent {
1657
1757
  }
1658
1758
  this.handleReadyEnteringView(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem);
1659
1759
  }
1660
- else if (enteringViewItem && !enteringViewItem.ionPageElement) {
1760
+ else if (enteringViewItem && !ionPageIsInDocument) {
1661
1761
  // Wait for ion-page to mount
1762
+ // This handles both: no ionPageElement, or stale ionPageElement (not in document)
1763
+ // Clear stale reference if the element is no longer in the document
1764
+ if (enteringViewItem.ionPageElement && !document.body.contains(enteringViewItem.ionPageElement)) {
1765
+ enteringViewItem.ionPageElement = undefined;
1766
+ }
1767
+ // Ensure the view is marked as mounted so ViewLifeCycleManager renders the IonPage
1768
+ if (!enteringViewItem.mount) {
1769
+ enteringViewItem.mount = true;
1770
+ }
1662
1771
  this.handleWaitingForIonPage(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem);
1663
1772
  return;
1664
1773
  }
@@ -1760,11 +1869,23 @@ class StackManager extends React.PureComponent {
1760
1869
  }
1761
1870
  const { routeInfo } = this.props;
1762
1871
  const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
1763
- const enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
1872
+ // First try to find the view in the current outlet
1873
+ let enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
1874
+ // If not found in current outlet, search all outlets (for cross-outlet swipe back)
1875
+ if (!enteringViewItem) {
1876
+ enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, undefined, false);
1877
+ }
1878
+ // Check if the ionPageElement is still in the document.
1879
+ // A view might have mount=false but still have its ionPageElement in the DOM
1880
+ // (due to timing differences in unmounting).
1881
+ const ionPageInDocument = Boolean((enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) && document.body.contains(enteringViewItem.ionPageElement));
1764
1882
  const canStartSwipe = !!enteringViewItem &&
1765
- // The root url '/' is treated as the first view item (but is never mounted),
1766
- // so we do not want to swipe back to the root url.
1767
- enteringViewItem.mount &&
1883
+ // Check if we can swipe to this view. Either:
1884
+ // 1. The view is mounted (mount=true), OR
1885
+ // 2. The view's ionPageElement is still in the document
1886
+ // The second case handles views that have been marked for unmount but haven't
1887
+ // actually been removed from the DOM yet.
1888
+ (enteringViewItem.mount || ionPageInDocument) &&
1768
1889
  // When on the first page it is possible for findViewItemByRouteInfo to
1769
1890
  // return the exact same view you are currently on.
1770
1891
  // Make sure that we are not swiping back to the same instances of a view.
@@ -1774,8 +1895,18 @@ class StackManager extends React.PureComponent {
1774
1895
  const onStart = async () => {
1775
1896
  const { routeInfo } = this.props;
1776
1897
  const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
1777
- const enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
1898
+ // First try to find the view in the current outlet, then search all outlets
1899
+ let enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
1900
+ if (!enteringViewItem) {
1901
+ enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, undefined, false);
1902
+ }
1778
1903
  const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
1904
+ // Ensure the entering view is mounted so React keeps rendering it during the gesture.
1905
+ // This is important when the view was previously marked for unmount but its
1906
+ // ionPageElement is still in the DOM.
1907
+ if (enteringViewItem && !enteringViewItem.mount) {
1908
+ enteringViewItem.mount = true;
1909
+ }
1779
1910
  // When the gesture starts, kick off a transition controlled via swipe gesture
1780
1911
  if (enteringViewItem && leavingViewItem) {
1781
1912
  await this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back', true);
@@ -1792,7 +1923,11 @@ class StackManager extends React.PureComponent {
1792
1923
  // Swipe gesture was aborted - re-hide the page that was going to enter
1793
1924
  const { routeInfo } = this.props;
1794
1925
  const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
1795
- const enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
1926
+ // First try to find the view in the current outlet, then search all outlets
1927
+ let enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
1928
+ if (!enteringViewItem) {
1929
+ enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, undefined, false);
1930
+ }
1796
1931
  const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
1797
1932
  // Don't hide if entering and leaving are the same (parameterized route edge case)
1798
1933
  if (enteringViewItem !== leavingViewItem && (enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) !== undefined) {