@ionic/react-router 9.0.6-dev.11790349282.1a877abd → 9.0.6-dev.11790615145.1d8c56a2

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
@@ -499,41 +499,6 @@ 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);
537
502
  /**
538
503
  * Sorts view items by route specificity (most specific first).
539
504
  *
@@ -895,7 +860,7 @@ class ReactRouterViewStack extends ViewStacks {
895
860
  }
896
861
  // Deactivate wildcard (catch-all) and empty-path (default) routes when a more-specific route matches.
897
862
  // This prevents "Not found" or fallback pages from showing alongside valid routes.
898
- if (routePath === '*' || routePath === '/*' || routePath === '') {
863
+ if (routePath === '*' || routePath === '') {
899
864
  // Check if any other view in this outlet has a match for the current route
900
865
  const outletViews = this.getViewItemsForOutlet(viewItem.outletId);
901
866
  // When parent path context is available, compute the relative pathname once
@@ -948,15 +913,8 @@ class ReactRouterViewStack extends ViewStacks {
948
913
  }
949
914
  }
950
915
  }
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
- }
916
+ if (hasSpecificMatch) {
917
+ viewItem.mount = false;
960
918
  if (viewItem.ionPageElement) {
961
919
  viewItem.ionPageElement.classList.add('ion-page-hidden');
962
920
  viewItem.ionPageElement.setAttribute('aria-hidden', 'true');
@@ -1026,11 +984,8 @@ class ReactRouterViewStack extends ViewStacks {
1026
984
  parentPath = result.parentPath;
1027
985
  // Persist the mount path for subsequent calls, mirroring StackManager.outletMountPath.
1028
986
  // Unlike outletParentPaths (cleared when parentPath is undefined), the mount path is
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)) {
987
+ // intentionally sticky — it anchors the outlet's scope and is only removed in clear().
988
+ if (result.outletMountPath && !this.outletMountPaths.has(outletId)) {
1034
989
  this.outletMountPaths.set(outletId, result.outletMountPath);
1035
990
  }
1036
991
  }
@@ -1452,6 +1407,12 @@ const revealIonPageForSwipeBack = (element) => {
1452
1407
  element.removeAttribute('aria-hidden');
1453
1408
  }
1454
1409
  };
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
+ };
1455
1416
  /**
1456
1417
  * A leaf view is "preservable" on browser-back (pop) when its React state
1457
1418
  * should survive a forward-pop round-trip. Non-parameterized leaf paths
@@ -1480,8 +1441,6 @@ class StackManager extends React.PureComponent {
1480
1441
  routerOutletElement;
1481
1442
  prevProps;
1482
1443
  skipTransition;
1483
- /** The view item the in-flight swipe gesture revealed, so the same one gets cleared. */
1484
- swipeRevealedViewItem;
1485
1444
  stackContextValue = {
1486
1445
  registerIonPage: this.registerIonPage.bind(this),
1487
1446
  isInOutlet: () => true,
@@ -1573,9 +1532,7 @@ class StackManager extends React.PureComponent {
1573
1532
  hasIndexRoute,
1574
1533
  hasWildcardRoute,
1575
1534
  });
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) {
1535
+ if (result.outletMountPath && !this.outletMountPath) {
1579
1536
  this.outletMountPath = result.outletMountPath;
1580
1537
  }
1581
1538
  return result.parentPath;
@@ -1630,16 +1587,12 @@ class StackManager extends React.PureComponent {
1630
1587
  return true;
1631
1588
  }
1632
1589
  // For non-replace actions, only unmount for back navigation
1633
- if (!isForwardPush(routeInfo) && routeInfo.routeDirection !== 'none' && enteringViewItem !== leavingViewItem) {
1590
+ const isForwardPush = routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'forward';
1591
+ if (!isForwardPush && routeInfo.routeDirection !== 'none' && enteringViewItem !== leavingViewItem) {
1634
1592
  return true;
1635
1593
  }
1636
1594
  return false;
1637
1595
  }
1638
- /** Clears the reveal flag from the view item the gesture marked. */
1639
- clearSwipeRevealedView() {
1640
- clearSwipeRevealed(this.swipeRevealedViewItem);
1641
- this.swipeRevealedViewItem = undefined;
1642
- }
1643
1596
  /**
1644
1597
  * Handles out-of-scope outlet. Returns true if transition should be aborted.
1645
1598
  */
@@ -1688,8 +1641,8 @@ class StackManager extends React.PureComponent {
1688
1641
  const allViewsInOutlet = this.context.getViewItemsForOutlet(this.id);
1689
1642
  allViewsInOutlet.forEach((viewItem) => {
1690
1643
  if (viewItem.ionPageElement && isViewVisible(viewItem.ionPageElement)) {
1691
- viewItem.ionPageElement.dispatchEvent(new CustomEvent('ionViewWillLeave', { bubbles: false, cancelable: false }));
1692
- viewItem.ionPageElement.dispatchEvent(new CustomEvent('ionViewDidLeave', { bubbles: false, cancelable: false }));
1644
+ dispatchLifecycleEvent(viewItem.ionPageElement, 'ionViewWillLeave');
1645
+ dispatchLifecycleEvent(viewItem.ionPageElement, 'ionViewDidLeave');
1693
1646
  }
1694
1647
  });
1695
1648
  // Defer removal of view items to allow the parent outlet's leaving-page
@@ -1724,8 +1677,8 @@ class StackManager extends React.PureComponent {
1724
1677
  return;
1725
1678
  }
1726
1679
  if (viewItem.ionPageElement && isViewVisible(viewItem.ionPageElement)) {
1727
- viewItem.ionPageElement.dispatchEvent(new CustomEvent('ionViewWillLeave', { bubbles: false, cancelable: false }));
1728
- viewItem.ionPageElement.dispatchEvent(new CustomEvent('ionViewDidLeave', { bubbles: false, cancelable: false }));
1680
+ dispatchLifecycleEvent(viewItem.ionPageElement, 'ionViewWillLeave');
1681
+ dispatchLifecycleEvent(viewItem.ionPageElement, 'ionViewDidLeave');
1729
1682
  }
1730
1683
  this.context.unMountViewItem(viewItem);
1731
1684
  });
