Skip to content

Commit e5634d4

Browse files
authored
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 ```
1 parent 72826ed commit e5634d4

File tree

2 files changed

+181
-0
lines changed

2 files changed

+181
-0
lines changed

core/src/components/modal/modal.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,6 +1116,11 @@ export class Modal implements ComponentInterface, OverlayInterface {
11161116
}
11171117

11181118
private handleViewTransition() {
1119+
// Only run view transitions when the modal is presented
1120+
if (!this.presented) {
1121+
return;
1122+
}
1123+
11191124
const isPortrait = window.innerWidth < 768;
11201125

11211126
// Only transition if view state actually changed
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { expect } from '@playwright/test';
2+
import { configs, test } from '@utils/test/playwright';
3+
4+
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
5+
test.describe(title('card modal: viewport resize'), () => {
6+
test.beforeEach(async ({ page }) => {
7+
// Start in portrait mode (mobile)
8+
await page.setViewportSize({ width: 375, height: 667 });
9+
10+
await page.setContent(
11+
`
12+
<ion-app>
13+
<div class="ion-page" id="main-page">
14+
<ion-header>
15+
<ion-toolbar>
16+
<ion-title>Card Viewport Resize Test</ion-title>
17+
</ion-toolbar>
18+
</ion-header>
19+
<ion-content class="ion-padding">
20+
<p>This page tests that viewport resize does not trigger card modal animation when modal is closed.</p>
21+
<ion-button id="open-modal">Open Card Modal</ion-button>
22+
<ion-modal id="card-modal">
23+
<ion-header>
24+
<ion-toolbar>
25+
<ion-title>Card Modal</ion-title>
26+
<ion-buttons slot="end">
27+
<ion-button id="close-modal">Close</ion-button>
28+
</ion-buttons>
29+
</ion-toolbar>
30+
</ion-header>
31+
<ion-content class="ion-padding">
32+
<p>Modal content</p>
33+
</ion-content>
34+
</ion-modal>
35+
</ion-content>
36+
</div>
37+
</ion-app>
38+
39+
<script>
40+
const modal = document.querySelector('#card-modal');
41+
const mainPage = document.querySelector('#main-page');
42+
modal.presentingElement = mainPage;
43+
44+
document.querySelector('#open-modal').addEventListener('click', () => {
45+
modal.present();
46+
});
47+
48+
document.querySelector('#close-modal').addEventListener('click', () => {
49+
modal.dismiss();
50+
});
51+
</script>
52+
`,
53+
config
54+
);
55+
});
56+
57+
test('should not animate presenting element when viewport resizes and modal is closed', async ({
58+
page,
59+
}, testInfo) => {
60+
testInfo.annotations.push({
61+
type: 'issue',
62+
description: 'https://github.com/ionic-team/ionic-framework/issues/30679',
63+
});
64+
65+
const mainPage = page.locator('#main-page');
66+
67+
// Verify the presenting element has no transform initially
68+
const initialTransform = await mainPage.evaluate((el) => {
69+
return window.getComputedStyle(el).transform;
70+
});
71+
expect(initialTransform).toBe('none');
72+
73+
// Resize from portrait to landscape (crossing the 768px threshold)
74+
await page.setViewportSize({ width: 900, height: 375 });
75+
76+
// Wait for the debounced resize handler (50ms) plus some buffer
77+
await page.waitForTimeout(150);
78+
79+
// The presenting element should still have no transform
80+
// If the bug exists, it would have scale(0.93) or similar applied
81+
const afterResizeTransform = await mainPage.evaluate((el) => {
82+
return window.getComputedStyle(el).transform;
83+
});
84+
expect(afterResizeTransform).toBe('none');
85+
});
86+
87+
test('should not animate presenting element when resizing multiple times with modal closed', async ({ page }) => {
88+
const mainPage = page.locator('#main-page');
89+
90+
// Multiple resize cycles should not trigger the animation
91+
for (let i = 0; i < 3; i++) {
92+
// Portrait to landscape
93+
await page.setViewportSize({ width: 900, height: 375 });
94+
await page.waitForTimeout(150);
95+
96+
let transform = await mainPage.evaluate((el) => {
97+
return window.getComputedStyle(el).transform;
98+
});
99+
expect(transform).toBe('none');
100+
101+
// Landscape to portrait
102+
await page.setViewportSize({ width: 375, height: 667 });
103+
await page.waitForTimeout(150);
104+
105+
transform = await mainPage.evaluate((el) => {
106+
return window.getComputedStyle(el).transform;
107+
});
108+
expect(transform).toBe('none');
109+
}
110+
});
111+
112+
test('should still animate presenting element correctly when modal is open and viewport resizes', async ({
113+
page,
114+
}) => {
115+
const mainPage = page.locator('#main-page');
116+
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
117+
118+
// Open the modal
119+
await page.click('#open-modal');
120+
await ionModalDidPresent.next();
121+
122+
// When modal is open in portrait, presenting element should be transformed
123+
let transform = await mainPage.evaluate((el) => {
124+
return window.getComputedStyle(el).transform;
125+
});
126+
// The presenting element should have a scale transform when modal is open
127+
expect(transform).not.toBe('none');
128+
129+
// Resize to landscape while modal is open
130+
await page.setViewportSize({ width: 900, height: 375 });
131+
await page.waitForTimeout(150);
132+
133+
// The modal transitions correctly - in landscape mode the presenting element
134+
// should have different (or no) transform than portrait
135+
transform = await mainPage.evaluate((el) => {
136+
return window.getComputedStyle(el).transform;
137+
});
138+
139+
// Note: The exact transform depends on the landscape handling
140+
// The main point is that when modal IS open, the transition should work
141+
// This test just ensures we don't break existing functionality
142+
});
143+
144+
test('presenting element should return to normal after modal is dismissed', async ({ page }) => {
145+
const mainPage = page.locator('#main-page');
146+
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
147+
const ionModalDidDismiss = await page.spyOnEvent('ionModalDidDismiss');
148+
149+
// Open the modal
150+
await page.click('#open-modal');
151+
await ionModalDidPresent.next();
152+
153+
// Close the modal
154+
await page.click('#close-modal');
155+
await ionModalDidDismiss.next();
156+
157+
// Wait for animations to complete
158+
await page.waitForTimeout(500);
159+
160+
// The presenting element should be back to normal
161+
const transform = await mainPage.evaluate((el) => {
162+
return window.getComputedStyle(el).transform;
163+
});
164+
expect(transform).toBe('none');
165+
166+
// Now resize the viewport - should not trigger animation
167+
await page.setViewportSize({ width: 900, height: 375 });
168+
await page.waitForTimeout(150);
169+
170+
const afterResizeTransform = await mainPage.evaluate((el) => {
171+
return window.getComputedStyle(el).transform;
172+
});
173+
expect(afterResizeTransform).toBe('none');
174+
});
175+
});
176+
});

0 commit comments

Comments
 (0)