@ionic/react-router 9.0.6-dev.11790615145.1d8c56a2 → 9.0.6-dev.11790776272.1d27d1bd
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 +108 -55
- 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
|
}
|
|
@@ -1407,12 +1452,6 @@ const revealIonPageForSwipeBack = (element) => {
|
|
|
1407
1452
|
element.removeAttribute('aria-hidden');
|
|
1408
1453
|
}
|
|
1409
1454
|
};
|
|
1410
|
-
/** Dispatches a view lifecycle event the way core's `lifecycle()` does. */
|
|
1411
|
-
const dispatchLifecycleEvent = (element, eventName) => {
|
|
1412
|
-
if (element) {
|
|
1413
|
-
element.dispatchEvent(new CustomEvent(eventName, { bubbles: false, cancelable: false }));
|
|
1414
|
-
}
|
|
1415
|
-
};
|
|
1416
1455
|
/**
|
|
1417
1456
|
* A leaf view is "preservable" on browser-back (pop) when its React state
|
|
1418
1457
|
* should survive a forward-pop round-trip. Non-parameterized leaf paths
|
|
@@ -1441,6 +1480,8 @@ class StackManager extends React.PureComponent {
|
|
|
1441
1480
|
routerOutletElement;
|
|
1442
1481
|
prevProps;
|
|
1443
1482
|
skipTransition;
|
|
1483
|
+
/** The view item the in-flight swipe gesture revealed, so the same one gets cleared. */
|
|
1484
|
+
swipeRevealedViewItem;
|
|
1444
1485
|
stackContextValue = {
|
|
1445
1486
|
registerIonPage: this.registerIonPage.bind(this),
|
|
1446
1487
|
isInOutlet: () => true,
|
|
@@ -1532,7 +1573,9 @@ class StackManager extends React.PureComponent {
|
|
|
1532
1573
|
hasIndexRoute,
|
|
1533
1574
|
hasWildcardRoute,
|
|
1534
1575
|
});
|
|
1535
|
-
|
|
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) {
|
|
1536
1579
|
this.outletMountPath = result.outletMountPath;
|
|
1537
1580
|
}
|
|
1538
1581
|
return result.parentPath;
|
|
@@ -1587,12 +1630,16 @@ class StackManager extends React.PureComponent {
|
|
|
1587
1630
|
return true;
|
|
1588
1631
|
}
|
|
1589
1632
|
// For non-replace actions, only unmount for back navigation
|
|
1590
|
-
|
|
1591
|
-
if (!isForwardPush && routeInfo.routeDirection !== 'none' && enteringViewItem !== leavingViewItem) {
|
|
1633
|
+
if (!isForwardPush(routeInfo) && routeInfo.routeDirection !== 'none' && enteringViewItem !== leavingViewItem) {
|
|
1592
1634
|
return true;
|
|
1593
1635
|
}
|
|
1594
1636
|
return false;
|
|
1595
1637
|
}
|
|
1638
|
+
/** Clears the reveal flag from the view item the gesture marked. */
|
|
1639
|
+
clearSwipeRevealedView() {
|
|
1640
|
+
clearSwipeRevealed(this.swipeRevealedViewItem);
|
|
1641
|
+
this.swipeRevealedViewItem = undefined;
|
|
1642
|
+
}
|
|
1596
1643
|
/**
|
|
1597
1644
|
* Handles out-of-scope outlet. Returns true if transition should be aborted.
|
|
1598
1645
|
*/
|
|
@@ -1641,8 +1688,8 @@ class StackManager extends React.PureComponent {
|
|
|
1641
1688
|
const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
|
|
1642
1689
|
allViewsInOutlet.forEach((viewItem) => {
|
|
1643
1690
|
if (viewItem.ionPageElement && isViewVisible(viewItem.ionPageElement)) {
|
|
1644
|
-
|
|
1645
|
-
|
|
1691
|
+
viewItem.ionPageElement.dispatchEvent(new CustomEvent('ionViewWillLeave', { bubbles: false, cancelable: false }));
|
|
1692
|
+
viewItem.ionPageElement.dispatchEvent(new CustomEvent('ionViewDidLeave', { bubbles: false, cancelable: false }));
|
|
1646
1693
|
}
|
|
1647
1694
|
});
|
|
1648
1695
|
// Defer removal of view items to allow the parent outlet's leaving-page
|
|
@@ -1677,8 +1724,8 @@ class StackManager extends React.PureComponent {
|
|
|
1677
1724
|
return;
|
|
1678
1725
|
}
|
|
1679
1726
|
if (viewItem.ionPageElement && isViewVisible(viewItem.ionPageElement)) {
|
|
1680
|
-
|
|
1681
|
-
|
|
1727
|
+
viewItem.ionPageElement.dispatchEvent(new CustomEvent('ionViewWillLeave', { bubbles: false, cancelable: false }));
|
|
1728
|
+
viewItem.ionPageElement.dispatchEvent(new CustomEvent('ionViewDidLeave', { bubbles: false, cancelable: false }));
|
|
1682
1729
|
}
|
|
1683
1730
|
this.context.unMountViewItem(viewItem);
|
|
1684
1731
|
});
|
|
@@ -1762,7 +1809,9 @@ class StackManager extends React.PureComponent {
|
|
|
1762
1809
|
const containerBase = routePath.replace(/\/\*$/, '');
|
|
1763
1810
|
const currentInContainer = routeInfo.pathname.startsWith(containerBase + '/') || routeInfo.pathname === containerBase;
|
|
1764
1811
|
const previousInContainer = routeInfo.lastPathname.startsWith(containerBase + '/') || routeInfo.lastPathname === containerBase;
|
|
1765
|
-
|
|
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) {
|
|
1766
1815
|
const updatedMatch = matchComponent(enteringViewItem.reactElement, routeInfo.pathname, true, this.outletMountPath);
|
|
1767
1816
|
if (updatedMatch) {
|
|
1768
1817
|
enteringViewItem.routeData.match = updatedMatch;
|
|
@@ -2224,6 +2273,7 @@ class StackManager extends React.PureComponent {
|
|
|
2224
2273
|
}
|
|
2225
2274
|
this.waitingForIonPage = false;
|
|
2226
2275
|
this.preservedViewItems.clear();
|
|
2276
|
+
this.clearSwipeRevealedView();
|
|
2227
2277
|
// Hide all views in this outlet before clearing.
|
|
2228
2278
|
// This is critical for nested outlets - when the parent component unmounts,
|
|
2229
2279
|
// the nested outlet's componentDidUpdate won't be called, so we must hide
|
|
@@ -2255,13 +2305,32 @@ class StackManager extends React.PureComponent {
|
|
|
2255
2305
|
this.pendingPageTransition = true;
|
|
2256
2306
|
return;
|
|
2257
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();
|
|
2258
2312
|
// Find entering and leaving view items
|
|
2259
2313
|
const viewItems = this.findViewItems(routeInfo);
|
|
2260
2314
|
let enteringViewItem = viewItems.enteringViewItem;
|
|
2261
2315
|
let leavingViewItem = viewItems.leavingViewItem;
|
|
2262
|
-
let shouldUnmountLeavingViewItem = this.shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem);
|
|
2263
2316
|
// Get parent path for nested outlets
|
|
2264
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);
|
|
2265
2334
|
// Handle out-of-scope outlet (route outside mount path)
|
|
2266
2335
|
if (this.handleOutOfScopeOutlet(routeInfo)) {
|
|
2267
2336
|
return;
|
|
@@ -2281,8 +2350,6 @@ class StackManager extends React.PureComponent {
|
|
|
2281
2350
|
if (this.handleOutOfContextNestedOutlet(parentPath, leavingViewItem)) {
|
|
2282
2351
|
return;
|
|
2283
2352
|
}
|
|
2284
|
-
// Find the matching route element
|
|
2285
|
-
const enteringRoute = findRouteByRouteInfo(this.ionRouterOutlet?.props.children, routeInfo, parentPath);
|
|
2286
2353
|
// Handle nested outlet with no matching route
|
|
2287
2354
|
if (this.handleNoMatchingRoute(enteringRoute, enteringViewItem, leavingViewItem)) {
|
|
2288
2355
|
return;
|
|
@@ -2479,17 +2546,17 @@ class StackManager extends React.PureComponent {
|
|
|
2479
2546
|
const { routeInfo } = this.props;
|
|
2480
2547
|
const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
|
|
2481
2548
|
const enteringViewItem = this.findEnteringViewForSwipe(swipeBackRouteInfo);
|
|
2549
|
+
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
|
|
2482
2550
|
// View might have mount=false but ionPageElement still in DOM
|
|
2483
2551
|
const ionPageInDocument = Boolean(enteringViewItem?.ionPageElement && document.body.contains(enteringViewItem.ionPageElement));
|
|
2484
|
-
//
|
|
2485
|
-
//
|
|
2486
|
-
//
|
|
2487
|
-
//
|
|
2488
|
-
// 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.
|
|
2489
2556
|
const canStartSwipe = !!enteringViewItem &&
|
|
2557
|
+
!!leavingViewItem &&
|
|
2490
2558
|
(enteringViewItem.mount || ionPageInDocument) &&
|
|
2491
|
-
enteringViewItem
|
|
2492
|
-
enteringViewItem.routeData.match.pathname !== routeInfo.pathname;
|
|
2559
|
+
enteringViewItem !== leavingViewItem;
|
|
2493
2560
|
debug('SwipeBackCanStart', () => ({
|
|
2494
2561
|
outletId: this.id,
|
|
2495
2562
|
routePathname: routeInfo.pathname,
|
|
@@ -2498,11 +2565,15 @@ class StackManager extends React.PureComponent {
|
|
|
2498
2565
|
enteringViewPath: enteringViewItem?.reactElement?.props?.path,
|
|
2499
2566
|
enteringMount: enteringViewItem?.mount,
|
|
2500
2567
|
ionPageInDocument,
|
|
2568
|
+
leavingViewId: leavingViewItem?.id,
|
|
2501
2569
|
canStartSwipe,
|
|
2502
2570
|
}));
|
|
2503
2571
|
return canStartSwipe;
|
|
2504
2572
|
};
|
|
2505
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();
|
|
2506
2577
|
const { routeInfo } = this.props;
|
|
2507
2578
|
const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
|
|
2508
2579
|
const enteringViewItem = this.findEnteringViewForSwipe(swipeBackRouteInfo);
|
|
@@ -2529,6 +2600,11 @@ class StackManager extends React.PureComponent {
|
|
|
2529
2600
|
revealIonPageForSwipeBack(enteringViewItem?.ionPageElement);
|
|
2530
2601
|
// When the gesture starts, kick off a transition controlled via swipe gesture
|
|
2531
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;
|
|
2532
2608
|
await this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back', true);
|
|
2533
2609
|
}
|
|
2534
2610
|
debug('SwipeBackOnStart:exit', () => ({
|
|
@@ -2553,6 +2629,7 @@ class StackManager extends React.PureComponent {
|
|
|
2553
2629
|
const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
|
|
2554
2630
|
const enteringViewItem = this.findEnteringViewForSwipe(swipeBackRouteInfo);
|
|
2555
2631
|
const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
|
|
2632
|
+
this.clearSwipeRevealedView();
|
|
2556
2633
|
// Don't hide if entering and leaving are the same (parameterized route edge case)
|
|
2557
2634
|
if (enteringViewItem !== leavingViewItem && enteringViewItem?.ionPageElement !== undefined) {
|
|
2558
2635
|
hideIonPageElement(enteringViewItem.ionPageElement);
|
|
@@ -2779,34 +2856,10 @@ class StackManager extends React.PureComponent {
|
|
|
2779
2856
|
// Bail out if the component unmounted during waitForComponentsReady
|
|
2780
2857
|
if (!this._isMounted)
|
|
2781
2858
|
return;
|
|
2782
|
-
const isCurrent = myGeneration === this.transitionGeneration;
|
|
2783
|
-
// A page the newest transition is entering is not leaving after all.
|
|
2784
|
-
const isLeaving = isCurrent || leavingEl !== this.transitionEnteringElement;
|
|
2785
|
-
// Already hidden means the leave events have fired. This checks the class rather
|
|
2786
|
-
// than `isViewVisible` because a nested outlet marks its leaving page
|
|
2787
|
-
// `visibility: hidden` before we get here and still needs `ionViewDidLeave`.
|
|
2788
|
-
const announceLeaving = isLeaving && !leavingEl.classList.contains('ion-page-hidden');
|
|
2789
|
-
/**
|
|
2790
|
-
* Dispatch the lifecycle events, since we skipped `commit()`. Only the
|
|
2791
|
-
* newest transition fires the entering events, and the class swap follows
|
|
2792
|
-
* all four so the ordering matches core's `transition()`.
|
|
2793
|
-
*
|
|
2794
|
-
* These run after `waitForComponentsReady` because on a first mount the
|
|
2795
|
-
* page has not attached its listeners yet.
|
|
2796
|
-
*/
|
|
2797
|
-
if (announceLeaving) {
|
|
2798
|
-
dispatchLifecycleEvent(leavingEl, 'ionViewWillLeave');
|
|
2799
|
-
}
|
|
2800
|
-
if (isCurrent) {
|
|
2801
|
-
dispatchLifecycleEvent(enteringEl, 'ionViewWillEnter');
|
|
2802
|
-
dispatchLifecycleEvent(enteringEl, 'ionViewDidEnter');
|
|
2803
|
-
}
|
|
2804
|
-
if (announceLeaving) {
|
|
2805
|
-
dispatchLifecycleEvent(leavingEl, 'ionViewDidLeave');
|
|
2806
|
-
}
|
|
2807
2859
|
// Swap visibility synchronously - show entering, hide leaving
|
|
2860
|
+
// Skip hiding if a newer transition already made leavingEl the entering view
|
|
2808
2861
|
enteringEl.classList.remove('ion-page-invisible');
|
|
2809
|
-
if (
|
|
2862
|
+
if (myGeneration === this.transitionGeneration || leavingEl !== this.transitionEnteringElement) {
|
|
2810
2863
|
leavingEl.classList.add('ion-page-hidden');
|
|
2811
2864
|
leavingEl.setAttribute('aria-hidden', 'true');
|
|
2812
2865
|
}
|