@ionic/react-router 8.8.1-dev.11774273905.1c3b9598 → 8.8.1-dev.11774636992.1511ce70
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 +98 -38
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1406,6 +1406,12 @@ class StackManager extends React.PureComponent {
|
|
|
1406
1406
|
this._isMounted = false;
|
|
1407
1407
|
/** In-flight requestAnimationFrame IDs from transitionPage, cancelled on unmount. */
|
|
1408
1408
|
this.transitionRafIds = [];
|
|
1409
|
+
/**
|
|
1410
|
+
* Monotonically increasing counter incremented at the start of each transitionPage call.
|
|
1411
|
+
* Used to detect when an async commit() resolves after a newer transition has already run,
|
|
1412
|
+
* preventing the stale commit from hiding an element that the newer transition made visible.
|
|
1413
|
+
*/
|
|
1414
|
+
this.transitionGeneration = 0;
|
|
1409
1415
|
this.outletMountPath = undefined;
|
|
1410
1416
|
/**
|
|
1411
1417
|
* Whether this outlet is at the root level (no parent route matches).
|
|
@@ -1556,6 +1562,26 @@ class StackManager extends React.PureComponent {
|
|
|
1556
1562
|
this.forceUpdate();
|
|
1557
1563
|
return true;
|
|
1558
1564
|
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Handles root navigation by unmounting all non-entering views in this outlet.
|
|
1567
|
+
* Fires ionViewWillLeave / ionViewDidLeave only on views that are currently visible.
|
|
1568
|
+
* Views that are mounted but not visible (e.g., pages earlier in the back stack)
|
|
1569
|
+
* are silently unmounted without lifecycle events, consistent with the behavior
|
|
1570
|
+
* of out-of-scope outlet cleanup.
|
|
1571
|
+
*/
|
|
1572
|
+
handleRootNavigation(enteringViewItem) {
|
|
1573
|
+
const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
|
|
1574
|
+
allViewsInOutlet.forEach((viewItem) => {
|
|
1575
|
+
if (viewItem === enteringViewItem) {
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
if (viewItem.ionPageElement && isViewVisible(viewItem.ionPageElement)) {
|
|
1579
|
+
viewItem.ionPageElement.dispatchEvent(new CustomEvent('ionViewWillLeave', { bubbles: false, cancelable: false }));
|
|
1580
|
+
viewItem.ionPageElement.dispatchEvent(new CustomEvent('ionViewDidLeave', { bubbles: false, cancelable: false }));
|
|
1581
|
+
}
|
|
1582
|
+
this.context.unMountViewItem(viewItem);
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1559
1585
|
/**
|
|
1560
1586
|
* Handles nested outlet with relative routes but no parent path. Returns true to abort.
|
|
1561
1587
|
*/
|
|
@@ -1973,7 +1999,15 @@ class StackManager extends React.PureComponent {
|
|
|
1973
1999
|
this._isMounted = true;
|
|
1974
2000
|
if (this.routerOutletElement) {
|
|
1975
2001
|
this.setupRouterOutlet(this.routerOutletElement);
|
|
1976
|
-
|
|
2002
|
+
// Defer to a microtask to avoid calling forceUpdate() synchronously during
|
|
2003
|
+
// React 19's reappearLayoutEffects phase, which re-runs componentDidMount
|
|
2004
|
+
// without a preceding componentWillUnmount and causes "Maximum update depth exceeded".
|
|
2005
|
+
const routeInfo = this.props.routeInfo;
|
|
2006
|
+
queueMicrotask(() => {
|
|
2007
|
+
if (this._isMounted && this.props.routeInfo.pathname === routeInfo.pathname) {
|
|
2008
|
+
this.handlePageTransition(routeInfo);
|
|
2009
|
+
}
|
|
2010
|
+
});
|
|
1977
2011
|
}
|
|
1978
2012
|
}
|
|
1979
2013
|
componentDidUpdate(prevProps) {
|
|
@@ -2044,14 +2078,20 @@ class StackManager extends React.PureComponent {
|
|
|
2044
2078
|
// Find entering and leaving view items
|
|
2045
2079
|
const viewItems = this.findViewItems(routeInfo);
|
|
2046
2080
|
let enteringViewItem = viewItems.enteringViewItem;
|
|
2047
|
-
|
|
2048
|
-
|
|
2081
|
+
let leavingViewItem = viewItems.leavingViewItem;
|
|
2082
|
+
let shouldUnmountLeavingViewItem = this.shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem);
|
|
2049
2083
|
// Get parent path for nested outlets
|
|
2050
2084
|
const parentPath = this.getParentPath();
|
|
2051
2085
|
// Handle out-of-scope outlet (route outside mount path)
|
|
2052
2086
|
if (this.handleOutOfScopeOutlet(routeInfo)) {
|
|
2053
2087
|
return;
|
|
2054
2088
|
}
|
|
2089
|
+
// Handle root navigation: unmount all non-entering views
|
|
2090
|
+
if (routeInfo.routeDirection === 'root') {
|
|
2091
|
+
this.handleRootNavigation(enteringViewItem);
|
|
2092
|
+
leavingViewItem = undefined;
|
|
2093
|
+
shouldUnmountLeavingViewItem = false;
|
|
2094
|
+
}
|
|
2055
2095
|
// Clear any pending out-of-scope unmount timeout
|
|
2056
2096
|
if (this.outOfScopeUnmountTimeout) {
|
|
2057
2097
|
clearTimeout(this.outOfScopeUnmountTimeout);
|
|
@@ -2300,6 +2340,7 @@ class StackManager extends React.PureComponent {
|
|
|
2300
2340
|
* overlapping content during the transition. Defaults to `false`.
|
|
2301
2341
|
*/
|
|
2302
2342
|
async transitionPage(routeInfo, enteringViewItem, leavingViewItem, direction, progressAnimation = false, skipAnimation = false) {
|
|
2343
|
+
const myGeneration = ++this.transitionGeneration;
|
|
2303
2344
|
const runCommit = async (enteringEl, leavingEl) => {
|
|
2304
2345
|
const skipTransition = this.skipTransition;
|
|
2305
2346
|
/**
|
|
@@ -2349,10 +2390,22 @@ class StackManager extends React.PureComponent {
|
|
|
2349
2390
|
const timeoutMs = 5000;
|
|
2350
2391
|
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve('timeout'), timeoutMs));
|
|
2351
2392
|
const result = await Promise.race([commitPromise.then(() => 'done'), timeoutPromise]);
|
|
2393
|
+
// Bail out if the component unmounted during the commit animation
|
|
2394
|
+
if (!this._isMounted)
|
|
2395
|
+
return;
|
|
2352
2396
|
if (result === 'timeout') {
|
|
2353
2397
|
// Force entering page visible even though commit hung
|
|
2354
2398
|
enteringEl.classList.remove('ion-page-invisible');
|
|
2355
2399
|
}
|
|
2400
|
+
/**
|
|
2401
|
+
* If a newer transitionPage call ran while this commit was in-flight (e.g., a tab
|
|
2402
|
+
* switch fired during a forward animation), the core commit may have applied
|
|
2403
|
+
* ion-page-hidden to leavingEl even though the newer transition already made it
|
|
2404
|
+
* visible. Undo that stale hide so the newer transition's DOM state wins.
|
|
2405
|
+
*/
|
|
2406
|
+
if (myGeneration !== this.transitionGeneration && leavingEl && leavingEl === this.transitionEnteringElement) {
|
|
2407
|
+
showIonPageElement(leavingEl);
|
|
2408
|
+
}
|
|
2356
2409
|
if (!progressAnimation) {
|
|
2357
2410
|
enteringEl.classList.remove('ion-page-invisible');
|
|
2358
2411
|
}
|
|
@@ -2361,6 +2414,7 @@ class StackManager extends React.PureComponent {
|
|
|
2361
2414
|
const routeInfoFallbackDirection = routeInfo.routeDirection === 'none' || routeInfo.routeDirection === 'root' ? undefined : routeInfo.routeDirection;
|
|
2362
2415
|
const directionToUse = direction !== null && direction !== void 0 ? direction : routeInfoFallbackDirection;
|
|
2363
2416
|
if (enteringViewItem && enteringViewItem.ionPageElement && this.routerOutletElement) {
|
|
2417
|
+
this.transitionEnteringElement = enteringViewItem.ionPageElement;
|
|
2364
2418
|
if (leavingViewItem && leavingViewItem.ionPageElement && enteringViewItem === leavingViewItem) {
|
|
2365
2419
|
// Clone page for same-view transitions (e.g., /user/1 → /user/2)
|
|
2366
2420
|
const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname, undefined, this.outletMountPath);
|
|
@@ -2461,15 +2515,22 @@ class StackManager extends React.PureComponent {
|
|
|
2461
2515
|
if (!this._isMounted)
|
|
2462
2516
|
return;
|
|
2463
2517
|
// Swap visibility synchronously - show entering, hide leaving
|
|
2518
|
+
// Skip hiding if a newer transition already made leavingEl the entering view
|
|
2464
2519
|
enteringEl.classList.remove('ion-page-invisible');
|
|
2465
|
-
leavingEl.
|
|
2466
|
-
|
|
2520
|
+
if (myGeneration === this.transitionGeneration || leavingEl !== this.transitionEnteringElement) {
|
|
2521
|
+
leavingEl.classList.add('ion-page-hidden');
|
|
2522
|
+
leavingEl.setAttribute('aria-hidden', 'true');
|
|
2523
|
+
}
|
|
2467
2524
|
}
|
|
2468
2525
|
else {
|
|
2469
2526
|
await runCommit(enteringViewItem.ionPageElement, leavingEl);
|
|
2470
2527
|
if (leavingEl && !progressAnimation) {
|
|
2471
|
-
leavingEl
|
|
2472
|
-
|
|
2528
|
+
// Skip hiding if a newer transition already made leavingEl the entering view
|
|
2529
|
+
// runCommit's generation check has already restored its visibility in that case
|
|
2530
|
+
if (myGeneration === this.transitionGeneration || leavingEl !== this.transitionEnteringElement) {
|
|
2531
|
+
leavingEl.classList.add('ion-page-hidden');
|
|
2532
|
+
leavingEl.setAttribute('aria-hidden', 'true');
|
|
2533
|
+
}
|
|
2473
2534
|
}
|
|
2474
2535
|
}
|
|
2475
2536
|
}
|
|
@@ -2769,34 +2830,14 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
2769
2830
|
* @param action The action that triggered the history change.
|
|
2770
2831
|
*/
|
|
2771
2832
|
const handleHistoryChange = (location, action) => {
|
|
2772
|
-
var _a, _b, _c, _d
|
|
2773
|
-
let leavingLocationInfo;
|
|
2833
|
+
var _a, _b, _c, _d;
|
|
2774
2834
|
/**
|
|
2775
|
-
*
|
|
2776
|
-
*
|
|
2835
|
+
* The leaving location is always the current route, for both programmatic
|
|
2836
|
+
* and external navigations. Using `previous()` for replace actions was
|
|
2837
|
+
* incorrect: it caused the equality check below to skip navigation when
|
|
2838
|
+
* the replace destination matched the entry two slots back in history.
|
|
2777
2839
|
*/
|
|
2778
|
-
|
|
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
|
-
}
|
|
2840
|
+
const leavingLocationInfo = locationHistory.current.current();
|
|
2800
2841
|
const leavingUrl = leavingLocationInfo.pathname + leavingLocationInfo.search;
|
|
2801
2842
|
if (leavingUrl !== location.pathname + location.search) {
|
|
2802
2843
|
if (!incomingRouteParams.current) {
|
|
@@ -2898,7 +2939,7 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
2898
2939
|
* An existing id indicates that it's re-activating an existing route.
|
|
2899
2940
|
* e.g., tab switching or navigating back to a previous route
|
|
2900
2941
|
*/
|
|
2901
|
-
if ((
|
|
2942
|
+
if ((_a = incomingRouteParams.current) === null || _a === void 0 ? void 0 : _a.id) {
|
|
2902
2943
|
routeInfo = Object.assign(Object.assign({}, incomingRouteParams.current), { lastPathname: leavingLocationInfo.pathname });
|
|
2903
2944
|
locationHistory.current.add(routeInfo);
|
|
2904
2945
|
/**
|
|
@@ -2907,9 +2948,9 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
2907
2948
|
*/
|
|
2908
2949
|
}
|
|
2909
2950
|
else {
|
|
2910
|
-
const isPushed = ((
|
|
2951
|
+
const isPushed = ((_b = incomingRouteParams.current) === null || _b === void 0 ? void 0 : _b.routeAction) === 'push' &&
|
|
2911
2952
|
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: ((
|
|
2953
|
+
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
2954
|
? filterUndefinedParams(incomingRouteParams.current.params)
|
|
2914
2955
|
: {}, prevRouteLastPathname: leavingLocationInfo.lastPathname });
|
|
2915
2956
|
if (isPushed) {
|
|
@@ -2949,7 +2990,7 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
2949
2990
|
routeInfo.pushedByRoute = lastRoute === null || lastRoute === void 0 ? void 0 : lastRoute.pushedByRoute;
|
|
2950
2991
|
}
|
|
2951
2992
|
else {
|
|
2952
|
-
routeInfo.pushedByRoute = (
|
|
2993
|
+
routeInfo.pushedByRoute = (_d = lastRoute === null || lastRoute === void 0 ? void 0 : lastRoute.pushedByRoute) !== null && _d !== void 0 ? _d : leavingLocationInfo.pathname;
|
|
2953
2994
|
}
|
|
2954
2995
|
// Triggered by `navigate()` with replace or a `<Navigate />` component, etc.
|
|
2955
2996
|
}
|
|
@@ -3058,6 +3099,7 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
3058
3099
|
*
|
|
3059
3100
|
* @param tab The tab to set as active.
|
|
3060
3101
|
*/
|
|
3102
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
3061
3103
|
const handleSetCurrentTab = (tab, _routeInfo) => {
|
|
3062
3104
|
currentTab.current = tab;
|
|
3063
3105
|
const current = locationHistory.current.current();
|
|
@@ -3197,6 +3239,24 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
3197
3239
|
routeAnimation, tab: navigationTab });
|
|
3198
3240
|
navigate(path, { replace: routeAction !== 'push' });
|
|
3199
3241
|
};
|
|
3242
|
+
/**
|
|
3243
|
+
* Navigates to a new root path, clearing Ionic's navigation history so that
|
|
3244
|
+
* canGoBack() returns false after the transition. All previously mounted views
|
|
3245
|
+
* are unmounted. Useful for post-login / post-logout root navigation.
|
|
3246
|
+
*
|
|
3247
|
+
* @param pathname The path to navigate to.
|
|
3248
|
+
* @param routeAnimation An optional custom animation builder.
|
|
3249
|
+
*/
|
|
3250
|
+
const handleNavigateRoot = (pathname, routeAnimation) => {
|
|
3251
|
+
currentTab.current = undefined;
|
|
3252
|
+
forwardStack.current = [];
|
|
3253
|
+
incomingRouteParams.current = {
|
|
3254
|
+
routeAction: 'replace',
|
|
3255
|
+
routeDirection: 'root',
|
|
3256
|
+
routeAnimation,
|
|
3257
|
+
};
|
|
3258
|
+
navigate(pathname, { replace: true });
|
|
3259
|
+
};
|
|
3200
3260
|
const routeMangerContextValue = {
|
|
3201
3261
|
canGoBack: () => locationHistory.current.canGoBack(),
|
|
3202
3262
|
clearOutlet: viewStack.current.clear,
|
|
@@ -3211,7 +3271,7 @@ const IonRouter = ({ children, registerHistoryListener }) => {
|
|
|
3211
3271
|
unMountViewItem: viewStack.current.remove,
|
|
3212
3272
|
};
|
|
3213
3273
|
return (React.createElement(RouteManagerContext.Provider, { value: routeMangerContextValue },
|
|
3214
|
-
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)));
|
|
3274
|
+
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)));
|
|
3215
3275
|
};
|
|
3216
3276
|
IonRouter.displayName = 'IonRouter';
|
|
3217
3277
|
|