diff --git a/core/src/components/modal/modal.tsx b/core/src/components/modal/modal.tsx index b1a494430d0..f75471f1a78 100644 --- a/core/src/components/modal/modal.tsx +++ b/core/src/components/modal/modal.tsx @@ -1183,6 +1183,20 @@ export class Modal implements ComponentInterface, OverlayInterface { return; } + /** + * Don't observe for controller-based modals or when the parent is the + * app root (document.body or ion-app). These parents won't be removed, + * and observing document.body with subtree: true causes performance + * issues with frameworks like Angular during change detection. + */ + if ( + this.hasController || + this.cachedOriginalParent === document.body || + this.cachedOriginalParent.tagName === 'ION-APP' + ) { + return; + } + this.parentRemovalObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'childList' && mutation.removedNodes.length > 0) { diff --git a/core/src/components/modal/test/basic/modal.e2e.ts b/core/src/components/modal/test/basic/modal.e2e.ts index 325c4b3fbb5..1c9ac92743c 100644 --- a/core/src/components/modal/test/basic/modal.e2e.ts +++ b/core/src/components/modal/test/basic/modal.e2e.ts @@ -1,6 +1,6 @@ import { expect } from '@playwright/test'; -import { configs, test, Viewports } from '@utils/test/playwright'; import type { E2EPage } from '@utils/test/playwright'; +import { configs, test, Viewports } from '@utils/test/playwright'; configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { test.describe(title('modal: focus trapping'), () => { @@ -104,6 +104,28 @@ configs().forEach(({ title, screenshot, config }) => { }); configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('modal: parent removal observer'), () => { + test('should not set up parentRemovalObserver for controller-created modals', async ({ page }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'FW-6766', + }); + + await page.goto('/src/components/modal/test/basic', config); + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + + await page.click('#basic-modal'); + await ionModalDidPresent.next(); + + const modal = page.locator('ion-modal'); + const hasObserver = await modal.evaluate((el: any) => { + return el.parentRemovalObserver !== undefined; + }); + + expect(hasObserver).toBe(false); + }); + }); + test.describe(title('modal: backdrop'), () => { test.beforeEach(async ({ page }) => { await page.goto('/src/components/modal/test/basic', config);