fix(modal): prevent card modal animation on viewport resize when modal is closed (#30894)

Issue number: resolves #30679

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## 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

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Current dev build:
```
8.7.16-dev.11767028735.16932cea
```
This commit is contained in:
Shane
2025-12-30 10:33:41 -08:00
committed by GitHub
parent 72826edf9a
commit e5634d45ee
2 changed files with 181 additions and 0 deletions

View File

@@ -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

View File

@@ -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(
`
<ion-app>
<div class="ion-page" id="main-page">
<ion-header>
<ion-toolbar>
<ion-title>Card Viewport Resize Test</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<p>This page tests that viewport resize does not trigger card modal animation when modal is closed.</p>
<ion-button id="open-modal">Open Card Modal</ion-button>
<ion-modal id="card-modal">
<ion-header>
<ion-toolbar>
<ion-title>Card Modal</ion-title>
<ion-buttons slot="end">
<ion-button id="close-modal">Close</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<p>Modal content</p>
</ion-content>
</ion-modal>
</ion-content>
</div>
</ion-app>
<script>
const modal = document.querySelector('#card-modal');
const mainPage = document.querySelector('#main-page');
modal.presentingElement = mainPage;
document.querySelector('#open-modal').addEventListener('click', () => {
modal.present();
});
document.querySelector('#close-modal').addEventListener('click', () => {
modal.dismiss();
});
</script>
`,
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');
});
});
});