@ionic/react 8.8.8-nightly.20260519 → 8.8.8

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
@@ -839,6 +839,14 @@ const detachEvent = (node, eventName) => {
839
839
  }
840
840
  };
841
841
 
842
+ /**
843
+ * Set to `true` when rendering inside another inline overlay. Nested
844
+ * overlays render at their JSX position (no portal) so that core's
845
+ * `el.closest('ion-popover')`-style nesting detection keeps working,
846
+ * and the outer overlay's portal already gives the subtree the correct
847
+ * React event-delegation root.
848
+ */
849
+ const NestedOverlayContext = React.createContext(false);
842
850
  const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateHost) => {
843
851
  if (defineCustomElement) {
844
852
  defineCustomElement();
@@ -847,6 +855,7 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
847
855
  const ReactComponent = class extends React.Component {
848
856
  constructor(props) {
849
857
  super(props);
858
+ this.isUnmounted = false;
850
859
  this.handleIonMount = () => {
851
860
  /**
852
861
  * Mount the inner component when the
@@ -890,12 +899,45 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
890
899
  this.state = { isOpen: false };
891
900
  // Create a local ref to the inner child element.
892
901
  this.wrapperRef = React.createRef();
902
+ // Marker stays at the JSX location so we can recover the immediate
903
+ // JSX parent after the overlay has been portaled to ion-app.
904
+ this.markerRef = React.createRef();
905
+ /**
906
+ * Resolve the portal target to the same container CoreDelegate
907
+ * teleports overlays into. Portaling here keeps the overlay inside
908
+ * React's tree so React's synthetic events still dispatch to its
909
+ * children, even after CoreDelegate moves the DOM node out of the
910
+ * declared JSX parent.
911
+ */
912
+ this.portalTarget = typeof document !== 'undefined' ? document.querySelector('ion-app') || document.body : null;
893
913
  }
894
914
  componentDidMount() {
915
+ // Reset for React 18 StrictMode: the dev-mode unmount/remount cycle
916
+ // re-uses this instance and leaves the flag set from the prior
917
+ // componentWillUnmount.
918
+ this.isUnmounted = false;
895
919
  this.componentDidUpdate(this.props);
896
920
  this.ref.current?.addEventListener('ionMount', this.handleIonMount);
897
921
  this.ref.current?.addEventListener('willPresent', this.handleWillPresent);
898
922
  this.ref.current?.addEventListener('didDismiss', this.handleDidDismiss);
923
+ /**
924
+ * The overlay is portaled to `portalTarget`, so Stencil caches that
925
+ * container as `cachedOriginalParent`. Modal features (sheet
926
+ * child-route passthrough, parent-removal auto-dismiss) walk up
927
+ * from `cachedOriginalParent` to find the enclosing `.ion-page`,
928
+ * so we redirect it at the marker's JSX parent.
929
+ */
930
+ const overlay = this.ref.current;
931
+ if (overlay) {
932
+ componentOnReady(overlay, () => {
933
+ if (this.isUnmounted)
934
+ return;
935
+ const markerParent = this.markerRef.current?.parentElement ?? null;
936
+ if (markerParent && markerParent !== this.portalTarget) {
937
+ overlay.cachedOriginalParent = markerParent;
938
+ }
939
+ });
940
+ }
899
941
  }
900
942
  componentDidUpdate(prevProps) {
901
943
  const node = this.ref.current;
@@ -905,10 +947,11 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
905
947
  * so they don't get attached twice and called twice.
906
948
  */
907
949
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
908
- const { onDidDismiss, onWillPresent, ...cProps } = this.props;
950
+ const { onDidDismiss, onWillPresent, isNested, ...cProps } = this.props;
909
951
  attachProps(node, cProps, prevProps);
910
952
  }
911
953
  componentWillUnmount() {
954
+ this.isUnmounted = true;
912
955
  const node = this.ref.current;
913
956
  /**
914
957
  * If the overlay is being unmounted, but is still
@@ -932,13 +975,28 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
932
975
  * avoid memory leaks.
933
976
  */
934
977
  node.removeEventListener('didDismiss', this.handleDidDismiss);
935
- node.remove();
978
+ if (this.props.isNested) {
979
+ /**
980
+ * Nested overlays render inline (no portal). CoreDelegate may
981
+ * have moved the node out of its React parent, so React's
982
+ * unmount won't reach it. Remove it directly.
983
+ */
984
+ node.remove();
985
+ }
986
+ else if (node.isConnected && this.portalTarget && node.parentNode !== this.portalTarget) {
987
+ /**
988
+ * Portaled path: move the overlay back into `portalTarget` so
989
+ * React's portal removeChild can find it. CoreDelegate (or user
990
+ * code in onWillPresent) may have moved it elsewhere while open.
991
+ */
992
+ this.portalTarget.appendChild(node);
993
+ }
936
994
  detachProps(node, this.props);
937
995
  }
938
996
  }
939
997
  render() {
940
998
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
941
- const { children, forwardedRef, style, className, ref, ...cProps } = this.props;
999
+ const { children, forwardedRef, style, className, ref, isNested, ...cProps } = this.props;
942
1000
  const propsToPass = Object.keys(cProps).reduce((acc, name) => {
943
1001
  if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) {
944
1002
  const eventName = name.substring(2).toLowerCase();
@@ -966,12 +1024,12 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
966
1024
  }
967
1025
  return DELEGATE_HOST;
968
1026
  };
969
- return createElement('template', {}, createElement(tagName, newProps,
1027
+ const overlayElement = createElement(tagName, newProps,
1028
+ // Children, not the overlay host, observe `isNested = true`.
1029
+ createElement(NestedOverlayContext.Provider, { value: true },
970
1030
  /**
971
- * We only want the inner component
972
- * to be mounted if the overlay is open,
973
- * so conditionally render the component
974
- * based on the isOpen state.
1031
+ * We only want the inner component to be mounted if the overlay
1032
+ * is open, so conditionally render based on `isOpen` state.
975
1033
  */
976
1034
  this.state.isOpen || this.props.keepContentsMounted
977
1035
  ? createElement('div', {
@@ -979,12 +1037,23 @@ const createInlineOverlayComponent = (tagName, defineCustomElement, hasDelegateH
979
1037
  className: getWrapperClasses(),
980
1038
  }, children)
981
1039
  : null));
1040
+ // Top-level overlays portal into `portalTarget` with a marker
1041
+ // `<template>` at the JSX location to recover the immediate JSX
1042
+ // parent after CoreDelegate teleports. Nested overlays and SSR
1043
+ // fall back to a `<template>` wrapper.
1044
+ if (!isNested && this.portalTarget) {
1045
+ return createElement(React.Fragment, null, createElement('template', { ref: this.markerRef }), createPortal(overlayElement, this.portalTarget));
1046
+ }
1047
+ return createElement('template', {}, overlayElement);
982
1048
  }
983
1049
  static get displayName() {
984
1050
  return displayName;
985
1051
  }
986
1052
  };
987
- return createForwardRef(ReactComponent, displayName);
1053
+ // Forward the nesting context as a prop to avoid contextType on the class.
1054
+ const ReactComponentWithNesting = (props) => createElement(NestedOverlayContext.Consumer, null, (isNested) => createElement(ReactComponent, { ...props, isNested }));
1055
+ ReactComponentWithNesting.displayName = displayName;
1056
+ return createForwardRef(ReactComponentWithNesting, displayName);
988
1057
  };
989
1058
  const DELEGATE_HOST = 'ion-delegate-host';
990
1059