@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 +148 -175
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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(/\/\*$/, '');
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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 = (
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
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
|
}
|