@ionic/react-router 9.0.6-nightly.20260925 → 9.0.6-nightly.20260929
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 +102 -19
- package/dist/index.js.map +1 -1
- package/dist/types/ReactRouter/utils/viewItemUtils.d.ts +27 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -499,6 +499,41 @@ const compareRouteSpecificity = (a, b) => {
|
|
|
499
499
|
}
|
|
500
500
|
return 0;
|
|
501
501
|
};
|
|
502
|
+
/**
|
|
503
|
+
* True when a route matches more pathnames than its own path, so a splat, an index route,
|
|
504
|
+
* or a route with an empty or absent path. A lookup can return one of these for a pathname
|
|
505
|
+
* a more specific sibling owns, so callers must confirm ownership against React Router's
|
|
506
|
+
* ranking before reusing the view item.
|
|
507
|
+
*
|
|
508
|
+
* This is deliberately wider than the catch-all checks in ReactRouterViewStack, which each
|
|
509
|
+
* gate on a narrower shape for a different reason. Don't unify them with this helper.
|
|
510
|
+
*/
|
|
511
|
+
const isOverMatchingRoute = (route) => {
|
|
512
|
+
const { path, index } = route;
|
|
513
|
+
return !path || path.includes('*') || !!index;
|
|
514
|
+
};
|
|
515
|
+
/** True when the navigation pushed a new page forward on top of the current one. */
|
|
516
|
+
const isForwardPush = (routeInfo) => routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'forward';
|
|
517
|
+
const swipeRevealed = new WeakSet();
|
|
518
|
+
/**
|
|
519
|
+
* Marks the page a swipe-back gesture has revealed. For the length of the drag that page
|
|
520
|
+
* is on screen while a more specific sibling still matches the current pathname, and the
|
|
521
|
+
* deactivation scan in `renderViewItem` would otherwise re-hide it on the next render and
|
|
522
|
+
* leave the user dragging a blank page.
|
|
523
|
+
*/
|
|
524
|
+
const markSwipeRevealed = (viewItem) => {
|
|
525
|
+
if (viewItem) {
|
|
526
|
+
swipeRevealed.add(viewItem);
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
/** Drops the mark once the gesture ends, so the view is hidden normally again. */
|
|
530
|
+
const clearSwipeRevealed = (viewItem) => {
|
|
531
|
+
if (viewItem) {
|
|
532
|
+
swipeRevealed.delete(viewItem);
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
/** True while a swipe-back gesture is showing this view. */
|
|
536
|
+
const isSwipeRevealed = (viewItem) => swipeRevealed.has(viewItem);
|
|
502
537
|
/**
|
|
503
538
|
* Sorts view items by route specificity (most specific first).
|
|
504
539
|
*
|
|
@@ -860,7 +895,7 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
860
895
|
}
|
|
861
896
|
// Deactivate wildcard (catch-all) and empty-path (default) routes when a more-specific route matches.
|
|
862
897
|
// This prevents "Not found" or fallback pages from showing alongside valid routes.
|
|
863
|
-
if (routePath === '*' || routePath === '') {
|
|
898
|
+
if (routePath === '*' || routePath === '/*' || routePath === '') {
|
|
864
899
|
// Check if any other view in this outlet has a match for the current route
|
|
865
900
|
const outletViews = this.getViewItemsForOutlet(viewItem.outletId);
|
|
866
901
|
// When parent path context is available, compute the relative pathname once
|
|
@@ -913,8 +948,15 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
913
948
|
}
|
|
914
949
|
}
|
|
915
950
|
}
|
|
916
|
-
if (hasSpecificMatch) {
|
|
917
|
-
|
|
951
|
+
if (hasSpecificMatch && !isSwipeRevealed(viewItem)) {
|
|
952
|
+
// A splat can be the outlet's container page rather than a "not found" fallback.
|
|
953
|
+
// Pushed over it is the page underneath, so unmounting it destroys its state and
|
|
954
|
+
// leaves nothing for back to reveal. Hiding it below covers that. A view with no
|
|
955
|
+
// ion-page was never a page in the stack and has nothing to hide, so it still
|
|
956
|
+
// unmounts.
|
|
957
|
+
if (!isForwardPush(routeInfo) || !viewItem.ionPageElement) {
|
|
958
|
+
viewItem.mount = false;
|
|
959
|
+
}
|
|
918
960
|
if (viewItem.ionPageElement) {
|
|
919
961
|
viewItem.ionPageElement.classList.add('ion-page-hidden');
|
|
920
962
|
viewItem.ionPageElement.setAttribute('aria-hidden', 'true');
|
|
@@ -984,8 +1026,11 @@ class ReactRouterViewStack extends ViewStacks {
|
|
|
984
1026
|
parentPath = result.parentPath;
|
|
985
1027
|
// Persist the mount path for subsequent calls, mirroring StackManager.outletMountPath.
|
|
986
1028
|
// Unlike outletParentPaths (cleared when parentPath is undefined), the mount path is
|
|
987
|
-
// intentionally sticky
|
|
988
|
-
|
|
1029
|
+
// intentionally sticky, it anchors the outlet's scope and is only removed in clear().
|
|
1030
|
+
// A root outlet is skipped because it is mounted under nothing, so an inferred path
|
|
1031
|
+
// would scope it to whatever route was active. parentPathnameBase is undefined
|
|
1032
|
+
// exactly when the outlet has no parent matches.
|
|
1033
|
+
if (parentPathnameBase && result.outletMountPath && !this.outletMountPaths.has(outletId)) {
|
|
989
1034
|
this.outletMountPaths.set(outletId, result.outletMountPath);
|
|
990
1035
|
}
|
|
991
1036
|
}
|
|
@@ -1435,6 +1480,8 @@ class StackManager extends React.PureComponent {
|
|
|
1435
1480
|
routerOutletElement;
|
|
1436
1481
|
prevProps;
|
|
1437
1482
|
skipTransition;
|
|
1483
|
+
/** The view item the in-flight swipe gesture revealed, so the same one gets cleared. */
|
|
1484
|
+
swipeRevealedViewItem;
|
|
1438
1485
|
stackContextValue = {
|
|
1439
1486
|
registerIonPage: this.registerIonPage.bind(this),
|
|
1440
1487
|
isInOutlet: () => true,
|
|
@@ -1526,7 +1573,9 @@ class StackManager extends React.PureComponent {
|
|
|
1526
1573
|
hasIndexRoute,
|
|
1527
1574
|
hasWildcardRoute,
|
|
1528
1575
|
});
|
|
1529
|
-
|
|
1576
|
+
// A root outlet is mounted under nothing, so caching the inferred path would scope
|
|
1577
|
+
// it to whatever route was active and make every other pathname look out of scope.
|
|
1578
|
+
if (!this.isRootOutlet && result.outletMountPath && !this.outletMountPath) {
|
|
1530
1579
|
this.outletMountPath = result.outletMountPath;
|
|
1531
1580
|
}
|
|
1532
1581
|
return result.parentPath;
|
|
@@ -1581,12 +1630,16 @@ class StackManager extends React.PureComponent {
|
|
|
1581
1630
|
return true;
|
|
1582
1631
|
}
|
|
1583
1632
|
// For non-replace actions, only unmount for back navigation
|
|
1584
|
-
|
|
1585
|
-
if (!isForwardPush && routeInfo.routeDirection !== 'none' && enteringViewItem !== leavingViewItem) {
|
|
1633
|
+
if (!isForwardPush(routeInfo) && routeInfo.routeDirection !== 'none' && enteringViewItem !== leavingViewItem) {
|
|
1586
1634
|
return true;
|
|
1587
1635
|
}
|
|
1588
1636
|
return false;
|
|
1589
1637
|
}
|
|
1638
|
+
/** Clears the reveal flag from the view item the gesture marked. */
|
|
1639
|
+
clearSwipeRevealedView() {
|
|
1640
|
+
clearSwipeRevealed(this.swipeRevealedViewItem);
|
|
1641
|
+
this.swipeRevealedViewItem = undefined;
|
|
1642
|
+
}
|
|
1590
1643
|
/**
|
|
1591
1644
|
* Handles out-of-scope outlet. Returns true if transition should be aborted.
|
|
1592
1645
|
*/
|
|
@@ -1756,7 +1809,9 @@ class StackManager extends React.PureComponent {
|
|
|
1756
1809
|
const containerBase = routePath.replace(/\/\*$/, '');
|
|
1757
1810
|
const currentInContainer = routeInfo.pathname.startsWith(containerBase + '/') || routeInfo.pathname === containerBase;
|
|
1758
1811
|
const previousInContainer = routeInfo.lastPathname.startsWith(containerBase + '/') || routeInfo.lastPathname === containerBase;
|
|
1759
|
-
|
|
1812
|
+
// A root-level "/*" leaves an empty base, so both checks above are true for every
|
|
1813
|
+
// pathname and the shortcut would otherwise skip every navigation in this outlet.
|
|
1814
|
+
if (containerBase !== '' && currentInContainer && previousInContainer) {
|
|
1760
1815
|
const updatedMatch = matchComponent(enteringViewItem.reactElement, routeInfo.pathname, true, this.outletMountPath);
|
|
1761
1816
|
if (updatedMatch) {
|
|
1762
1817
|
enteringViewItem.routeData.match = updatedMatch;
|
|
@@ -2218,6 +2273,7 @@ class StackManager extends React.PureComponent {
|
|
|
2218
2273
|
}
|
|
2219
2274
|
this.waitingForIonPage = false;
|
|
2220
2275
|
this.preservedViewItems.clear();
|
|
2276
|
+
this.clearSwipeRevealedView();
|
|
2221
2277
|
// Hide all views in this outlet before clearing.
|
|
2222
2278
|
// This is critical for nested outlets - when the parent component unmounts,
|
|
2223
2279
|
// the nested outlet's componentDidUpdate won't be called, so we must hide
|
|
@@ -2249,13 +2305,32 @@ class StackManager extends React.PureComponent {
|
|
|
2249
2305
|
this.pendingPageTransition = true;
|
|
2250
2306
|
return;
|
|
2251
2307
|
}
|
|
2308
|
+
// A completed swipe keeps its reveal flag past goBack() so the deactivation scan does
|
|
2309
|
+
// not re-hide the page between the gesture ending and the URL settling. Clear it at
|
|
2310
|
+
// the start of the transition for the new pathname.
|
|
2311
|
+
this.clearSwipeRevealedView();
|
|
2252
2312
|
// Find entering and leaving view items
|
|
2253
2313
|
const viewItems = this.findViewItems(routeInfo);
|
|
2254
2314
|
let enteringViewItem = viewItems.enteringViewItem;
|
|
2255
2315
|
let leavingViewItem = viewItems.leavingViewItem;
|
|
2256
|
-
let shouldUnmountLeavingViewItem = this.shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem);
|
|
2257
2316
|
// Get parent path for nested outlets
|
|
2258
2317
|
const parentPath = this.getParentPath();
|
|
2318
|
+
// Find the matching route element. This is React Router's own ranking of the outlet's
|
|
2319
|
+
// routes, so it decides which route owns the pathname.
|
|
2320
|
+
const enteringRoute = findRouteByRouteInfo(this.ionRouterOutlet?.props.children, routeInfo, parentPath);
|
|
2321
|
+
// The lookup above only matches view items that already exist, so a sibling route with
|
|
2322
|
+
// no view item yet never wins there and an over-matching route (splat, index, empty
|
|
2323
|
+
// path) comes back for pathnames it does not own. Drop it rather than reusing it,
|
|
2324
|
+
// because overwriting its reactElement below would swap its page for the winning
|
|
2325
|
+
// route's and unmount it. This has to resolve before shouldUnmountLeavingView and
|
|
2326
|
+
// handleRootNavigation, which both branch on the entering view item.
|
|
2327
|
+
const matchedRouteProps = enteringViewItem?.routeData?.childProps;
|
|
2328
|
+
const matchedAnOverMatchingRoute = isOverMatchingRoute(matchedRouteProps ?? {});
|
|
2329
|
+
const routeOwnerDisagrees = enteringRoute !== undefined && enteringRoute.props.path !== matchedRouteProps?.path;
|
|
2330
|
+
if (enteringViewItem && matchedAnOverMatchingRoute && routeOwnerDisagrees) {
|
|
2331
|
+
enteringViewItem = undefined;
|
|
2332
|
+
}
|
|
2333
|
+
let shouldUnmountLeavingViewItem = this.shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem);
|
|
2259
2334
|
// Handle out-of-scope outlet (route outside mount path)
|
|
2260
2335
|
if (this.handleOutOfScopeOutlet(routeInfo)) {
|
|
2261
2336
|
return;
|
|
@@ -2275,8 +2350,6 @@ class StackManager extends React.PureComponent {
|
|
|
2275
2350
|
if (this.handleOutOfContextNestedOutlet(parentPath, leavingViewItem)) {
|
|
2276
2351
|
return;
|
|
2277
2352
|
}
|
|
2278
|
-
// Find the matching route element
|
|
2279
|
-
const enteringRoute = findRouteByRouteInfo(this.ionRouterOutlet?.props.children, routeInfo, parentPath);
|
|
2280
2353
|
// Handle nested outlet with no matching route
|
|
2281
2354
|
if (this.handleNoMatchingRoute(enteringRoute, enteringViewItem, leavingViewItem)) {
|
|
2282
2355
|
return;
|
|
@@ -2473,17 +2546,17 @@ class StackManager extends React.PureComponent {
|
|
|
2473
2546
|
const { routeInfo } = this.props;
|
|
2474
2547
|
const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
|
|
2475
2548
|
const enteringViewItem = this.findEnteringViewForSwipe(swipeBackRouteInfo);
|
|
2549
|
+
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
|
|
2476
2550
|
// View might have mount=false but ionPageElement still in DOM
|
|
2477
2551
|
const ionPageInDocument = Boolean(enteringViewItem?.ionPageElement && document.body.contains(enteringViewItem.ionPageElement));
|
|
2478
|
-
//
|
|
2479
|
-
//
|
|
2480
|
-
//
|
|
2481
|
-
//
|
|
2482
|
-
// views are the same and the swipe gesture shouldn't start.
|
|
2552
|
+
// A splat consumes whatever is left of the pathname, so its match resolves to the whole
|
|
2553
|
+
// current pathname and comparing pathnames can't tell a container page underneath a
|
|
2554
|
+
// pushed sibling from the page being left. Compare the view items instead, like onEnd
|
|
2555
|
+
// does. Without a leaving view onStart skips the transition, so reject that here too.
|
|
2483
2556
|
const canStartSwipe = !!enteringViewItem &&
|
|
2557
|
+
!!leavingViewItem &&
|
|
2484
2558
|
(enteringViewItem.mount || ionPageInDocument) &&
|
|
2485
|
-
enteringViewItem
|
|
2486
|
-
enteringViewItem.routeData.match.pathname !== routeInfo.pathname;
|
|
2559
|
+
enteringViewItem !== leavingViewItem;
|
|
2487
2560
|
debug('SwipeBackCanStart', () => ({
|
|
2488
2561
|
outletId: this.id,
|
|
2489
2562
|
routePathname: routeInfo.pathname,
|
|
@@ -2492,11 +2565,15 @@ class StackManager extends React.PureComponent {
|
|
|
2492
2565
|
enteringViewPath: enteringViewItem?.reactElement?.props?.path,
|
|
2493
2566
|
enteringMount: enteringViewItem?.mount,
|
|
2494
2567
|
ionPageInDocument,
|
|
2568
|
+
leavingViewId: leavingViewItem?.id,
|
|
2495
2569
|
canStartSwipe,
|
|
2496
2570
|
}));
|
|
2497
2571
|
return canStartSwipe;
|
|
2498
2572
|
};
|
|
2499
2573
|
const onStart = async () => {
|
|
2574
|
+
// Core only calls onEnd when a progress animation exists, so with animations off a
|
|
2575
|
+
// previous gesture can leave its mark behind. Drop any stale one before marking.
|
|
2576
|
+
this.clearSwipeRevealedView();
|
|
2500
2577
|
const { routeInfo } = this.props;
|
|
2501
2578
|
const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
|
|
2502
2579
|
const enteringViewItem = this.findEnteringViewForSwipe(swipeBackRouteInfo);
|
|
@@ -2523,6 +2600,11 @@ class StackManager extends React.PureComponent {
|
|
|
2523
2600
|
revealIonPageForSwipeBack(enteringViewItem?.ionPageElement);
|
|
2524
2601
|
// When the gesture starts, kick off a transition controlled via swipe gesture
|
|
2525
2602
|
if (enteringViewItem && leavingViewItem) {
|
|
2603
|
+
// Without a leaving view there is no progress animation, and core only calls
|
|
2604
|
+
// swipeHandler.onEnd when one exists, so nothing would clear the mark until the
|
|
2605
|
+
// next transition.
|
|
2606
|
+
markSwipeRevealed(enteringViewItem);
|
|
2607
|
+
this.swipeRevealedViewItem = enteringViewItem;
|
|
2526
2608
|
await this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back', true);
|
|
2527
2609
|
}
|
|
2528
2610
|
debug('SwipeBackOnStart:exit', () => ({
|
|
@@ -2547,6 +2629,7 @@ class StackManager extends React.PureComponent {
|
|
|
2547
2629
|
const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
|
|
2548
2630
|
const enteringViewItem = this.findEnteringViewForSwipe(swipeBackRouteInfo);
|
|
2549
2631
|
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
|
|
2632
|
+
this.clearSwipeRevealedView();
|
|
2550
2633
|
// Don't hide if entering and leaving are the same (parameterized route edge case)
|
|
2551
2634
|
if (enteringViewItem !== leavingViewItem && enteringViewItem?.ionPageElement !== undefined) {
|
|
2552
2635
|
hideIonPageElement(enteringViewItem.ionPageElement);
|