From 36f4b4d600a8d9e53959a24ba51087a0eb587030 Mon Sep 17 00:00:00 2001 From: Shane Date: Tue, 30 Dec 2025 10:33:41 -0800 Subject: [PATCH] fix(modal): prevent card modal animation on viewport resize when modal is closed (#30894) Issue number: resolves #30679 --------- ## What is the current behavior? When a page contains a card modal with a `presentingElement`, resizing the viewport (e.g., rotating from portrait to landscape) triggers the card modal's "lean back" animation on the presenting element, even when the modal has never been opened. ## What is the new behavior? Viewport resize events no longer trigger the presenting element animation when the modal is not presented. The animation only runs when the modal is actually open. ## Does this introduce a breaking change? - [ ] Yes - [X] No ## Other information Current dev build: ``` 8.7.16-dev.11767028735.16932cea ``` --- core/src/components/modal/modal.tsx | 5 + .../test/card-viewport-resize/modal.e2e.ts | 176 ++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 core/src/components/modal/test/card-viewport-resize/modal.e2e.ts diff --git a/core/src/components/modal/modal.tsx b/core/src/components/modal/modal.tsx index 174ac2f9d8..a96d59c8e9 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 0000000000..3d7adc849c --- /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'); + }); + }); +});