@ionic/react-router 8.8.1-dev.11774029927.130994f5 → 8.8.1-dev.11774384072.1e807ca8

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
@@ -1556,6 +1556,26 @@ class StackManager extends React.PureComponent {
1556
1556
  this.forceUpdate();
1557
1557
  return true;
1558
1558
  }
1559
+ /**
1560
+ * Handles root navigation by unmounting all non-entering views in this outlet.
1561
+ * Fires ionViewWillLeave / ionViewDidLeave only on views that are currently visible.
1562
+ * Views that are mounted but not visible (e.g., pages earlier in the back stack)
1563
+ * are silently unmounted without lifecycle events, consistent with the behavior
1564
+ * of out-of-scope outlet cleanup.
1565
+ */
1566
+ handleRootNavigation(enteringViewItem) {
1567
+ const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
1568
+ allViewsInOutlet.forEach((viewItem) => {
1569
+ if (viewItem === enteringViewItem) {
1570
+ return;
1571
+ }
1572
+ if (viewItem.ionPageElement && isViewVisible(viewItem.ionPageElement)) {
1573
+ viewItem.ionPageElement.dispatchEvent(new CustomEvent('ionViewWillLeave', { bubbles: false, cancelable: false }));
1574
+ viewItem.ionPageElement.dispatchEvent(new CustomEvent('ionViewDidLeave', { bubbles: false, cancelable: false }));
1575
+ }
1576
+ this.context.unMountViewItem(viewItem);
1577
+ });
1578
+ }
1559
1579
  /**
1560
1580
  * Handles nested outlet with relative routes but no parent path. Returns true to abort.
1561
1581
  */
@@ -2044,14 +2064,20 @@ class StackManager extends React.PureComponent {
2044
2064
  // Find entering and leaving view items
2045
2065
  const viewItems = this.findViewItems(routeInfo);
2046
2066
  let enteringViewItem = viewItems.enteringViewItem;
2047
- const leavingViewItem = viewItems.leavingViewItem;
2048
- const shouldUnmountLeavingViewItem = this.shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem);
2067
+ let leavingViewItem = viewItems.leavingViewItem;
2068
+ let shouldUnmountLeavingViewItem = this.shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem);
2049
2069
  // Get parent path for nested outlets
2050
2070
  const parentPath = this.getParentPath();
2051
2071
  // Handle out-of-scope outlet (route outside mount path)
2052
2072
  if (this.handleOutOfScopeOutlet(routeInfo)) {
2053
2073
  return;
2054
2074
  }
2075
+ // Handle root navigation: unmount all non-entering views
2076
+ if (routeInfo.routeDirection === 'root') {
2077
+ this.handleRootNavigation(enteringViewItem);
2078
+ leavingViewItem = undefined;
2079
+ shouldUnmountLeavingViewItem = false;
2080
+ }
2055
2081
  // Clear any pending out-of-scope unmount timeout