@@ -1809,9 +1762,7 @@ class StackManager extends React.PureComponent {
1809
1762
  const containerBase = routePath.replace(/\/\*$/, '');
1810
1763
  const currentInContainer = routeInfo.pathname.startsWith(containerBase + '/') || routeInfo.pathname === containerBase;
1811
1764
  const previousInContainer = routeInfo.lastPathname.startsWith(containerBase + '/') || routeInfo.lastPathname === containerBase;
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) {
1765
+ if (currentInContainer && previousInContainer) {
1815
1766
  const updatedMatch = matchComponent(enteringViewItem.reactElement, routeInfo.pathname, true, this.outletMountPath);
1816
1767
  if (updatedMatch) {
1817
1768
  enteringViewItem.routeData.match = updatedMatch;
@@ -2273,7 +2224,6 @@ class StackManager extends React.PureComponent {
2273
2224
  }
2274
2225
  this.waitingForIonPage = false;
2275
2226
  this.preservedViewItems.clear();
2276
- this.clearSwipeRevealedView();
2277
2227
  // Hide all views in this outlet before clearing.
2278
2228
  // This is critical for nested outlets - when the parent component unmounts,
2279
2229
  // the nested outlet's componentDidUpdate won't be called, so we must hide
@@ -2305,32 +2255,13 @@ class StackManager extends React.PureComponent {
2305
2255
  this.pendingPageTransition = true;
2306
2256
  return;
2307
2257
  }
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();
2312
2258
  // Find entering and leaving view items
2313
2259
  const viewItems = this.findViewItems(routeInfo);
2314
2260
  let enteringViewItem = viewItems.enteringViewItem;
2315
2261
  let leavingViewItem = viewItems.leavingViewItem;
2262
+ let shouldUnmountLeavingViewItem = this.shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem);
2316
2263
  // Get parent path for nested outlets
2317
2264
  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
- if (enteringViewItem &&
2328
- enteringRoute &&
2329
- isOverMatchingRoute(enteringViewItem.routeData?.childProps ?? {}) &&
2330
- enteringRoute.props.path !== enteringViewItem.routeData?.childProps?.path) {
2331
- enteringViewItem = undefined;
2332
- }
2333
- let shouldUnmountLeavingViewItem = this.shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem);
2334
2265
  // Handle out-of-scope outlet (route outside mount path)
2335
2266
  if (this.handleOutOfScopeOutlet(routeInfo)) {
2336
2267
  return;
@@ -2350,6 +2281,8 @@ class StackManager extends React.PureComponent {
2350
2281
  if (this.handleOutOfContextNestedOutlet(parentPath, leavingViewItem)) {
2351
2282
  return;
2352
2283
  }
2284
+ // Find the matching route element
2285
+ const enteringRoute = findRouteByRouteInfo(this.ionRouterOutlet?.props.children, routeInfo, parentPath);
2353
2286
  // Handle nested outlet with no matching route
2354
2287
  if (this.handleNoMatchingRoute(enteringRoute, enteringViewItem, leavingViewItem)) {
2355
2288
  return;
@@ -2546,17 +2479,17 @@ class StackManager extends React.PureComponent {
2546
2479
  const { routeInfo } = this.props;
2547
2480
  const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
2548
2481
  const enteringViewItem = this.findEnteringViewForSwipe(swipeBackRouteInfo);
2549
- const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
2550
2482
  // View might have mount=false but ionPageElement still in DOM
2551
2483
  const ionPageInDocument = Boolean(enteringViewItem?.ionPageElement && document.body.contains(enteringViewItem.ionPageElement));
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.
2484
+ // For wildcard/parameterized routes, the pattern path (e.g. "/foo/*") will
2485
+ // never equal the resolved pathname (e.g. "/foo/bar"), so the pattern check
2486
+ // alone isn't sufficient. Also, verify the entering view's resolved pathname
2487
+ // differs from the current pathname — if they match, the entering and leaving
2488
+ // views are the same and the swipe gesture shouldn't start.
2556
2489
  const canStartSwipe = !!enteringViewItem &&
2557
- !!leavingViewItem &&
2558
2490
  (enteringViewItem.mount || ionPageInDocument) &&
2559
- enteringViewItem !== leavingViewItem;
2491
+ enteringViewItem.routeData.match.pattern.path !== routeInfo.pathname &&
2492
+ enteringViewItem.routeData.match.pathname !== routeInfo.pathname;
2560
2493
  debug('SwipeBackCanStart', () => ({
2561
2494
  outletId: this.id,
2562
2495
  routePathname: routeInfo.pathname,
@@ -2565,15 +2498,11 @@ class StackManager extends React.PureComponent {
2565
2498
  enteringViewPath: enteringViewItem?.reactElement?.props?.path,
2566
2499
  enteringMount: enteringViewItem?.mount,
2567
2500
  ionPageInDocument,
2568
- leavingViewId: leavingViewItem?.id,
2569
2501
  canStartSwipe,
2570
2502
  }));
2571
2503
  return canStartSwipe;
2572
2504
  };
2573
2505
  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();
2577
2506
  const { routeInfo } = this.props;
2578
2507
  const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
2579
2508
  const enteringViewItem = this.findEnteringViewForSwipe(swipeBackRouteInfo);
@@ -2600,11 +2529,6 @@ class StackManager extends React.PureComponent {
2600
2529
  revealIonPageForSwipeBack(enteringViewItem?.ionPageElement);
2601
2530
  // When the gesture starts, kick off a transition controlled via swipe gesture
2602
2531
  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;
2608
2532
  await this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back', true);
2609
2533
  }
2610
2534
  debug('SwipeBackOnStart:exit', () => ({
@@ -2629,7 +2553,6 @@ class StackManager extends React.PureComponent {
2629
2553
  const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
2630
2554
  const enteringViewItem = this.findEnteringViewForSwipe(swipeBackRouteInfo);
2631
2555
  const leavingViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id, false);
2632
- this.clearSwipeRevealedView();
2633
2556
  // Don't hide if entering and leaving are the same (parameterized route edge case)
2634
2557
  if (enteringViewItem !== leavingViewItem && enteringViewItem?.ionPageElement !== undefined) {
2635
2558
  hideIonPageElement(enteringViewItem.ionPageElement);
@@ -2856,10 +2779,34 @@ class StackManager extends React.PureComponent {
2856
2779
  // Bail out if the component unmounted during waitForComponentsReady
2857
2780
  if (!this._isMounted)
2858
2781
  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
+ }
2859
2807
  // Swap visibility synchronously - show entering, hide leaving
2860
- // Skip hiding if a newer transition already made leavingEl the entering view
2861
2808
  enteringEl.classList.remove('ion-page-invisible');
2862
- if (myGeneration === this.transitionGeneration || leavingEl !== this.transitionEnteringElement) {
2809
+ if (isLeaving) {
2863
2810
  leavingEl.classList.add('ion-page-hidden');
2864
2811
  leavingEl.setAttribute('aria-hidden', 'true');
2865
2812
  }