From 59f2dbfec99c637e65f60606db7fd9229280a394 Mon Sep 17 00:00:00 2001 From: ShaneK Date: Fri, 21 Nov 2025 13:23:07 -0800 Subject: [PATCH] fix(react): automatically dismiss inline overlays on navigation --- .../createInlineOverlayComponent.tsx | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/packages/react/src/components/createInlineOverlayComponent.tsx b/packages/react/src/components/createInlineOverlayComponent.tsx index 65f1711cbb..f359e136b2 100644 --- a/packages/react/src/components/createInlineOverlayComponent.tsx +++ b/packages/react/src/components/createInlineOverlayComponent.tsx @@ -40,6 +40,7 @@ export const createInlineOverlayComponent = ( ref: React.RefObject; wrapperRef: React.RefObject; stableMergedRefs: React.RefCallback; + mutationObserver: MutationObserver | null = null; constructor(props: IonicReactInternalProps) { super(props); @@ -59,6 +60,51 @@ export const createInlineOverlayComponent = ( this.ref.current?.addEventListener('ionMount', this.handleIonMount); this.ref.current?.addEventListener('willPresent', this.handleWillPresent); this.ref.current?.addEventListener('didDismiss', this.handleDidDismiss); + + /** + * Watch for when ancestor pages become hidden (ion-page-hidden class). + * This handles React Router 6 where pages stay mounted on forward PUSH. + * When a page is hidden, we dismiss any open overlays within it. + */ + this.setupPageVisibilityObserver(); + } + + setupPageVisibilityObserver() { + /** + * Watch for when ANY element in the document gets the ion-page-hidden class. + * We use a subtree observer on a parent container because: + * 1. The overlay's component might not have an IonPage wrapper + * 2. Pages might be added dynamically after this component mounts + * 3. We want to dismiss overlays when ANY navigation occurs + * + * This handles React Router 6 where pages stay mounted but get hidden. + */ + this.mutationObserver = new MutationObserver((mutations) => { + if (!this.state.isOpen) return; + + for (const mutation of mutations) { + if (mutation.type === 'attributes' && mutation.attributeName === 'class') { + const target = mutation.target as HTMLElement; + // If any element gets the ion-page-hidden or ion-page-invisible class, dismiss overlay + if ( + target.classList.contains('ion-page-hidden') || + target.classList.contains('ion-page-invisible') + ) { + this.dismissOverlay(); + return; + } + } + } + }); + + // Observe a parent container with subtree: true to catch all descendant class changes + // This works even if pages are added after mount or if the current route has no IonPage + const appRoot = document.querySelector('#root') || document.body; + this.mutationObserver.observe(appRoot, { + attributes: true, + attributeFilter: ['class'], + subtree: true, // Watch all descendants + }); } componentDidUpdate(prevProps: IonicReactInternalProps) { @@ -100,6 +146,25 @@ export const createInlineOverlayComponent = ( node.remove(); detachProps(node, this.props); } + + // Clean up mutation observer + if (this.mutationObserver) { + this.mutationObserver.disconnect(); + this.mutationObserver = null; + } + } + + dismissOverlay() { + const node = this.ref.current; + if (node && this.state.isOpen) { + /** + * Dismiss the overlay without animation when the page is hidden. + * This matches the behavior in componentWillUnmount. + */ + node.removeEventListener('didDismiss', this.handleDidDismiss); + node.remove(); + detachProps(node, this.props); + } } render() {