2056
2082
  if (this.outOfScopeUnmountTimeout) {
2057
2083
  clearTimeout(this.outOfScopeUnmountTimeout);
@@ -2769,34 +2795,14 @@ const IonRouter = ({ children, registerHistoryListener }) => {
2769
2795
  * @param action The action that triggered the history change.
2770
2796
  */
2771
2797
  const handleHistoryChange = (location, action) => {
2772
- var _a, _b, _c, _d, _e;
2773
- let leavingLocationInfo;
2798
+ var _a, _b, _c, _d;
2774
2799
  /**
2775
- * A programmatic navigation was triggered.
2776
- * e.g., `<Navigate />`, `navigate()`, or `handleNavigate()`
2800
+ * The leaving location is always the current route, for both programmatic
2801
+ * and external navigations. Using `previous()` for replace actions was
2802
+ * incorrect: it caused the equality check below to skip navigation when
2803
+ * the replace destination matched the entry two slots back in history.
2777
2804
  */
2778
- if (incomingRouteParams.current) {
2779
- /**
2780
- * The current history entry is overwritten, so the previous entry
2781
- * is the one we are leaving.
2782
- */
2783
- if (((_a = incomingRouteParams.current) === null || _a === void 0 ? void 0 : _a.routeAction) === 'replace') {
2784
- leavingLocationInfo = locationHistory.current.previous();
2785
- }
2786
- else {
2787
- // If the action is 'push' or 'pop', we want to use the current route.
2788
- leavingLocationInfo = locationHistory.current.current();
2789
- }
2790
- }
2791
- else {
2792
- /**
2793
- * An external navigation was triggered
2794
- * e.g., browser back/forward button or direct link
2795
- *
2796
- * The leaving location is the current route.
2797
- */
2798
- leavingLocationInfo = locationHistory.current.current();
2799
- }
2805
+ const leavingLocationInfo = locationHistory.current.current();
2800
2806
  const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
2801
2807
  if (leavingUrl !== location.pathname + location.search) {
2802
2808
  if (!incomingRouteParams.current) {
@@ -2898,7 +2904,7 @@ const IonRouter = ({ children, registerHistoryListener }) => {
2898
2904
  * An existing id indicates that it's re-activating an existing route.
2899
2905
  * e.g., tab switching or navigating back to a previous route
2900
2906
  */
2901
- if ((_b = incomingRouteParams.current) === null || _b === void 0 ? void 0 : _b.id) {
2907
+ if ((_a = incomingRouteParams.current) === null || _a === void 0 ? void 0 : _a.id) {
2902
2908
  routeInfo = Object.assign(Object.assign({}, incomingRouteParams.current), { lastPathname: leavingLocationInfo.pathname });
2903
2909
  locationHistory.current.add(routeInfo);
2904
2910
  /**
@@ -2907,9 +2913,9 @@ const IonRouter = ({ children, registerHistoryListener }) => {
2907
2913
  */
2908
2914
  }
2909
2915
  else {
2910
- const isPushed = ((_c = incomingRouteParams.current) === null || _c === void 0 ? void 0 : _c.routeAction) === 'push' &&
2916
+ const isPushed = ((_b = incomingRouteParams.current) === null || _b === void 0 ? void 0 : _b.routeAction) === 'push' &&
2911
2917
  incomingRouteParams.current.routeDirection === 'forward';
2912
- routeInfo = Object.assign(Object.assign({ id: generateId('routeInfo') }, incomingRouteParams.current), { lastPathname: leavingLocationInfo.pathname, pathname: location.pathname, search: location.search, params: ((_d = incomingRouteParams.current) === null || _d === void 0 ? void 0 : _d.params)
2918
+ routeInfo = Object.assign(Object.assign({ id: generateId('routeInfo') }, incomingRouteParams.current), { lastPathname: leavingLocationInfo.pathname, pathname: location.pathname, search: location.search, params: ((_c = incomingRouteParams.current) === null || _c === void 0 ? void 0 : _c.params)
2913
2919
  ? filterUndefinedParams(incomingRouteParams.current.params)
2914
2920
  : {}, prevRouteLastPathname: leavingLocationInfo.lastPathname });
2915
2921
  if (isPushed) {
@@ -2949,7 +2955,7 @@ const IonRouter = ({ children, registerHistoryListener }) => {
2949
2955
  routeInfo.pushedByRoute = lastRoute === null || lastRoute === void 0 ? void 0 : lastRoute.pushedByRoute;
2950
2956
  }
2951
2957
  else {
2952
- routeInfo.pushedByRoute = (_e = lastRoute === null || lastRoute === void 0 ? void 0 : lastRoute.pushedByRoute) !== null && _e !== void 0 ? _e : leavingLocationInfo.pathname;
2958
+ routeInfo.pushedByRoute = (_d = lastRoute === null || lastRoute === void 0 ? void 0 : lastRoute.pushedByRoute) !== null && _d !== void 0 ? _d : leavingLocationInfo.pathname;
2953
2959
  }
2954
2960
  // Triggered by `navigate()` with replace or a `<Navigate />` component, etc.
2955
2961
  }
@@ -3058,6 +3064,7 @@ const IonRouter = ({ children, registerHistoryListener }) => {
3058
3064
  *
3059
3065
  * @param tab The tab to set as active.
3060
3066
  */
3067
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3061
3068
  const handleSetCurrentTab = (tab, _routeInfo) => {
3062
3069
  currentTab.current = tab;
3063
3070
  const current = locationHistory.current.current();
@@ -3090,9 +3097,9 @@ const IonRouter = ({ children, registerHistoryListener }) => {
3090
3097
  * @param routeAnimation A custom animation builder to override the
3091
3098
  * default "back" animation.
3092
3099
  */
3093
- const handleNavigateBack = (defaultHref = '/', routeAnimation) => {
3100
+ const handleNavigateBack = (defaultHref, routeAnimation) => {
3094
3101
  const config = getConfig();
3095
- defaultHref = defaultHref ? defaultHref : config && config.get('backButtonDefaultHref');
3102
+ defaultHref = defaultHref !== null && defaultHref !== void 0 ? defaultHref : (config && config.get('backButtonDefaultHref'));
3096
3103
  const routeInfo = locationHistory.current.current();
3097
3104
  // It's a linear navigation.
3098
3105
  if (routeInfo && routeInfo.pushedByRoute) {
@@ -3131,18 +3138,19 @@ const IonRouter = ({ children, registerHistoryListener }) => {
3131
3138
  * the history stack.
3132
3139
  */
3133
3140
  }
3134
- else {
3141
+ else if (defaultHref) {
3135
3142
  handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
3136
3143
  }
3137
3144
  /**
3138
3145
  * No `pushedByRoute` (e.g., initial page load or tab root).
3139
- * Tabs with no back history should not navigate.
3146
+ * Navigate to defaultHref so the back button works on direct
3147
+ * deep-link loads (e.g., loading /tab1/child directly).
3148
+ * Only navigate when defaultHref is explicitly set. The core
3149
+ * back-button component hides itself when no defaultHref is
3150
+ * provided, so a click here means the user set one intentionally.
3140
3151
  */
3141
3152
  }
3142
- else {
3143
- if (routeInfo && routeInfo.tab) {
3144
- return;
3145
- }
3153
+ else if (defaultHref) {
3146
3154
  handleNavigate(defaultHref, 'pop', 'back', routeAnimation);
3147
3155
  }
3148
3156
  };
@@ -3196,6 +3204,24 @@ const IonRouter = ({ children, registerHistoryListener }) => {
3196
3204
  routeAnimation, tab: navigationTab });
3197
3205
  navigate(path, { replace: routeAction !== 'push' });
3198
3206
  };
3207
+ /**
3208
+ * Navigates to a new root path, clearing Ionic's navigation history so that
3209
+ * canGoBack() returns false after the transition. All previously mounted views
3210
+ * are unmounted. Useful for post-login / post-logout root navigation.
3211
+ *
3212
+ * @param pathname The path to navigate to.
3213
+ * @param routeAnimation An optional custom animation builder.
3214
+ */
3215
+ const handleNavigateRoot = (pathname, routeAnimation) => {
3216
+ currentTab.current = undefined;
3217
+ forwardStack.current = [];
3218
+ incomingRouteParams.current = {
3219
+ routeAction: 'replace',
3220
+ routeDirection: 'root',
3221
+ routeAnimation,
3222
+ };
3223
+ navigate(pathname, { replace: true });
3224
+ };
3199
3225
  const routeMangerContextValue = {
3200
3226
  canGoBack: () => locationHistory.current.canGoBack(),
3201
3227
  clearOutlet: viewStack.current.clear,
@@ -3210,7 +3236,7 @@ const IonRouter = ({ children, registerHistoryListener }) => {
3210
3236
  unMountViewItem: viewStack.current.remove,
3211
3237
  };
3212
3238
  return (React.createElement(RouteManagerContext.Provider, { value: routeMangerContextValue },
3213
- React.createElement(NavManager, { ionRoute: IonRouteInner, stackManager: StackManager, routeInfo: routeInfo, onNativeBack: handleNativeBack, onNavigateBack: handleNavigateBack, onNavigate: handleNavigate, onSetCurrentTab: handleSetCurrentTab, onChangeTab: handleChangeTab, onResetTab: handleResetTab, locationHistory: locationHistory.current }, children)));
3239
+ React.createElement(NavManager, { ionRoute: IonRouteInner, stackManager: StackManager, routeInfo: routeInfo, onNativeBack: handleNativeBack, onNavigateBack: handleNavigateBack, onNavigate: handleNavigate, onNavigateRoot: handleNavigateRoot, onSetCurrentTab: handleSetCurrentTab, onChangeTab: handleChangeTab, onResetTab: handleResetTab, locationHistory: locationHistory.current }, children)));
3214
3240
  };
3215
3241
  IonRouter.displayName = 'IonRouter';
3216
3242