Skip to content

Commit 34d3993

Browse files
committed
fix(modal): allow interaction with parent content when sheet modal has showBackdrop=false in child routes
1 parent ea8a22d commit 34d3993

File tree

5 files changed

+60
-71
lines changed

5 files changed

+60
-71
lines changed

core/src/components/modal/modal.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,20 @@ ion-backdrop {
5757
pointer-events: auto;
5858
}
5959

60+
/**
61+
* When focus trap is disabled on a visible modal, allow clicks to pass through
62+
* to content behind it. The .modal-wrapper retains pointer-events: auto so
63+
* modal content remains interactive. We only apply this when show-modal is
64+
* present to avoid affecting hidden modals that haven't presented.
65+
*/
66+
:host(.ion-disable-focus-trap.show-modal) {
67+
pointer-events: none;
68+
}
69+
70+
:host(.ion-disable-focus-trap.show-modal) ion-backdrop {
71+
pointer-events: none;
72+
}
73+
6074
:host(.overlay-hidden) {
6175
display: none;
6276
}

core/src/components/modal/modal.tsx

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,14 @@ export class Modal implements ComponentInterface, OverlayInterface {
644644
window.addEventListener(KEYBOARD_DID_OPEN, this.keyboardOpenCallback);
645645
}
646646

647-
if (this.isSheetModal) {
647+
/**
648+
* Recalculate isSheetModal here because framework bindings (e.g., Angular)
649+
* may not have been applied when componentWillLoad ran.
650+
*/
651+
const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
652+
this.isSheetModal = isSheetModal;
653+
654+
if (isSheetModal) {
648655
this.initSheetGesture();
649656
} else if (hasCardModal) {
650657
this.initSwipeToClose();
@@ -753,6 +760,21 @@ export class Modal implements ComponentInterface, OverlayInterface {
753760
this.moveSheetToBreakpoint = moveSheetToBreakpoint;
754761

755762
this.gesture.enable(true);
763+
764+
/**
765+
* 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+
* See https://github.com/ionic-team/ionic-framework/issues/30700
769+
*/
770+
if ((this.showBackdrop === false || this.focusTrap === false) && this.cachedOriginalParent) {
771+
this.cachedOriginalParent.style.setProperty('pointer-events', 'none');
772+
773+
const immediateParent = this.cachedOriginalParent.parentElement;
774+
if (immediateParent?.tagName === 'ION-ROUTER-OUTLET') {
775+
immediateParent.style.setProperty('pointer-events', 'none');
776+
}
777+
}
756778
}
757779

758780
private sheetOnDismiss() {
@@ -862,6 +884,17 @@ export class Modal implements ComponentInterface, OverlayInterface {
862884
}
863885
this.cleanupViewTransitionListener();
864886
this.cleanupParentRemovalObserver();
887+
888+
/**
889+
* Clean up pointer-events changes made in initSheetGesture.
890+
*/
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+
}
897+
}
865898
}
866899
this.currentBreakpoint = undefined;
867900
this.animation = undefined;

packages/angular/test/base/e2e/src/standalone/modal-child-route.spec.ts

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,37 @@
11
import { expect, test } from '@playwright/test';
22

