@ionic/react-router 8.7.13-dev.11766070268.1ab3dde2 → 8.7.13-dev.11766090775.11e2bebb

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
@@ -796,7 +796,6 @@ class ReactRouterViewStack extends ViewStacks {
796
796
  });
797
797
  if (hasSpecificMatch) {
798
798
  viewItem.mount = false;
799
- // Also hide the ion-page element immediately to prevent visual overlap
800
799
  if (viewItem.ionPageElement) {
801
800
  viewItem.ionPageElement.classList.add('ion-page-hidden');
802
801
  viewItem.ionPageElement.setAttribute('aria-hidden', 'true');
@@ -1191,18 +1190,12 @@ const VIEW_UNMOUNT_DELAY_MS = 250;
1191
1190
  */
1192
1191
  const ION_PAGE_WAIT_TIMEOUT_MS = 50;
1193
1192
  const isViewVisible = (el) => !el.classList.contains('ion-page-invisible') && !el.classList.contains('ion-page-hidden');
1194
- /**
1195
- * Hides an ion-page element by adding hidden class and aria attribute.
1196
- */
1197
1193
  const hideIonPageElement = (element) => {
1198
1194
  if (element) {
1199
1195
  element.classList.add('ion-page-hidden');
1200
1196
  element.setAttribute('aria-hidden', 'true');
1201
1197
  }
1202
1198
  };
1203
- /**
1204
- * Shows an ion-page element by removing hidden class and aria attribute.
1205
- */
1206
1199
  const showIonPageElement = (element) => {
1207
1200
  if (element) {
1208
1201
  element.classList.remove('ion-page-hidden');
@@ -1227,28 +1220,18 @@ class StackManager extends React.PureComponent {
1227
1220
  this.skipTransition = false;
1228
1221
  }
1229
1222
  /**
1230
- * Determines the parent path that was matched to reach this outlet.
1231
- * This helps with nested routing in React Router 6.
1232
- *
1233
- * The algorithm finds the shortest parent path where a route matches the remaining path.
1223
+ * Determines the parent path for nested routing in React Router 6.
1234
1224
  * Priority: specific routes > wildcard routes > index routes (only at mount point)
1235
1225
  */
1236
1226
  getParentPath() {
1237
1227
  const currentPathname = this.props.routeInfo.pathname;
1238
- // If this outlet previously established a mount path and the current
1239
- // pathname is outside of that scope, do not attempt to re-compute a new
1240
- // parent path. This prevents out-of-scope outlets from "adopting"
1241
- // unrelated routes (e.g., matching their index route under /overlays).
1228
+ // Prevent out-of-scope outlets from adopting unrelated routes
1242
1229
  if (this.outletMountPath && !currentPathname.startsWith(this.outletMountPath)) {
1243
1230
  return undefined;
1244
1231
  }
1245
- // Check if this outlet has route children to analyze
1246
1232
  if (this.ionRouterOutlet) {
1247
1233
  const routeChildren = extractRouteChildren(this.ionRouterOutlet.props.children);
1248
1234
  const { hasRelativeRoutes, hasIndexRoute, hasWildcardRoute } = analyzeRouteChildren(routeChildren);
1249
- // Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
1250
- // But even outlets with auto-generated IDs may need parent path computation
1251
- // if they have relative routes (indicating they're nested outlets)
1252
1235
  const isRootOutlet = this.id.startsWith('routerOutlet');
1253
1236
  const needsParentPath = !isRootOutlet || hasRelativeRoutes || hasIndexRoute;
1254
1237
  if (needsParentPath) {
@@ -1260,7 +1243,6 @@ class StackManager extends React.PureComponent {
1260
1243
  hasIndexRoute,
1261
1244
  hasWildcardRoute,
1262
1245
  });
1263
- // Update the outlet mount path if it was set
1264
1246
  if (result.outletMountPath && !this.outletMountPath) {
1265
1247
  this.outletMountPath = result.outletMountPath;
1266
1248
  }
@@ -1270,22 +1252,16 @@ class StackManager extends React.PureComponent {
1270
1252
  return this.outletMountPath;
1271
1253
  }
1272
1254
  /**
1273
- * Finds the entering and leaving view items for a route transition,
1274
- * handling special redirect cases.
1255
+ * Finds the entering and leaving view items, handling redirect cases.
1275
1256
  */
1276
1257
  findViewItems(routeInfo) {
1277
1258
  const enteringViewItem = this.context.findViewItemByRouteInfo(routeInfo, this.id);
1278
1259
  let leavingViewItem = this.context.findLeavingViewItemByRouteInfo(routeInfo, this.id);
1279
- // If we don't have a leaving view item, but the route info indicates
1280
- // that the user has routed from a previous path, then the leaving view
1281
- // can be found by the last known pathname.
1260
+ // Try to find leaving view by previous pathname
1282
1261
  if (!leavingViewItem && routeInfo.prevRouteLastPathname) {
1283
1262
  leavingViewItem = this.context.findViewItemByPathname(routeInfo.prevRouteLastPathname, this.id);
1284
1263
  }
1285
- // Special case for redirects: When a redirect happens inside a nested route,
1286
- // the entering and leaving view might be the same (the container route like tabs/*).
1287
- // In this case, we need to look at prevRouteLastPathname to find the actual
1288
- // view we're transitioning away from.
1264
+ // For redirects where entering === leaving, find the actual previous view
1289
1265
  if (enteringViewItem &&
1290
1266
  leavingViewItem &&
1291
1267
  enteringViewItem === leavingViewItem &&
@@ -1296,8 +1272,7 @@ class StackManager extends React.PureComponent {
1296
1272
  leavingViewItem = actualLeavingView;
1297
1273
  }
1298
1274
  }
1299
- // Also check if we're in a redirect scenario where entering and leaving are different
1300
- // but we still need to handle the actual previous view.
1275
+ // Handle redirect scenario with no leaving view
1301
1276
  if (enteringViewItem &&
1302
1277
  !leavingViewItem &&
1303
1278
  routeInfo.routeAction === 'replace' &&
@@ -1309,9 +1284,6 @@ class StackManager extends React.PureComponent {
1309
1284
  }
1310
1285
  return { enteringViewItem, leavingViewItem };
1311
1286
  }
1312
- /**
1313
- * Determines if the leaving view item should be unmounted after a transition.
1314
- */
1315
1287
  shouldUnmountLeavingView(routeInfo, enteringViewItem, leavingViewItem) {
1316
1288
  var _a, _b, _c, _d;
1317
1289
  if (!leavingViewItem) {
@@ -1320,22 +1292,19 @@ class StackManager extends React.PureComponent {
1320
1292
  if (routeInfo.routeAction === 'replace') {
1321
1293
  const enteringRoutePath = (_b = (_a = enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
1322
1294
  const leavingRoutePath = (_d = (_c = leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
1323
- // Never unmount the root path "/" - it's the main entry point for back navigation
1295
+ // Never unmount root path - needed for back navigation
1324
1296
  if (leavingRoutePath === '/' || leavingRoutePath === '') {
1325
1297
  return false;
1326
1298
  }
1327
1299
  if (enteringRoutePath && leavingRoutePath) {
1328
- // Get parent paths to check if routes share a common parent
1329
1300
  const getParentPath = (path) => {
1330
- const normalized = path.replace(/\/\*$/, ''); // Remove trailing /*
1301
+ const normalized = path.replace(/\/\*$/, '');
1331
1302
  const lastSlash = normalized.lastIndexOf('/');
1332
1303
  return lastSlash > 0 ? normalized.substring(0, lastSlash) : '/';
1333
1304
  };
1334
1305
  const enteringParent = getParentPath(enteringRoutePath);
1335
1306
  const leavingParent = getParentPath(leavingRoutePath);
1336
- // Unmount if:
1337
- // 1. Routes are siblings (same parent, e.g., /page1 and /page2, or /foo/page1 and /foo/page2)
1338
- // 2. Entering is a child of leaving (redirect, e.g., /tabs -> /tabs/tab1)
1307
+ // Unmount if routes are siblings or entering is a child of leaving (redirect)
1339
1308
  const areSiblings = enteringParent === leavingParent && enteringParent !== '/';
1340
1309
  const isChildRedirect = enteringRoutePath.startsWith(leavingRoutePath) ||
1341
1310
  (leavingRoutePath.endsWith('/*') && enteringRoutePath.startsWith(leavingRoutePath.slice(0, -2)));
@@ -1343,7 +1312,7 @@ class StackManager extends React.PureComponent {
1343
1312
  }
1344
1313
  return false;
1345
1314
  }
1346
- // For non-replace actions, only unmount for back navigation (not forward push)
1315
+ // For non-replace actions, only unmount for back navigation
1347
1316
  const isForwardPush = routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'forward';
1348
1317
  if (!isForwardPush && routeInfo.routeDirection !== 'none' && enteringViewItem !== leavingViewItem) {
1349
1318
  return true;
@@ -1351,21 +1320,17 @@ class StackManager extends React.PureComponent {
1351
1320
  return false;
1352
1321
  }
1353
1322
  /**
1354
- * Handles the case when the outlet is out of scope (current route is outside mount path).
1355
- * Returns true if the transition should be aborted.
1323
+ * Handles out-of-scope outlet. Returns true if transition should be aborted.
1356
1324
  */
1357
1325
  handleOutOfScopeOutlet(routeInfo) {
1358
1326
  if (!this.outletMountPath || routeInfo.pathname.startsWith(this.outletMountPath)) {
1359
1327
  return false;
1360
1328
  }
1361
- // Clear any pending unmount timeout to avoid conflicts
1362
1329
  if (this.outOfScopeUnmountTimeout) {
1363
1330
  clearTimeout(this.outOfScopeUnmountTimeout);
1364
1331
  this.outOfScopeUnmountTimeout = undefined;
1365
1332
  }
1366
- // When an outlet is out of scope, unmount its views immediately
1367
1333
  const allViewsInOutlet = this.context.getViewItemsForOutlet ? this.context.getViewItemsForOutlet(this.id) : [];
1368
- // Unmount and remove all views in this outlet immediately to avoid leftover content
1369
1334
  allViewsInOutlet.forEach((viewItem) => {
1370
1335
  hideIonPageElement(viewItem.ionPageElement);
1371
1336
  this.context.unMountViewItem(viewItem);
@@ -1374,12 +1339,10 @@ class StackManager extends React.PureComponent {
1374
1339
  return true;
1375
1340
  }
1376
1341
  /**
1377
- * Handles the case when this is a nested outlet with relative routes but no valid parent path.
1378
- * Returns true if the transition should be aborted.
1342
+ * Handles nested outlet with relative routes but no parent path. Returns true to abort.
1379
1343
  */
1380
1344
  handleOutOfContextNestedOutlet(parentPath, leavingViewItem) {
1381
1345
  var _a;
1382
- // Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
1383
1346
  const isRootOutlet = this.id.startsWith('routerOutlet');
1384
1347
  if (isRootOutlet || parentPath !== undefined || !this.ionRouterOutlet) {
1385
1348
  return false;
@@ -1391,7 +1354,6 @@ class StackManager extends React.PureComponent {
1391
1354
  return path && !path.startsWith('/') && path !== '*';
1392
1355
  });
1393
1356
  if (hasRelativeRoutes) {
1394
- // Hide any visible views in this outlet since it's out of scope
1395
1357
  hideIonPageElement(leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
1396
1358
  if (leavingViewItem) {
1397
1359
  leavingViewItem.mount = false;
@@ -1402,16 +1364,13 @@ class StackManager extends React.PureComponent {
1402
1364
  return false;
1403
1365
  }
1404
1366
  /**
1405
- * Handles the case when a nested outlet has no matching route.
1406
- * Returns true if the transition should be aborted.
1367
+ * Handles nested outlet with no matching route. Returns true to abort.
1407
1368
  */
1408
1369
  handleNoMatchingRoute(enteringRoute, enteringViewItem, leavingViewItem) {
1409
- // Root outlets have IDs like 'routerOutlet' or 'routerOutlet-2'
1410
1370
  const isRootOutlet = this.id.startsWith('routerOutlet');
1411
1371
  if (isRootOutlet || enteringRoute || enteringViewItem) {
1412
1372
  return false;
1413
1373
  }
1414
- // Hide any visible views in this outlet since it has no matching route
1415
1374
  hideIonPageElement(leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
1416
1375
  if (leavingViewItem) {
1417
1376
  leavingViewItem.mount = false;
@@ -1420,16 +1379,15 @@ class StackManager extends React.PureComponent {
1420
1379
  return true;
1421
1380
  }
1422
1381
  /**
1423
- * Handles the transition when entering view item has an ion-page element ready.
1382
+ * Handles transition when entering view has ion-page element ready.
1424
1383
  */
1425
1384
  handleReadyEnteringView(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem) {
1426
1385
  var _a, _b;
1427
- // Handle same view item case (e.g., parameterized route changes)
1386
+ // Handle parameterized route changes (same view, different params)
1428
1387
  if (enteringViewItem === leavingViewItem) {
1429
1388
  const routePath = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
1430
1389
  const isParameterizedRoute = routePath ? routePath.includes(':') : false;
1431
1390
  if (isParameterizedRoute) {
1432
- // Refresh match metadata so the component receives updated params
1433
1391
  const updatedMatch = matchComponent(enteringViewItem.reactElement, routeInfo.pathname, true);
1434
1392
  if (updatedMatch) {
1435
1393
  enteringViewItem.routeData.match = updatedMatch;
@@ -1443,23 +1401,16 @@ class StackManager extends React.PureComponent {
1443
1401
  return;
1444
1402
  }
1445
1403
  }
1446
- // Try to find leaving view using prev route info if still not found
1447
1404
  if (!leavingViewItem && this.props.routeInfo.prevRouteLastPathname) {
1448
1405
  leavingViewItem = this.context.findViewItemByPathname(this.props.routeInfo.prevRouteLastPathname, this.id);
1449
1406
  }
1450
- // Ensure the entering view is marked as mounted.
1451
- // This is critical for views that were previously unmounted (e.g., navigating back to home).
1452
- // When mount=false, the ViewLifeCycleManager doesn't render the IonPage, so the
1453
- // ionPageElement reference becomes stale. By setting mount=true, we ensure the view
1454
- // gets re-rendered and a new IonPage is created.
1407
+ // Re-mount views that were previously unmounted (e.g., navigating back to home)
1455
1408
  if (!enteringViewItem.mount) {
1456
1409
  enteringViewItem.mount = true;
1457
1410
  }
1458
- // Check visibility state BEFORE showing the entering view.
1459
- // This must be done before showIonPageElement to get accurate visibility state.
1411
+ // Check visibility state BEFORE showing entering view
1460
1412
  const enteringWasVisible = enteringViewItem.ionPageElement && isViewVisible(enteringViewItem.ionPageElement);
1461
1413
  const leavingIsHidden = leavingViewItem !== undefined && leavingViewItem.ionPageElement && !isViewVisible(leavingViewItem.ionPageElement);
1462
- // Check for duplicate transition
1463
1414
  const currentTransition = {
1464
1415
  enteringId: enteringViewItem.id,
1465
1416
  leavingId: leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.id,
@@ -1469,91 +1420,56 @@ class StackManager extends React.PureComponent {
1469
1420
  this.lastTransition.leavingId &&
1470
1421
  this.lastTransition.enteringId === currentTransition.enteringId &&
1471
1422
  this.lastTransition.leavingId === currentTransition.leavingId;
1472
- // Skip transition if entering view was ALREADY visible and leaving view is not visible.
1473
- // This indicates the transition has already been performed (e.g., via swipe gesture).
1474
- // IMPORTANT: Only skip if both ionPageElements are the same as when the transition was last done.
1475
- // If the leaving view's ionPageElement changed (e.g., component re-rendered with different IonPage),
1476
- // we should NOT skip because the DOM state is inconsistent.
1423
+ // Skip if transition already performed (e.g., via swipe gesture)
1477
1424
  if (enteringWasVisible && leavingIsHidden && isDuplicateTransition) {
1478
- // For swipe-to-go-back, the transition animation was handled by the gesture.
1479
- // We still need to set mount=false so React unmounts the leaving view.
1480
- // Only do this when skipTransition is set (indicating gesture completion).
1481
1425
  if (this.skipTransition &&
1482
1426
  shouldUnmountLeavingViewItem &&
1483
1427
  leavingViewItem &&
1484
1428
  enteringViewItem !== leavingViewItem) {
1485
1429
  leavingViewItem.mount = false;
1486
- // Call transitionPage with duration 0 to trigger ionViewDidLeave lifecycle
1487
- // which is needed for ViewLifeCycleManager to remove the view.
1430
+ // Trigger ionViewDidLeave lifecycle for ViewLifeCycleManager cleanup
1488
1431
  this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back');
1489
1432
  }
1490
- // Clear skipTransition since we're not calling transitionPage which normally clears it
1491
1433
  this.skipTransition = false;
1492
- // Must call forceUpdate to trigger re-render after mount state change
1493
1434
  this.forceUpdate();
1494
1435
  return;
1495
1436
  }
1496
- // Ensure the entering view is not hidden from previous navigations
1497
- // This must happen AFTER the visibility check above
1498
1437
  showIonPageElement(enteringViewItem.ionPageElement);
1499
- // Skip if this is a duplicate transition (but visibility state didn't match above)
1500
- // OR if skipTransition is set (swipe gesture already handled the animation)
1438
+ // Handle duplicate transition or swipe gesture completion
1501
1439
  if (isDuplicateTransition || this.skipTransition) {
1502
- // For swipe-to-go-back, we still need to handle unmounting even if visibility
1503
- // conditions aren't fully met (animation might still be in progress)
1504
1440
  if (this.skipTransition &&
1505
1441
  shouldUnmountLeavingViewItem &&
1506
1442
  leavingViewItem &&
1507
1443
  enteringViewItem !== leavingViewItem) {
1508
1444
  leavingViewItem.mount = false;
1509
- // For swipe-to-go-back, we need to call transitionPage with duration 0 to
1510
- // trigger the ionViewDidLeave lifecycle event. The ViewLifeCycleManager
1511
- // uses componentCanBeDestroyed callback to remove the view, which is
1512
- // only called from ionViewDidLeave. Since the gesture animation already
1513
- // completed before mount=false was set, we need to re-fire the lifecycle.
1445
+ // Re-fire ionViewDidLeave since gesture completed before mount=false was set
1514
1446
  this.transitionPage(routeInfo, enteringViewItem, leavingViewItem, 'back');
1515
1447
  }
1516
- // Clear skipTransition since we're not calling transitionPage which normally clears it
1517
1448
  this.skipTransition = false;
1518
- // Must call forceUpdate to trigger re-render after mount state change
1519
1449
  this.forceUpdate();
1520
1450
  return;
1521
1451
  }
1522
1452
  this.lastTransition = currentTransition;
1523
1453
  this.transitionPage(routeInfo, enteringViewItem, leavingViewItem);
1524
- // Handle unmounting the leaving view
1525
1454
  if (shouldUnmountLeavingViewItem && leavingViewItem && enteringViewItem !== leavingViewItem) {
1526
1455
  leavingViewItem.mount = false;
1527
1456
  this.handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem);
1528
1457
  }
1529
- // Clean up any orphaned sibling views that are no longer reachable
1530
- // This is important for replace actions (like redirects) where sibling views
1531
- // that were pushed earlier become unreachable
1458
+ // Clean up orphaned sibling views after replace actions (redirects)
1532
1459
  this.cleanupOrphanedSiblingViews(routeInfo, enteringViewItem, leavingViewItem);
1533
1460
  }
1534
1461
  /**
1535
- * Handles the delayed unmount of the leaving view item.
1536
- * For 'replace' actions: handles container route transitions specially.
1537
- * For back navigation: explicitly unmounts because the ionViewDidLeave lifecycle
1538
- * fires DURING transitionPage, but mount=false is set AFTER.
1539
- *
1540
- * @param routeInfo Current route information
1541
- * @param enteringViewItem The view being navigated to
1542
- * @param leavingViewItem The view being navigated from
1462
+ * Handles leaving view unmount for replace actions.
1543
1463
  */
1544
1464
  handleLeavingViewUnmount(routeInfo, enteringViewItem, leavingViewItem) {
1545
1465
  var _a, _b, _c, _d, _e, _f;
1546
1466
  if (!leavingViewItem.ionPageElement) {
1547
1467
  return;
1548
1468
  }
1549
- // For push/pop actions, do NOT unmount - views are cached for navigation history.
1550
- // Push: Forward navigation caches views for back navigation
1551
- // Pop: Back navigation should not unmount the entering view's history
1552
- // Only 'replace' actions should actually unmount views since they replace history.
1469
+ // Only replace actions unmount views; push/pop cache for navigation history
1553
1470
  if (routeInfo.routeAction !== 'replace') {
1554
1471
  return;
1555
1472
  }
1556
- // For replace actions, check if we should skip removal for nested outlet redirects
1557
1473
  const enteringRoutePath = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
1558
1474
  const leavingRoutePath = (_d = (_c = leavingViewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
1559
1475
  const isEnteringContainerRoute = enteringRoutePath && enteringRoutePath.endsWith('/*');
@@ -1570,41 +1486,43 @@ class StackManager extends React.PureComponent {
1570
1486
  const viewToUnmount = leavingViewItem;
1571
1487
  setTimeout(() => {
1572
1488
  this.context.unMountViewItem(viewToUnmount);
1573
- // Trigger re-render to remove the view from DOM
1574
1489
  this.forceUpdate();
1575
1490
  }, VIEW_UNMOUNT_DELAY_MS);
1576
1491
  }
1577
1492
  /**
1578
- * Cleans up orphaned sibling views after a replace action.
1579
- * When navigating via replace (e.g., through a redirect), sibling views that were
1580
- * pushed earlier may become orphaned (unreachable via back navigation).
1581
- * This method identifies and unmounts such views.
1493
+ * Cleans up orphaned sibling views after replace actions or push-to-container navigations.
1582
1494
  */
1583
1495
  cleanupOrphanedSiblingViews(routeInfo, enteringViewItem, leavingViewItem) {
1584
- var _a, _b, _c, _d;
1585
- // Only cleanup for replace actions
1586
- if (routeInfo.routeAction !== 'replace') {
1587
- return;
1588
- }
1496
+ var _a, _b, _c, _d, _e, _f, _g;
1589
1497
  const enteringRoutePath = (_b = (_a = enteringViewItem.reactElement) === null || _a === void 0 ? void 0 : _a.props) === null || _b === void 0 ? void 0 : _b.path;
1590
1498
  if (!enteringRoutePath) {
1591
1499
  return;
1592
1500
  }
1593
- // Get all views in this outlet
1501
+ const leavingRoutePath = (_d = (_c = leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
1502
+ const isContainerRoute = (path) => path === null || path === void 0 ? void 0 : path.endsWith('/*');
1503
+ const isReplaceAction = routeInfo.routeAction === 'replace';
1504
+ const isPushToContainer = routeInfo.routeAction === 'push' && routeInfo.routeDirection === 'none' && isContainerRoute(enteringRoutePath);
1505
+ if (!isReplaceAction && !isPushToContainer) {
1506
+ return;
1507
+ }
1508
+ // Skip cleanup for tab switches
1509
+ const isSameView = enteringViewItem === leavingViewItem;
1510
+ const isSameContainerRoute = isContainerRoute(enteringRoutePath) && leavingRoutePath === enteringRoutePath;
1511
+ const isNavigatingWithinContainer = isPushToContainer &&
1512
+ !leavingViewItem &&
1513
+ ((_e = routeInfo.prevRouteLastPathname) === null || _e === void 0 ? void 0 : _e.startsWith(enteringRoutePath.replace(/\/\*$/, '')));
1514
+ if (isSameView || isSameContainerRoute || isNavigatingWithinContainer) {
1515
+ return;
1516
+ }
1594
1517
  const allViewsInOutlet = this.context.getViewItemsForOutlet ? this.context.getViewItemsForOutlet(this.id) : [];
1595
- // Check if routes are "siblings" - direct children of the same outlet at the same level
1596
1518
  const areSiblingRoutes = (path1, path2) => {
1597
- // Both are relative routes (don't start with /)
1598
1519
  const path1IsRelative = !path1.startsWith('/');
1599
1520
  const path2IsRelative = !path2.startsWith('/');
1600
- // For relative routes at the outlet root level, they're siblings
1601
1521
  if (path1IsRelative && path2IsRelative) {
1602
- // Check if they're at the same depth (no nested slashes, except for wildcards)
1603
1522
  const path1Depth = path1.replace(/\/\*$/, '').split('/').filter(Boolean).length;
1604
1523
  const path2Depth = path2.replace(/\/\*$/, '').split('/').filter(Boolean).length;
1605
1524
  return path1Depth === path2Depth && path1Depth <= 1;
1606
1525
  }
1607
- // For absolute routes, check if they share the same parent
1608
1526
  const getParent = (path) => {
1609
1527
  const normalized = path.replace(/\/\*$/, '');
1610
1528
  const lastSlash = normalized.lastIndexOf('/');
@@ -1613,13 +1531,7 @@ class StackManager extends React.PureComponent {
1613
1531
  return getParent(path1) === getParent(path2);
1614
1532
  };
1615
1533
  for (const viewItem of allViewsInOutlet) {
1616
- const viewRoutePath = (_d = (_c = viewItem.reactElement) === null || _c === void 0 ? void 0 : _c.props) === null || _d === void 0 ? void 0 : _d.path;
1617
- // Skip views that shouldn't be cleaned up:
1618
- // - The entering view itself
1619
- // - The immediate leaving view (handled separately by handleLeavingViewUnmount)
1620
- // - Already unmounted views
1621
- // - Views without a route path
1622
- // - Container routes (ending in /*) when entering is also a container route
1534
+ const viewRoutePath = (_g = (_f = viewItem.reactElement) === null || _f === void 0 ? void 0 : _f.props) === null || _g === void 0 ? void 0 : _g.path;
1623
1535
  const shouldSkip = viewItem.id === enteringViewItem.id ||
1624
1536
  (leavingViewItem && viewItem.id === leavingViewItem.id) ||
1625
1537
  !viewItem.mount ||
@@ -1628,12 +1540,9 @@ class StackManager extends React.PureComponent {
1628
1540
  if (shouldSkip) {
1629
1541
  continue;
1630
1542
  }
1631
- // Check if this is a sibling route that should be cleaned up
1632
1543
  if (areSiblingRoutes(enteringRoutePath, viewRoutePath)) {
1633
- // Hide and unmount the orphaned view
1634
1544
  hideIonPageElement(viewItem.ionPageElement);
1635
1545
  viewItem.mount = false;
1636
- // Schedule removal
1637
1546
  const viewToRemove = viewItem;
1638
1547
  setTimeout(() => {
1639
1548
  this.context.unMountViewItem(viewToRemove);
@@ -1643,7 +1552,7 @@ class StackManager extends React.PureComponent {
1643
1552
  }
1644
1553
  }
1645
1554
  /**
1646
- * Handles the case when entering view has no ion-page element yet (waiting for render).
1555
+ * Handles entering view with no ion-page element yet (waiting for render).
1647
1556
  */
1648
1557
  handleWaitingForIonPage(routeInfo, enteringViewItem, leavingViewItem, shouldUnmountLeavingViewItem) {
1649
1558
  var _a, _b;
@@ -1858,6 +1767,18 @@ class StackManager extends React.PureComponent {
1858
1767
  * @param routeInfo The route information that associates with `<IonPage>`.
1859
1768
  */
1860
1769
  registerIonPage(page, routeInfo) {
1770
+ /**
1771
+ * DO NOT remove ion-page-invisible here.
1772
+ *
1773
+ * PageManager.componentDidMount adds ion-page-invisible before calling registerIonPage.
1774
+ * At this point, the <IonPage> div exists but its CHILDREN (header, toolbar, menu-button)
1775
+ * have NOT rendered yet. If we remove ion-page-invisible now, the page becomes visible
1776
+ * with empty/incomplete content, causing a flicker (especially for ion-menu-button which
1777
+ * starts with menu-button-hidden class).
1778
+ *
1779
+ * Instead, let transitionPage handle visibility AFTER waiting for components to be ready.
1780
+ * This ensures the page only becomes visible when its content is fully rendered.
1781
+ */
1861
1782
  this.waitingForIonPage = false;
1862
1783
  if (this.ionPageWaitTimeout) {
1863
1784
  clearTimeout(this.ionPageWaitTimeout);
@@ -1894,8 +1815,7 @@ class StackManager extends React.PureComponent {
1894
1815
  this.handlePageTransition(routeInfo);
1895
1816
  }
1896
1817
  /**
1897
- * Determines if a new IonPage registration should be rejected as orphaned.
1898
- * This happens when a component re-renders with a different IonPage while navigating away.
1818
+ * Checks if a new IonPage should be rejected (component re-rendered while navigating away).
1899
1819
  */
1900
1820
  shouldRejectOrphanedPage(newPage, oldPageElement, routeInfo) {
1901
1821
  if (!oldPageElement || oldPageElement === newPage) {
@@ -1903,16 +1823,11 @@ class StackManager extends React.PureComponent {
1903
1823
  }
1904
1824
  const newPageId = newPage.getAttribute('data-pageid');
1905
1825
  const oldPageId = oldPageElement.getAttribute('data-pageid');
1906
- // Only reject if both pageIds exist and are different
1907
1826
  if (!newPageId || !oldPageId || newPageId === oldPageId) {
1908
1827
  return false;
1909
1828
  }
1910
- // Reject only if we're navigating away from this route
1911
1829
  return this.props.routeInfo.pathname !== routeInfo.pathname;
1912
1830
  }
1913
- /**
1914
- * Hides an orphaned IonPage and schedules its removal from the DOM.
1915
- */
1916
1831
  hideAndRemoveOrphanedPage(page) {
1917
1832
  page.classList.add('ion-page-hidden');
1918
1833
  page.setAttribute('aria-hidden', 'true');
@@ -1923,40 +1838,25 @@ class StackManager extends React.PureComponent {
1923
1838
  }, VIEW_UNMOUNT_DELAY_MS);
1924
1839
  }
1925
1840
  /**
1926
- * Configures the router outlet for the swipe-to-go-back gesture.
1927
- *
1928
- * @param routerOutlet The Ionic router outlet component: `<IonRouterOutlet>`.
1841
+ * Configures swipe-to-go-back gesture for the router outlet.
1929
1842
  */
1930
1843
  async setupRouterOutlet(routerOutlet) {
1931
1844
  const canStart = () => {
1932
1845
  const config = getConfig();
1933
- // Check if swipe back is enabled in config (default to true for iOS mode)
1934
1846
  const swipeEnabled = config && config.get('swipeBackEnabled', routerOutlet.mode === 'ios');
1935
1847
  if (!swipeEnabled) {
1936
1848
  return false;
1937
1849
  }
1938
1850
  const { routeInfo } = this.props;
1939
1851
  const swipeBackRouteInfo = this.getSwipeBackRouteInfo();
1940
- // First try to find the view in the current outlet
1941
1852
  let enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, this.id, false);
1942
- // If not found in current outlet, search all outlets (for cross-outlet swipe back)
1943
1853
  if (!enteringViewItem) {
1944
1854
  enteringViewItem = this.context.findViewItemByRouteInfo(swipeBackRouteInfo, undefined, false);
1945
1855
  }
1946
- // Check if the ionPageElement is still in the document.
1947
- // A view might have mount=false but still have its ionPageElement in the DOM
1948
- // (due to timing differences in unmounting).
1856
+ // View might have mount=false but ionPageElement still in DOM
1949
1857
  const ionPageInDocument = Boolean((enteringViewItem === null || enteringViewItem === void 0 ? void 0 : enteringViewItem.ionPageElement) && document.body.contains(enteringViewItem.ionPageElement));
1950
1858
  const canStartSwipe = !!enteringViewItem &&
1951
- // Check if we can swipe to this view. Either:
1952
- // 1. The view is mounted (mount=true), OR
1953
- // 2. The view's ionPageElement is still in the document
1954
- // The second case handles views that have been marked for unmount but haven't
1955
- // actually been removed from the DOM yet.
1956
1859
  (enteringViewItem.mount || ionPageInDocument) &&
1957
- // When on the first page it is possible for findViewItemByRouteInfo to
1958
- // return the exact same view you are currently on.
1959
- // Make sure that we are not swiping back to the same instances of a view.
1960
1860
  enteringViewItem.routeData.match.pattern.path !== routeInfo.pathname;
1961
1861
  return canStartSwipe;
1962
1862
  };
@@ -2064,9 +1964,7 @@ class StackManager extends React.PureComponent {
2064
1964
  const directionToUse = direction !== null && direction !== void 0 ? direction : routeInfoFallbackDirection;
2065
1965
  if (enteringViewItem && enteringViewItem.ionPageElement && this.routerOutletElement) {
2066
1966
  if (leavingViewItem && leavingViewItem.ionPageElement && enteringViewItem === leavingViewItem) {
2067
- // If a page is transitioning to another version of itself
2068
- // we clone it so we can have an animation to show
2069
- // (e.g., `/user/1` → `/user/2`)
1967
+ // Clone page for same-view transitions (e.g., /user/1 /user/2)
2070
1968
  const match = matchComponent(leavingViewItem.reactElement, routeInfo.pathname);
2071
1969
  if (match) {
2072
1970
  const newLeavingElement = clonePageElement(leavingViewItem.ionPageElement.outerHTML);
@@ -2077,23 +1975,98 @@ class StackManager extends React.PureComponent {
2077
1975
  }
2078
1976
  }
2079
1977
  else {
2080
- /**
2081
- * The route no longer matches the component type of the leaving view.
2082
- * (e.g., `/user/1` → `/settings`)
2083
- *
2084
- * This can also occur in edge cases like rapid navigation
2085
- * or during parent component re-renders that briefly cause
2086
- * the view items to be the same instance before the final
2087
- * route component is determined.
2088
- */
1978
+ // Route no longer matches (e.g., /user/1 → /settings)
2089
1979
  await runCommit(enteringViewItem.ionPageElement, undefined);
2090
1980
  }
2091
1981
  }
2092
1982
  else {
2093
- await runCommit(enteringViewItem.ionPageElement, leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement);
2094
- if (leavingViewItem && leavingViewItem.ionPageElement && !progressAnimation) {
2095
- leavingViewItem.ionPageElement.classList.add('ion-page-hidden');
2096
- leavingViewItem.ionPageElement.setAttribute('aria-hidden', 'true');
1983
+ const leavingEl = leavingViewItem === null || leavingViewItem === void 0 ? void 0 : leavingViewItem.ionPageElement;
1984
+ // For non-animated transitions, don't pass leaving element to commit() to avoid
1985
+ // flicker caused by commit() briefly unhiding the leaving page
1986
+ const isNonAnimatedTransition = directionToUse === undefined && !progressAnimation;
1987
+ if (isNonAnimatedTransition && leavingEl) {
1988
+ /**
1989
+ * Flicker prevention for non-animated transitions:
1990
+ * 1. Keep entering invisible during commit and component mounting
1991
+ * 2. Wait for components (including menu button) to be ready
1992
+ * 3. Swap visibility atomically
1993
+ */
1994
+ const enteringEl = enteringViewItem.ionPageElement;
1995
+ enteringEl.classList.add('ion-page');
1996
+ if (!enteringEl.classList.contains('ion-page-invisible')) {
1997
+ enteringEl.classList.add('ion-page-invisible');
1998
+ }
1999
+ enteringEl.classList.remove('ion-page-hidden');
2000
+ enteringEl.removeAttribute('aria-hidden');
2001
+ await routerOutlet.commit(enteringEl, undefined, {
2002
+ duration: 0,
2003
+ direction: undefined,
2004
+ showGoBack: !!routeInfo.pushedByRoute,
2005
+ progressAnimation: false,
2006
+ animationBuilder: routeInfo.routeAnimation,
2007
+ });
2008
+ // Re-add invisible after commit removes it (commit's afterTransition handling)
2009
+ enteringEl.classList.add('ion-page-invisible');
2010
+ /**
2011
+ * Wait for components to be ready. Menu buttons start hidden (menu-button-hidden)
2012
+ * and become visible after componentDidLoad. Wait for hydration and visibility.
2013
+ */
2014
+ const waitForComponentsReady = () => {
2015
+ return new Promise((resolve) => {
2016
+ const checkReady = () => {
2017
+ const ionicComponents = enteringEl.querySelectorAll('ion-header, ion-toolbar, ion-buttons, ion-menu-button, ion-title, ion-content');
2018
+ const allHydrated = Array.from(ionicComponents).every((el) => el.classList.contains('hydrated'));
2019
+ const menuButtons = enteringEl.querySelectorAll('ion-menu-button');
2020
+ const menuButtonsReady = Array.from(menuButtons).every((el) => !el.classList.contains('menu-button-hidden'));
2021
+ return allHydrated && menuButtonsReady;
2022
+ };
2023
+ if (checkReady()) {
2024
+ resolve();
2025
+ return;
2026
+ }
2027
+ let resolved = false;
2028
+ const observer = new MutationObserver(() => {
2029
+ if (!resolved && checkReady()) {
2030
+ resolved = true;
2031
+ observer.disconnect();
2032
+ resolve();
2033
+ }
2034
+ });
2035
+ observer.observe(enteringEl, {
2036
+ subtree: true,
2037
+ attributes: true,
2038
+ attributeFilter: ['class'],
2039
+ });
2040
+ setTimeout(() => {
2041
+ if (!resolved) {
2042
+ resolved = true;
2043
+ observer.disconnect();
2044
+ resolve();
2045
+ }
2046
+ }, 100);
2047
+ });
2048
+ };
2049
+ await waitForComponentsReady();
2050
+ // Swap visibility in sync with browser's render cycle
2051
+ await new Promise((resolve) => {
2052
+ requestAnimationFrame(() => {
2053
+ enteringEl.classList.remove('ion-page-invisible');
2054
+ // Second rAF ensures entering is painted before hiding leaving
2055
+ requestAnimationFrame(() => {
2056
+ leavingEl.classList.add('ion-page-hidden');
2057
+ leavingEl.setAttribute('aria-hidden', 'true');
2058
+ resolve();
2059
+ });
2060
+ });
2061
+ });
2062
+ }
2063
+ else {
2064
+ await runCommit(enteringViewItem.ionPageElement, leavingEl);
2065
+ // For animated transitions, hide leaving element after commit completes
2066
+ if (leavingEl && !progressAnimation) {
2067
+ leavingEl.classList.add('ion-page-hidden');
2068
+ leavingEl.setAttribute('aria-hidden', 'true');
2069
+ }
2097
2070
  }
2098
2071
  }
2099
2072
  }