@@ -100,6 +100,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
100100 private parentRemovalObserver ?: MutationObserver ;
101101 // Cached original parent from before modal is moved to body during presentation
102102 private cachedOriginalParent ?: HTMLElement ;
103+ // Elements that had pointer-events disabled for background interaction
104+ private pointerEventsDisabledElements : HTMLElement [ ] = [ ] ;
103105
104106 lastFocus ?: HTMLElement ;
105107 animation ?: Animation ;
@@ -763,16 +765,53 @@ export class Modal implements ComponentInterface, OverlayInterface {
763765
764766 /**
765767 * When showBackdrop or focusTrap is false, the modal's original parent may
766- * block pointer events after the modal is moved to ion-app. Disable
767- * pointer-events on the parent elements to allow background interaction.
768+ * block pointer events after the modal is moved to ion-app. This only applies
769+ * when the modal is in a child route (detected by the modal being inside
770+ * a route wrapper like ion-page). Disable pointer-events on the child
771+ * route's wrapper elements up to (and including) the first ion-router-outlet.
772+ * We stop there because parent elements may contain sibling content that
773+ * should remain interactive.
768774 * See https://github.com/ionic-team/ionic-framework/issues/30700
769775 */
770776 if ( ( this . showBackdrop === false || this . focusTrap === false ) && this . cachedOriginalParent ) {
771- this . cachedOriginalParent . style . setProperty ( 'pointer-events' , 'none' ) ;
777+ // Find the first meaningful parent (skip template and other non-semantic wrappers).
778+ // In Ionic React, modals are wrapped in a <template> element.
779+ let semanticParent : HTMLElement | null = this . cachedOriginalParent ;
780+ while ( semanticParent && ( semanticParent . tagName === 'TEMPLATE' || semanticParent . tagName === 'SLOT' ) ) {
781+ semanticParent = semanticParent . parentElement ;
782+ }
783+
784+ // Check if the modal is inside a route wrapper (ion-page or div.ion-page)
785+ // If the modal is inside ion-content or other content containers, this fix doesn't apply
786+ const parentIsRouteWrapper =
787+ semanticParent &&
788+ ( semanticParent . tagName === 'ION-PAGE' || semanticParent . classList . contains ( 'ion-page' ) ) ;
789+
790+ if ( parentIsRouteWrapper && semanticParent ) {
791+ this . pointerEventsDisabledElements = [ ] ;
792+ let current : HTMLElement | null = semanticParent ;
793+
794+ while ( current && current . tagName !== 'ION-APP' ) {
795+ const tagName = current . tagName ;
796+ // Check for ion-page tag or elements with ion-page class
797+ // (React renders IonPage as div.ion-page, not ion-page tag)
798+ const isIonPage = tagName === 'ION-PAGE' || current . classList . contains ( 'ion-page' ) ;
799+ const isRouterOutlet = tagName === 'ION-ROUTER-OUTLET' ;
800+ const isNav = tagName === 'ION-NAV' ;
801+
802+ if ( isIonPage || isRouterOutlet || isNav ) {
803+ current . style . setProperty ( 'pointer-events' , 'none' ) ;
804+ this . pointerEventsDisabledElements . push ( current ) ;
805+ }
772806
773- const immediateParent = this . cachedOriginalParent . parentElement ;
774- if ( immediateParent ?. tagName === 'ION-ROUTER-OUTLET' ) {
775- immediateParent . style . setProperty ( 'pointer-events' , 'none' ) ;
807+ // Stop after processing the first ion-router-outlet - parent elements
808+ // may contain sibling content (like buttons) that should remain interactive
809+ if ( isRouterOutlet ) {
810+ break ;
811+ }
812+
813+ current = current . parentElement ;
814+ }
776815 }
777816 }
778817 }
@@ -888,13 +927,10 @@ export class Modal implements ComponentInterface, OverlayInterface {
888927 /**
889928 * Clean up pointer-events changes made in initSheetGesture.
890929 */
891- if ( this . cachedOriginalParent ) {
892- this . cachedOriginalParent . style . removeProperty ( 'pointer-events' ) ;
893- const immediateParent = this . cachedOriginalParent . parentElement ;
894- if ( immediateParent ?. tagName === 'ION-ROUTER-OUTLET' ) {
895- immediateParent . style . removeProperty ( 'pointer-events' ) ;
896- }
930+ for ( const element of this . pointerEventsDisabledElements ) {
931+ element . style . removeProperty ( 'pointer-events' ) ;
897932 }
933+ this . pointerEventsDisabledElements = [ ] ;
898934 }
899935 this . currentBreakpoint = undefined ;
900936 this . animation = undefined ;
0 commit comments