33
/**
4-
* Tests for INLINE sheet modals in child routes with showBackdrop=false.
5-
*
6-
* Related issue: https://github.com/ionic-team/ionic-framework/issues/30700
7-
*
8-
* This test mimics the EXACT structure from the issue reproduction:
9-
* - PARENT component has interactive content (buttons) AND a nested ion-router-outlet
10-
* - CHILD route (rendered in that nested outlet) contains ONLY the modal
11-
* - The modal has showBackdrop=false
12-
*
13-
* The bug: when the modal opens in the child route, the buttons in the PARENT
14-
* become non-interactive even though showBackdrop=false should allow interaction.
15-
*
16-
* DOM structure:
17-
* - ion-app > ion-router-outlet (root) > PARENT (buttons + nested outlet) > ion-router-outlet > CHILD (modal only)
4+
* Tests for sheet modals in child routes with showBackdrop=false.
5+
* Parent has buttons + nested outlet; child route contains only the modal.
6+
* See https://github.com/ionic-team/ionic-framework/issues/30700
187
*/
198
test.describe('Modals: Inline Sheet in Child Route (standalone)', () => {
209
test.beforeEach(async ({ page }) => {
21-
// Navigate to the child route - this will:
22-
// 1. Render the parent with buttons
23-
// 2. Render the child in the nested outlet, which immediately opens the modal
2410
await page.goto('/standalone/modal-child-route/child');
2511
});
2612

2713
test('should render parent content and child modal', async ({ page }) => {
28-
// Verify the PARENT content is visible (buttons are in parent, not child)
2914
await expect(page.locator('#increment-btn')).toBeVisible();
3015
await expect(page.locator('#decrement-btn')).toBeVisible();
3116
await expect(page.locator('#background-action-count')).toHaveText('0');
32-
33-
// Verify the modal from CHILD route is visible (opens immediately with isOpen=true)
3417
await expect(page.locator('ion-modal.show-modal')).toBeVisible();
3518
await expect(page.locator('#modal-content-loaded')).toBeVisible();
3619
});
3720

38-
test('should allow interacting with PARENT content while modal (showBackdrop: false) is open in CHILD route', async ({ page }) => {
39-
// Modal should already be open (isOpen=true in child component)
21+
test('should allow interacting with parent content while modal is open in child route', async ({ page }) => {
4022
await expect(page.locator('ion-modal.show-modal')).toBeVisible();
41-
await expect(page.locator('#modal-content-loaded')).toBeVisible();
4223

43-
// Click the increment button in the PARENT - this should work with showBackdrop=false
44-
// This is the KEY test - it FAILS due to issue #30700
4524
await page.locator('#increment-btn').click();
46-
47-
// Verify the click was registered
4825
await expect(page.locator('#background-action-count')).toHaveText('1');
4926
});
5027

51-
test('should allow multiple interactions with PARENT content while modal is open', async ({ page }) => {
52-
// Modal should already be open
28+
test('should allow multiple interactions with parent content while modal is open', async ({ page }) => {
5329
await expect(page.locator('ion-modal.show-modal')).toBeVisible();
5430

55-
// Click increment multiple times
5631
await page.locator('#increment-btn').click();
5732
await page.locator('#increment-btn').click();
5833
await expect(page.locator('#background-action-count')).toHaveText('2');
5934

60-
// Click decrement
6135
await page.locator('#decrement-btn').click();
6236
await expect(page.locator('#background-action-count')).toHaveText('1');
6337
});
Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,14 @@
11
import { CommonModule } from '@angular/common';
22
import { Component } from '@angular/core';
3-
import { IonButton, IonContent, IonHeader, IonModal, IonTitle, IonToolbar } from '@ionic/angular/standalone';
3+
import { IonContent, IonHeader, IonModal, IonTitle, IonToolbar } from '@ionic/angular/standalone';
44

55
/**
6-
* Child component that ONLY contains the modal.
7-
*
8-
* This mimics the EXACT structure from issue #30700 reproduction where:
9-
* - The PARENT page has the interactive content (buttons)
10-
* - The CHILD route (this component) contains ONLY the modal
11-
*
12-
* The structure is:
13-
* - ion-app > ion-router-outlet (root) > PARENT (has buttons + nested outlet) > ion-router-outlet (nested) > THIS COMPONENT (has modal)
14-
*
15-
* The bug is: when this modal opens, the buttons in the PARENT become non-interactive
16-
* even though showBackdrop=false should allow background interaction.
17-
*
18-
* Related issue: https://github.com/ionic-team/ionic-framework/issues/30700
6+
* Child route component containing only the sheet modal with showBackdrop=false.
7+
* Verifies issue https://github.com/ionic-team/ionic-framework/issues/30700
198
*/
209
@Component({
2110
selector: 'app-modal-child-route-child',
2211
template: `
23-
<!-- This child route component contains ONLY the modal, which opens immediately -->
24-
<!-- The interactive content (buttons) are in the PARENT component -->
25-
26-
<!-- INLINE modal with showBackdrop=false - parent content should be interactive -->
2712
<ion-modal
2813
[isOpen]="true"
2914
[breakpoints]="[0.2, 0.5, 0.7]"
@@ -38,12 +23,11 @@ import { IonButton, IonContent, IonHeader, IonModal, IonTitle, IonToolbar } from
3823
</ion-header>
3924
<ion-content class="ion-padding">
4025
<p id="modal-content-loaded">Modal content loaded in child route</p>
41-
<p>The +/- buttons in the parent should be clickable!</p>
4226
</ion-content>
4327
</ng-template>
4428
</ion-modal>
4529
`,
4630
standalone: true,
47-
imports: [CommonModule, IonButton, IonContent, IonHeader, IonModal, IonTitle, IonToolbar],
31+
imports: [CommonModule, IonContent, IonHeader, IonModal, IonTitle, IonToolbar],
4832
})
4933
export class ModalChildRouteChildComponent {}

packages/angular/test/base/src/app/standalone/modal-child-route/modal-child-route-parent.component.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,8 @@ import { Component } from '@angular/core';
22
import { IonButton, IonContent, IonHeader, IonRouterOutlet, IonTitle, IonToolbar } from '@ionic/angular/standalone';
33

44
/**
5-
* Parent component that contains:
6-
* 1. Interactive content (buttons) that should remain clickable
7-
* 2. A nested ion-router-outlet where the child route with the modal will render
8-
*
9-
* This mimics the EXACT structure from issue #30700 reproduction:
10-
* - Parent page has buttons (+/-)
11-
* - Parent page has a nested IonRouterOutlet
12-
* - Child route (rendered in that outlet) contains ONLY the modal
13-
*
14-
* The bug is: when the modal opens in the child route, the buttons in THIS
15-
* parent component become non-interactive even with showBackdrop=false.
16-
*
17-
* Related issue: https://github.com/ionic-team/ionic-framework/issues/30700
5+
* Parent with interactive buttons and nested outlet for child route modal.
6+
* See https://github.com/ionic-team/ionic-framework/issues/30700
187
*/
198
@Component({
209
selector: 'app-modal-child-route-parent',
@@ -25,17 +14,12 @@ import { IonButton, IonContent, IonHeader, IonRouterOutlet, IonTitle, IonToolbar
2514
</ion-toolbar>
2615
</ion-header>
2716
<ion-content class="ion-padding">
28-
<!-- These buttons are in the PARENT and should be clickable when modal is open in CHILD -->
2917
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
3018
<ion-button id="decrement-btn" (click)="decrement()">-</ion-button>
3119
<p id="background-action-count">{{ count }}</p>
3220
<ion-button id="increment-btn" (click)="increment()">+</ion-button>
3321
</div>
34-
35-
<p>The modal will be rendered from a child route below:</p>
36-
37-
<!-- Nested router outlet - child route with modal renders here -->
38-
<ion-router-outlet id="child-router-outlet"></ion-router-outlet>
22+
<ion-router-outlet></ion-router-outlet>
3923
</ion-content>
4024
`,
4125
standalone: true,

0 commit comments

Comments
 (0)