@ionic/react 9.0.1-dev.11788212611.154b1596 → 9.0.1-dev.11788375537.1f38f077

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
@@ -1395,72 +1395,6 @@ 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
- };
1464
1398
  const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateHost) => {
1465
1399
  if (defineCustomElement) {
1466
1400
  defineCustomElement();
@@ -1472,18 +1406,7 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
1472
1406
  markerRef;
1473
1407
  stableMergedRefs;
1474
1408
  portalTarget;
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;
1409
+ isUnmounted = false;
1487
1410
  constructor(props) {
1488
1411
  super(props);
1489
1412
  // Create a local ref to to attach props to the wrapped element.
@@ -1494,6 +1417,8 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
1494
1417
  this.state = { isOpen: false };
1495
1418
  // Create a local ref to the inner child element.
1496
1419
  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.
1497
1422
  this.markerRef = React.createRef();
1498
1423
  /**
1499
1424
  * Resolve the portal target to the same container CoreDelegate
@@ -1505,11 +1430,10 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
1505
1430
  this.portalTarget = typeof document !== 'undefined' ? document.querySelector('ion-app') || document.body : null;
1506
1431
  }
1507
1432
  componentDidMount() {
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();
1433
+ // Reset for React 18 StrictMode: the dev-mode unmount/remount cycle
1434
+ // re-uses this instance and leaves the flag set from the prior
1435
+ // componentWillUnmount.
1436
+ this.isUnmounted = false;
1513
1437
  this.componentDidUpdate(this.props);
1514
1438
  this.ref.current?.addEventListener('ionMount', this.handleIonMount);
1515
1439
  this.ref.current?.addEventListener('willPresent', this.handleWillPresent);
@@ -1520,23 +1444,14 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
1520
1444
  * child-route passthrough, parent-removal auto-dismiss) walk up
1521
1445
  * from `cachedOriginalParent` to find the enclosing `.ion-page`,
1522
1446
  * 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`.
1526
1447
  */
1527
1448
  const overlay = this.ref.current;
1528
- const generation = this.mountCount;
1529
- if (overlay && !this.props.isNested && !this.hasRedirectedOriginalParent) {
1449
+ if (overlay) {
1530
1450
  componentOnReady(overlay, () => {
1531
- // Stale: a newer mount owns this, or React nulled the marker ref.
1532
- // Leave the latch open so the reveal can try again.
1533
- const markerParent = this.markerRef.current?.parentElement ?? null;
1534
- if (this.mountCount !== generation || markerParent === null) {
1451
+ if (this.isUnmounted)
1535
1452
  return;
1536
- }
1537
- // Reached the JSX parent, so close the latch against later reveals.
1538
- this.hasRedirectedOriginalParent = true;
1539
- if (markerParent !== this.portalTarget) {
1453
+ const markerParent = this.markerRef.current?.parentElement ?? null;
1454
+ if (markerParent && markerParent !== this.portalTarget) {
1540
1455
  overlay.cachedOriginalParent = markerParent;
1541
1456
  }
1542
1457
  });
@@ -1554,80 +1469,43 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
1554
1469
  attachProps(node, cProps, prevProps);
1555
1470
  }
1556
1471
  componentWillUnmount() {
1472
+ this.isUnmounted = true;
1557
1473
  const node = this.ref.current;
1558
1474
  if (!node) {
1559
1475
  return;
1560
1476
  }
1561
- this.nodesAtUnmount = { host: node, wrapper: this.wrapperRef.current };
1562
- /**
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.
1567
- */
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
1477
  /**
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.
1478
+ * CoreDelegate (or user code in onWillPresent) can move the overlay out
1479
+ * of where React rendered it. React's unmount only removes the node from
1480
+ * its original React location, so we recover a relocated host here,
1481
+ * regardless of open state.
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.
1577
1489
  */
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;
1584
- }
1585
- if (marker?.isConnected) {
1586
- this.watchForDestroyWhileHidden(marker, node, mountCount);
1587
- return;
1490
+ if (node.isConnected) {
1491
+ if (this.props.isNested) {
1492
+ /**
1493
+ * Nested overlays render inline inside a `<template>`. If the host
1494
+ * has been moved out of that template, React's unmount won't reach
1495
+ * it, so remove it directly. A host still in its template is left
1496
+ * for React to remove.
1497
+ */
1498
+ if (!(node.parentElement instanceof HTMLTemplateElement)) {
1499
+ node.remove();
1500
+ }
1588
1501
  }
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;
1502
+ else if (this.portalTarget && node.parentNode !== this.portalTarget) {
1503
+ /**
1504
+ * Portaled overlays: move the host back into `portalTarget` so
1505
+ * React's portal `removeChild` can find it.
1506
+ */
1507
+ this.portalTarget.appendChild(node);
1613
1508
  }
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();
1631
1509
  }
1632
1510
  /**
1633
1511
  * If the overlay is being unmounted, but is still
@@ -1695,14 +1573,14 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
1695
1573
  className: getWrapperClasses(),
1696
1574
  }, children)
1697
1575
  : null));
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.
1576
+ // Top-level overlays portal into `portalTarget` with a marker
1577
+ // `<template>` at the JSX location to recover the immediate JSX
1578
+ // parent after CoreDelegate teleports. Nested overlays and SSR
1579
+ // fall back to a `<template>` wrapper.
1702
1580
  if (!isNested && this.portalTarget) {
1703
1581
  return createElement(React.Fragment, null, createElement('template', { ref: this.markerRef }), createPortal(overlayElement, this.portalTarget));
1704
1582
  }
1705
- return createElement('template', { ref: this.markerRef }, overlayElement);
1583
+ return createElement('template', {}, overlayElement);
1706
1584
  }
1707
1585
  static get displayName() {
1708
1586
  return displayName;
@@ -1728,20 +1606,18 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
1728
1606
  this.props.onWillPresent && this.props.onWillPresent(evt);
1729
1607
  };
1730
1608
  handleDidDismiss = (evt) => {
1731
- const wrapper = this.wrapperRef.current ?? this.nodesAtUnmount?.wrapper ?? null;
1732
- const el = this.ref.current ?? this.nodesAtUnmount?.host ?? null;
1609
+ const wrapper = this.wrapperRef.current;
1610
+ const el = this.ref.current;
1733
1611
  /**
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.
1612
+ * This component might be unmounted already, if the containing
1613
+ * element was removed while the overlay was still open. (For
1614
+ * example, if an item contains an inline overlay with a button
1615
+ * that removes the item.)
1738
1616
  */
1739
- if (wrapper && el && wrapper.parentElement !== el) {
1617
+ if (wrapper && el) {
1740
1618
  el.append(wrapper);
1619
+ this.setState({ isOpen: false });
1741
1620
  }
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 });
1745
1621
  this.props.onDidDismiss && this.props.onDidDismiss(evt);
1746
1622
  };
1747
1623
  };