Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 13.3k
fix(react): re-attach nested inline overlay after a Suspense hide#31390
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base:main
Are you sure you want to change the base?
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -57,6 +57,9 @@ export const createInlineOverlayComponent = <PropType, ElementType>( | ||
| stableMergedRefs: React.RefCallback<HTMLElement>; | ||
| portalTarget: HTMLElement | null; | ||
| isUnmounted = false; | ||
| // A nested host removed in `componentWillUnmount`, with the comment left in | ||
| // its place, so `componentDidMount` can put it back where it was. | ||
| removedHost: { node: HTMLElement; anchor: Comment } | null = null; | ||
| constructor(props: InternalProps) { | ||
| super(props); | ||
| @@ -87,6 +90,21 @@ export const createInlineOverlayComponent = <PropType, ElementType>( | ||
| // componentWillUnmount. | ||
| this.isUnmounted = false; | ||
| // React runs `componentWillUnmount` when it only hides a subtree and | ||
| // mounts the same instance again on the reveal, so a host removed there | ||
| // goes back at the position it came from - document order decides which | ||
| // overlay is on top. The spec covers the flow. | ||
| const { removedHost } = this; | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The So the overlay comes back, but an app that focuses an input or fires analytics off ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, taking a closer look at this, it seems difficult to fix without a (much?) larger refactor. Analyzing this with Opus:
Is this on the right track? I worry that this is turning into a lot of code for what seems like it should be a more straightforward fix. | ||
| this.removedHost = null; | ||
| if (removedHost) { | ||
| const { node, anchor } = removedHost; | ||
| if (!node.isConnected && anchor.isConnected) { | ||
| anchor.replaceWith(node); | ||
| } else { | ||
| anchor.remove(); | ||
| } | ||
| } | ||
| this.componentDidUpdate(this.props); | ||
| this.ref.current?.addEventListener('ionMount', this.handleIonMount); | ||
| @@ -149,9 +167,14 @@ export const createInlineOverlayComponent = <PropType, ElementType>( | ||
| * Nested overlays render inline inside a `<template>`. If the host | ||
| * has been moved out of that template, React's unmount won't reach | ||
| * it, so remove it directly. A host still in its template is left | ||
| * for React to remove. | ||
| * for React to remove. A comment marks the spot, the way CoreDelegate | ||
| * marks a teleport, so a reveal can put the host back where it was. | ||
| */ | ||
| if (!(node.parentElement instanceof HTMLTemplateElement)) { | ||
| const parent = node.parentElement; | ||
| if (parent && !(parent instanceof HTMLTemplateElement)) { | ||
| const anchor = document.createComment(RESTORE_ANCHOR); | ||
| parent.insertBefore(anchor, node); | ||
| this.removedHost = { node, anchor }; | ||
| node.remove(); | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Calling Present a modal, remove it, then put it back: The flow in the linked issue doesn't reach this, since the removal happens before ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm having trouble fixing this. Opus analysis suggests this can't be fixed in React:
I'm having trouble grokking the code so I have to defer to you on this. | ||
| } | ||
| } else if (this.portalTarget && node.parentNode !== this.portalTarget) { | ||
| @@ -318,3 +341,9 @@ export const createInlineOverlayComponent = <PropType, ElementType>( | ||
| }; | ||
| const DELEGATE_HOST = 'ion-delegate-host'; | ||
| /** | ||
| * Marks where a nested overlay host was removed from, so it can be restored to | ||
| * the same position if React was only hiding the subtree. | ||
| */ | ||
| const RESTORE_ANCHOR = 'ionic hidden overlay'; | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These prove the node came back. They don't show that the overlay presents, or that it can still be dismissed, and since the removal fires the overlay's
disconnectedCallback, a reconnected node isn't the same as a healthy overlay. I did check the test fails against the base implementation, so it's a genuine regression test. Nothing here covers stacking order or the dismiss path after a reveal though.Using
keepContentsMountedalso means the contents are already mounted, so the suspension comes from a sibling rather than the overlay's own first render, which is the trigger the issue describes. There's a home for a browser level version next to IonPopoverNested andModalTeleport.tsx, and the react19 app supportsuse(). Failing that, two nested overlays here would at least pin the ordering. A nested-branch StrictMode test would be good too, since the existing one only covers the portaled branch.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I added several more tests; let me know if this is enough coverage.