@ionic/react 9.0.1-nightly.20260825 → 9.0.1
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 +177 -53
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1395,6 +1395,72 @@ const detachEvent = (node, eventName) => {
|
|
|
1395
1395
|
* React event-delegation root.
|
|
1396
1396
|
*/
|
|
1397
1397
|
const NestedOverlayContext = React.createContext(false);
|
|
1398
|
+
/**
|
|
1399
|
+
* React skips `componentWillUnmount` when it destroys a subtree it had already
|
|
1400
|
+
* hidden, so a hidden overlay watches its own marker leave the document. One
|
|
1401
|
+
* observer serves every watcher, since each one allocates its own records for
|
|
1402
|
+
* a whole root.
|
|
1403
|
+
*
|
|
1404
|
+
* This all assumes React hides with `display: none` rather than detaching, so
|
|
1405
|
+
* the marker survives a hide. True today, but an implementation detail.
|
|
1406
|
+
*/
|
|
1407
|
+
const destroyWatches = new Set();
|
|
1408
|
+
let destroyObserver;
|
|
1409
|
+
const stopDestroyObserverIfIdle = () => {
|
|
1410
|
+
if (destroyWatches.size === 0) {
|
|
1411
|
+
destroyObserver?.disconnect();
|
|
1412
|
+
destroyObserver = undefined;
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
const watchForDestroy = (marker, onDestroy) => {
|
|
1416
|
+
if (typeof MutationObserver === 'undefined') {
|
|
1417
|
+
return undefined;
|
|
1418
|
+
}
|
|
1419
|
+
const watch = { marker, onDestroy };
|
|
1420
|
+
destroyWatches.add(watch);
|
|
1421
|
+
if (destroyObserver === undefined) {
|
|
1422
|
+
destroyObserver = new MutationObserver(() => {
|
|
1423
|
+
try {
|
|
1424
|
+
for (const candidate of Array.from(destroyWatches)) {
|
|
1425
|
+
if (candidate.marker.isConnected) {
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
destroyWatches.delete(candidate);
|
|
1429
|
+
// `detachProps` assigns arbitrary props onto a custom element, so a
|
|
1430
|
+
// throwing setter would otherwise strand every remaining overlay.
|
|
1431
|
+
try {
|
|
1432
|
+
candidate.onDestroy();
|
|
1433
|
+
}
|
|
1434
|
+
catch (error) {
|
|
1435
|
+
console.error(error);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
finally {
|
|
1440
|
+
stopDestroyObserverIfIdle();
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* React usually removes the marker's own parent, and a `childList` observer
|
|
1446
|
+
* never fires for its own removal, so watch from the root down. Removing a
|
|
1447
|
+
* shadow host disconnects the marker without mutating anything inside its
|
|
1448
|
+
* root, so walk up the host chain too.
|
|
1449
|
+
*/
|
|
1450
|
+
for (let root = marker.getRootNode(); root !== null;) {
|
|
1451
|
+
destroyObserver.observe(root, { childList: true, subtree: true });
|
|
1452
|
+
const { host } = root;
|
|
1453
|
+
root = host === undefined ? null : host.getRootNode();
|
|
1454
|
+
}
|
|
1455
|
+
return watch;
|
|
1456
|
+
};
|
|
1457
|
+
const unwatchForDestroy = (watch) => {
|
|
1458
|
+
if (watch === undefined) {
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
destroyWatches.delete(watch);
|
|
1462
|
+
stopDestroyObserverIfIdle();
|
|
1463
|
+
};
|
|
1398
1464
|
const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateHost) => {
|
|
1399
1465
|
if (defineCustomElement) {
|
|
1400
1466
|
defineCustomElement();
|
|
@@ -1406,7 +1472,18 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
|
|
|
1406
1472
|
markerRef;
|
|
1407
1473
|
stableMergedRefs;
|
|
1408
1474
|
portalTarget;
|
|
1409
|
-
|
|
1475
|
+
// Bumped on every mount, so a deferred teardown can tell it was revealed.
|
|
1476
|
+
mountCount = 0;
|
|
1477
|
+
hiddenDestroyWatch;
|
|
1478
|
+
// Where a relocated portaled host sat before `componentWillUnmount` moved
|
|
1479
|
+
// it back into `portalTarget`, so a reveal can put it back.
|
|
1480
|
+
relocatedPortalHost;
|
|
1481
|
+
// React nulls the refs for as long as it keeps this subtree hidden, and a
|
|
1482
|
+
// dismiss can land in that window, so `handleDidDismiss` falls back to these.
|
|
1483
|
+
nodesAtUnmount = null;
|
|
1484
|
+
// Core clears `cachedOriginalParent` when the parent is removed, so the
|
|
1485
|
+
// redirect must not re-run on a reveal and put the reference back.
|
|
1486
|
+
hasRedirectedOriginalParent = false;
|
|
1410
1487
|
constructor(props) {
|
|
1411
1488
|
super(props);
|
|
1412
1489
|
// Create a local ref to to attach props to the wrapped element.
|
|
@@ -1417,8 +1494,6 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
|
|
|
1417
1494
|
this.state = { isOpen: false };
|
|
1418
1495
|
// Create a local ref to the inner child element.
|
|
1419
1496
|
this.wrapperRef = React.createRef();
|
|
1420
|
-
// Marker stays at the JSX location so we can recover the immediate
|
|
1421
|
-
// JSX parent after the overlay has been portaled to ion-app.
|
|
1422
1497
|
this.markerRef = React.createRef();
|
|
1423
1498
|
/**
|
|
1424
1499
|
* Resolve the portal target to the same container CoreDelegate
|
|
@@ -1430,10 +1505,11 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
|
|
|
1430
1505
|
this.portalTarget = typeof document !== 'undefined' ? document.querySelector('ion-app') || document.body : null;
|
|
1431
1506
|
}
|
|
1432
1507
|
componentDidMount() {
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
//
|
|
1436
|
-
this.
|
|
1508
|
+
this.mountCount++;
|
|
1509
|
+
this.nodesAtUnmount = null;
|
|
1510
|
+
// React is showing this subtree again, so cancel the hide's teardown.
|
|
1511
|
+
this.stopWatchingForDestroy();
|
|
1512
|
+
this.restoreRelocatedPortalHost();
|
|
1437
1513
|
this.componentDidUpdate(this.props);
|
|
1438
1514
|
this.ref.current?.addEventListener('ionMount', this.handleIonMount);
|
|
1439
1515
|
this.ref.current?.addEventListener('willPresent', this.handleWillPresent);
|
|
@@ -1444,14 +1520,23 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
|
|
|
1444
1520
|
* child-route passthrough, parent-removal auto-dismiss) walk up
|
|
1445
1521
|
* from `cachedOriginalParent` to find the enclosing `.ion-page`,
|
|
1446
1522
|
* so we redirect it at the marker's JSX parent.
|
|
1523
|
+
*
|
|
1524
|
+
* Nested overlays never portal, so they already cached their
|
|
1525
|
+
* `<template>` at the JSX position and resolve the right `.ion-page`.
|
|
1447
1526
|
*/
|
|
1448
1527
|
const overlay = this.ref.current;
|
|
1449
|
-
|
|
1528
|
+
const generation = this.mountCount;
|
|
1529
|
+
if (overlay && !this.props.isNested && !this.hasRedirectedOriginalParent) {
|
|
1450
1530
|
componentOnReady(overlay, () => {
|
|
1451
|
-
|
|
1452
|
-
|
|
1531
|
+
// Stale: a newer mount owns this, or React nulled the marker ref.
|
|
1532
|
+
// Leave the latch open so the reveal can try again.
|
|
1453
1533
|
const markerParent = this.markerRef.current?.parentElement ?? null;
|
|
1454
|
-
if (
|
|
1534
|
+
if (this.mountCount !== generation || markerParent === null) {
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
// Reached the JSX parent, so close the latch against later reveals.
|
|
1538
|
+
this.hasRedirectedOriginalParent = true;
|
|
1539
|
+
if (markerParent !== this.portalTarget) {
|
|
1455
1540
|
overlay.cachedOriginalParent = markerParent;
|
|
1456
1541
|
}
|
|
1457
1542
|
});
|
|
@@ -1469,43 +1554,80 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
|
|
|
1469
1554
|
attachProps(node, cProps, prevProps);
|
|
1470
1555
|
}
|
|
1471
1556
|
componentWillUnmount() {
|
|
1472
|
-
this.isUnmounted = true;
|
|
1473
1557
|
const node = this.ref.current;
|
|
1474
1558
|
if (!node) {
|
|
1475
1559
|
return;
|
|
1476
1560
|
}
|
|
1561
|
+
this.nodesAtUnmount = { host: node, wrapper: this.wrapperRef.current };
|
|
1477
1562
|
/**
|
|
1478
|
-
* CoreDelegate (or user code in onWillPresent) can move
|
|
1479
|
-
*
|
|
1480
|
-
*
|
|
1481
|
-
*
|
|
1482
|
-
*
|
|
1483
|
-
* We can't gate this on `isOpen`: the overlay can be moved before it
|
|
1484
|
-
* finishes presenting (e.g. the React 18 StrictMode mount/unmount cycle,
|
|
1485
|
-
* where the present events that flip `isOpen` haven't fired yet), which
|
|
1486
|
-
* would orphan the relocated host in the DOM. It also has to run
|
|
1487
|
-
* synchronously here, since React's portal `removeChild` runs after
|
|
1488
|
-
* `componentWillUnmount` returns and needs the host where it expects it.
|
|
1563
|
+
* CoreDelegate (or user code in onWillPresent) can move a portaled
|
|
1564
|
+
* overlay out of `portalTarget`. React's portal `removeChild` runs as
|
|
1565
|
+
* soon as this returns and needs the host where it left it, so this has
|
|
1566
|
+
* to stay synchronous, before we know a hide from a destroy.
|
|
1489
1567
|
*/
|
|
1490
|
-
if (node.isConnected) {
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1568
|
+
if (node.isConnected && !this.props.isNested && this.portalTarget && node.parentNode !== this.portalTarget) {
|
|
1569
|
+
this.relocatedPortalHost = { node, parent: node.parentNode, nextSibling: node.nextSibling };
|
|
1570
|
+
this.portalTarget.appendChild(node);
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* React also calls this when it only *hides* a subtree - a Suspense
|
|
1574
|
+
* fallback, an Offscreen tree, the StrictMode cycle - and remounts the
|
|
1575
|
+
* same instance on the reveal. Nothing here tells the two apart, so the
|
|
1576
|
+
* teardown waits a microtask and runs only if the marker really left.
|
|
1577
|
+
*/
|
|
1578
|
+
const marker = this.markerRef.current;
|
|
1579
|
+
const mountCount = this.mountCount;
|
|
1580
|
+
queueMicrotask(() => {
|
|
1581
|
+
// Already back (the StrictMode cycle remounts before this runs).
|
|
1582
|
+
if (this.mountCount !== mountCount) {
|
|
1583
|
+
return;
|
|
1501
1584
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1585
|
+
if (marker?.isConnected) {
|
|
1586
|
+
this.watchForDestroyWhileHidden(marker, node, mountCount);
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
this.cleanupAfterUnmount(node);
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
// Undoes the move `componentWillUnmount` made. Skipped if something else
|
|
1593
|
+
// moved the host while hidden, since the pre-hide position is then wrong.
|
|
1594
|
+
restoreRelocatedPortalHost() {
|
|
1595
|
+
const relocated = this.relocatedPortalHost;
|
|
1596
|
+
this.relocatedPortalHost = undefined;
|
|
1597
|
+
if (!relocated || !relocated.parent.isConnected || relocated.node.parentNode !== this.portalTarget) {
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
const { node, parent, nextSibling } = relocated;
|
|
1601
|
+
// The recorded sibling can have moved or been removed while hidden, in
|
|
1602
|
+
// which case appending is the closest we can get.
|
|
1603
|
+
parent.insertBefore(node, nextSibling?.parentNode === parent ? nextSibling : null);
|
|
1604
|
+
}
|
|
1605
|
+
// `cleanupAfterUnmount` decides what to undo. Gating on the state at hide
|
|
1606
|
+
// time would miss an overlay that only starts presenting afterwards.
|
|
1607
|
+
watchForDestroyWhileHidden(marker, node, mountCount) {
|
|
1608
|
+
this.hiddenDestroyWatch = watchForDestroy(marker, () => {
|
|
1609
|
+
this.hiddenDestroyWatch = undefined;
|
|
1610
|
+
// A reveal already happened, so `componentDidMount` owns the teardown.
|
|
1611
|
+
if (this.mountCount !== mountCount) {
|
|
1612
|
+
return;
|
|
1508
1613
|
}
|
|
1614
|
+
this.cleanupAfterUnmount(node);
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
stopWatchingForDestroy() {
|
|
1618
|
+
unwatchForDestroy(this.hiddenDestroyWatch);
|
|
1619
|
+
this.hiddenDestroyWatch = undefined;
|
|
1620
|
+
}
|
|
1621
|
+
cleanupAfterUnmount(node) {
|
|
1622
|
+
this.nodesAtUnmount = null;
|
|
1623
|
+
this.relocatedPortalHost = undefined;
|
|
1624
|
+
/**
|
|
1625
|
+
* Nested overlays render inline inside a `<template>`. React's unmount
|
|
1626
|
+
* won't reach a host that has been moved out of one, so remove it here.
|
|
1627
|
+
* A host still in its template is left for React.
|
|
1628
|
+
*/
|
|
1629
|
+
if (this.props.isNested && node.isConnected && !(node.parentElement instanceof HTMLTemplateElement)) {
|
|
1630
|
+
node.remove();
|
|
1509
1631
|
}
|
|
1510
1632
|
/**
|
|
1511
1633
|
* If the overlay is being unmounted, but is still
|
|
@@ -1573,14 +1695,14 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
|
|
|
1573
1695
|
className: getWrapperClasses(),
|
|
1574
1696
|
}, children)
|
|
1575
1697
|
: null));
|
|
1576
|
-
//
|
|
1577
|
-
//
|
|
1578
|
-
//
|
|
1579
|
-
//
|
|
1698
|
+
// The marker sits at the JSX location on both branches and leaves the
|
|
1699
|
+
// document only on a real unmount, which is how `componentWillUnmount`
|
|
1700
|
+
// tells a hide from a destroy. Portaled overlays also recover their
|
|
1701
|
+
// JSX parent from it.
|
|
1580
1702
|
if (!isNested && this.portalTarget) {
|
|
1581
1703
|
return createElement(React.Fragment, null, createElement('template', { ref: this.markerRef }), createPortal(overlayElement, this.portalTarget));
|
|
1582
1704
|
}
|
|
1583
|
-
return createElement('template', {}, overlayElement);
|
|
1705
|
+
return createElement('template', { ref: this.markerRef }, overlayElement);
|
|
1584
1706
|
}
|
|
1585
1707
|
static get displayName() {
|
|
1586
1708
|
return displayName;
|
|
@@ -1606,18 +1728,20 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
|
|
|
1606
1728
|
this.props.onWillPresent && this.props.onWillPresent(evt);
|
|
1607
1729
|
};
|
|
1608
1730
|
handleDidDismiss = (evt) => {
|
|
1609
|
-
const wrapper = this.wrapperRef.current;
|
|
1610
|
-
const el = this.ref.current;
|
|
1731
|
+
const wrapper = this.wrapperRef.current ?? this.nodesAtUnmount?.wrapper ?? null;
|
|
1732
|
+
const el = this.ref.current ?? this.nodesAtUnmount?.host ?? null;
|
|
1611
1733
|
/**
|
|
1612
|
-
*
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
1615
|
-
*
|
|
1734
|
+
* React's `removeChild` needs the wrapper as a direct child of the host,
|
|
1735
|
+
* and a scoped overlay's slot relocation can nest it deeper. So this runs
|
|
1736
|
+
* before the flip below, which triggers that removal. Appending one that
|
|
1737
|
+
* is already a direct child would reorder it against core's own nodes.
|
|
1616
1738
|
*/
|
|
1617
|
-
if (wrapper && el) {
|
|
1739
|
+
if (wrapper && el && wrapper.parentElement !== el) {
|
|
1618
1740
|
el.append(wrapper);
|
|
1619
|
-
this.setState({ isOpen: false });
|
|
1620
1741
|
}
|
|
1742
|
+
// Flip either way, or a dismiss landing while hidden leaves the contents
|
|
1743
|
+
// mounted against a closed overlay on reveal. A no-op after a real unmount.
|
|
1744
|
+
this.setState({ isOpen: false });
|
|
1621
1745
|
this.props.onDidDismiss && this.props.onDidDismiss(evt);
|
|
1622
1746
|
};
|
|
1623
1747
|
};
|