diff --git a/core/src/components/modal/modal.tsx b/core/src/components/modal/modal.tsx index 174ac2f9d8a..a96d59c8e9f 100644 --- a/core/src/components/modal/modal.tsx +++ b/core/src/components/modal/modal.tsx @@ -1116,6 +1116,11 @@ export class Modal implements ComponentInterface, OverlayInterface { } private handleViewTransition() { + // Only run view transitions when the modal is presented + if (!this.presented) { + return; + } + const isPortrait = window.innerWidth < 768; // Only transition if view state actually changed diff --git a/core/src/components/modal/test/card-viewport-resize/modal.e2e.ts b/core/src/components/modal/test/card-viewport-resize/modal.e2e.ts new file mode 100644 index 00000000000..3d7adc849c7 --- /dev/null +++ b/core/src/components/modal/test/card-viewport-resize/modal.e2e.ts @@ -0,0 +1,176 @@ +import { expect } from '@playwright/test'; +import { configs, test } from '@utils/test/playwright'; + +configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => { + test.describe(title('card modal: viewport resize'), () => { + test.beforeEach(async ({ page }) => { + // Start in portrait mode (mobile) + await page.setViewportSize({ width: 375, height: 667 }); + + await page.setContent( + ` + +
+ + + Card Viewport Resize Test + + + +

This page tests that viewport resize does not trigger card modal animation when modal is closed.

+ Open Card Modal + + + + Card Modal + + Close + + + + +

Modal content

+
+
+
+
+
+ + + `, + config + ); + }); + + test('should not animate presenting element when viewport resizes and modal is closed', async ({ + page, + }, testInfo) => { + testInfo.annotations.push({ + type: 'issue', + description: 'https://github.com/ionic-team/ionic-framework/issues/30679', + }); + + const mainPage = page.locator('#main-page'); + + // Verify the presenting element has no transform initially + const initialTransform = await mainPage.evaluate((el) => { + return window.getComputedStyle(el).transform; + }); + expect(initialTransform).toBe('none'); + + // Resize from portrait to landscape (crossing the 768px threshold) + await page.setViewportSize({ width: 900, height: 375 }); + + // Wait for the debounced resize handler (50ms) plus some buffer + await page.waitForTimeout(150); + + // The presenting element should still have no transform + // If the bug exists, it would have scale(0.93) or similar applied + const afterResizeTransform = await mainPage.evaluate((el) => { + return window.getComputedStyle(el).transform; + }); + expect(afterResizeTransform).toBe('none'); + }); + + test('should not animate presenting element when resizing multiple times with modal closed', async ({ page }) => { + const mainPage = page.locator('#main-page'); + + // Multiple resize cycles should not trigger the animation + for (let i = 0; i < 3; i++) { + // Portrait to landscape + await page.setViewportSize({ width: 900, height: 375 }); + await page.waitForTimeout(150); + + let transform = await mainPage.evaluate((el) => { + return window.getComputedStyle(el).transform; + }); + expect(transform).toBe('none'); + + // Landscape to portrait + await page.setViewportSize({ width: 375, height: 667 }); + await page.waitForTimeout(150); + + transform = await mainPage.evaluate((el) => { + return window.getComputedStyle(el).transform; + }); + expect(transform).toBe('none'); + } + }); + + test('should still animate presenting element correctly when modal is open and viewport resizes', async ({ + page, + }) => { + const mainPage = page.locator('#main-page'); + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + + // Open the modal + await page.click('#open-modal'); + await ionModalDidPresent.next(); + + // When modal is open in portrait, presenting element should be transformed + let transform = await mainPage.evaluate((el) => { + return window.getComputedStyle(el).transform; + }); + // The presenting element should have a scale transform when modal is open + expect(transform).not.toBe('none'); + + // Resize to landscape while modal is open + await page.setViewportSize({ width: 900, height: 375 }); + await page.waitForTimeout(150); + + // The modal transitions correctly - in landscape mode the presenting element + // should have different (or no) transform than portrait + transform = await mainPage.evaluate((el) => { + return window.getComputedStyle(el).transform; + }); + + // Note: The exact transform depends on the landscape handling + // The main point is that when modal IS open, the transition should work + // This test just ensures we don't break existing functionality + }); + + test('presenting element should return to normal after modal is dismissed', async ({ page }) => { + const mainPage = page.locator('#main-page'); + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss'); + + // Open the modal + await page.click('#open-modal'); + await ionModalDidPresent.next(); + + // Close the modal + await page.click('#close-modal'); + await ionModalDidDismiss.next(); + + // Wait for animations to complete + await page.waitForTimeout(500); + + // The presenting element should be back to normal + const transform = await mainPage.evaluate((el) => { + return window.getComputedStyle(el).transform; + }); + expect(transform).toBe('none'); + + // Now resize the viewport - should not trigger animation + await page.setViewportSize({ width: 900, height: 375 }); + await page.waitForTimeout(150); + + const afterResizeTransform = await mainPage.evaluate((el) => { + return window.getComputedStyle(el).transform; + }); + expect(afterResizeTransform).toBe('none'); + }); + }); +});