From 96ee3ce44e29c1f596950f0391543640e0e4b323 Mon Sep 17 00:00:00 2001 From: Patrick Kenny Date: Fri, 21 Aug 2026 01:15:11 +0900 Subject: [PATCH 1/3] fix(react): re-attach nested inline overlay after a Suspense hide --- .../createInlineOverlayComponent.spec.tsx | 66 +++++++++++++++++++ .../createInlineOverlayComponent.tsx | 29 +++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/__tests__/createInlineOverlayComponent.spec.tsx b/packages/react/src/components/__tests__/createInlineOverlayComponent.spec.tsx index e2cb8176af8..6e256d2d20f 100644 --- a/packages/react/src/components/__tests__/createInlineOverlayComponent.spec.tsx +++ b/packages/react/src/components/__tests__/createInlineOverlayComponent.spec.tsx @@ -131,6 +131,72 @@ describe('createInlineOverlayComponent: unmount cleanup', () => { expect(document.querySelector('ion-popover')).toBeNull(); }); + it('restores a relocated nested overlay when a Suspense boundary hides and reveals it', async () => { + /** + * React runs `componentWillUnmount` when it *hides* a subtree as well as + * when it destroys one: a Suspense boundary falling back after mount runs + * it, then runs `componentDidMount` again on the same instance when the + * boundary reveals its content. A host removed while hidden has to come + * back, since React only re-inserts nodes it removed itself. Otherwise an + * overlay that was mid-`present()` is gone for good, with no dismiss + * lifecycle ever firing. + */ + let resolveSuspender!: () => void; + let hasResolved = false; + const suspenderPromise = new Promise((resolve) => { + resolveSuspender = () => { + hasResolved = true; + resolve(); + }; + }); + + const Suspender = () => { + if (!hasResolved) { + throw suspenderPromise; + } + return null; + }; + + let suspend!: () => void; + const Boundary = () => { + const [isSuspended, setIsSuspended] = React.useState(false); + suspend = () => setIsSuspended(true); + + return ( + loading}> + + + + {isSuspended ? : null} + + ); + }; + + render(); + + const popover = document.body.querySelector('ion-popover') as HTMLElement; + + // CoreDelegate teleports the host out of its `