Skip to content

Commit fea0a3d

Browse files
committed
fix(modal): dynamically handle safe-area insets based on modal type and position
1 parent f66c84a commit fea0a3d

File tree

3 files changed

+59
-13
lines changed

3 files changed

+59
-13
lines changed

core/src/components/modal/modal.tsx

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -595,14 +595,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
595595
await waitForMount();
596596
}
597597

598-
// Set all safe-areas to 0 before modal becomes visible to prevent flash
599-
// for centered dialogs. After animation, updateSafeAreaOverrides() will
600-
// restore values for edges that touch the viewport.
601-
const style = this.el.style;
602-
style.setProperty('--ion-safe-area-top', '0px');
603-
style.setProperty('--ion-safe-area-bottom', '0px');
604-
style.setProperty('--ion-safe-area-left', '0px');
605-
style.setProperty('--ion-safe-area-right', '0px');
598+
// Predict safe-area needs based on modal configuration to avoid visual snap
599+
this.setInitialSafeAreaOverrides(presentingElement);
606600

607601
writeTask(() => this.el.classList.add('show-modal'));
608602

@@ -872,11 +866,63 @@ export class Modal implements ComponentInterface, OverlayInterface {
872866
this.cachedPageParent = undefined;
873867
}
874868

869+
/**
870+
* Sets initial safe-area overrides based on modal configuration before
871+
* the modal becomes visible. This predicts whether the modal will touch
872+
* screen edges to avoid a visual snap after animation completes.
873+
*/
874+
private setInitialSafeAreaOverrides(presentingElement: HTMLElement | undefined) {
875+
const style = this.el.style;
876+
const isSheetModal = this.breakpoints !== undefined && this.initialBreakpoint !== undefined;
877+
const isCardModal = presentingElement !== undefined;
878+
const isTablet = window.innerWidth >= 768;
879+
880+
// Sheet modals: always touch bottom, top depends on breakpoint
881+
if (isSheetModal) {
882+
style.setProperty('--ion-safe-area-top', '0px');
883+
// Don't override bottom - sheet always touches bottom
884+
style.setProperty('--ion-safe-area-left', '0px');
885+
style.setProperty('--ion-safe-area-right', '0px');
886+
return;
887+
}
888+
889+
// Card modals are inset from edges (rounded corners), no safe areas needed
890+
if (isCardModal) {
891+
style.setProperty('--ion-safe-area-top', '0px');
892+
style.setProperty('--ion-safe-area-bottom', '0px');
893+
style.setProperty('--ion-safe-area-left', '0px');
894+
style.setProperty('--ion-safe-area-right', '0px');
895+
return;
896+
}
897+
898+
// Phone modals are fullscreen, need all safe areas
899+
if (!isTablet) {
900+
// Don't set any overrides - inherit from :root
901+
return;
902+
}
903+
904+
// Default tablet modal: centered dialog, no safe areas needed
905+
// Check for fullscreen override via CSS custom properties
906+
const computedStyle = getComputedStyle(this.el);
907+
const width = computedStyle.getPropertyValue('--width').trim();
908+
const height = computedStyle.getPropertyValue('--height').trim();
909+
910+
if (width === '100%' && height === '100%') {
911+
// Fullscreen modal - need safe areas, don't override
912+
return;
913+
}
914+
915+
// Centered dialog - zero out all safe areas
916+
style.setProperty('--ion-safe-area-top', '0px');
917+
style.setProperty('--ion-safe-area-bottom', '0px');
918+
style.setProperty('--ion-safe-area-left', '0px');
919+
style.setProperty('--ion-safe-area-right', '0px');
920+
}
921+
875922
/**
876923
* Updates safe-area CSS variable overrides based on whether the modal
877-
* is touching each edge of the viewport. This ensures that modals which
878-
* don't touch an edge (e.g., centered dialogs) don't have unnecessary
879-
* safe-area padding, while full-screen modals properly respect safe areas.
924+
* is touching each edge of the viewport. This is called after animation
925+
* and during gestures to handle dynamic position changes.
880926
*/
881927
private updateSafeAreaOverrides() {
882928
const wrapper = this.wrapperEl;

core/src/components/modal/test/safe-area/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ <h2>Sheet Modal (Full)</h2>
7979
<ion-item>
8080
<ion-label>
8181
<h2>Card Modal (iOS)</h2>
82-
<p>Card presentation - should have safe-area padding</p>
82+
<p>Card presentation - inset from edges, no safe-area padding</p>
8383
</ion-label>
8484
<ion-button slot="end" id="card-modal" onclick="presentCardModal()">Present</ion-button>
8585
</ion-item>

core/src/components/modal/test/safe-area/modal.e2e.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, screenshot, c
5858
await page.goto('/src/components/modal/test/safe-area', config);
5959
});
6060

61-
test('card modal should have safe-area padding', async ({ page }) => {
61+
test('card modal should not have safe-area padding', async ({ page }) => {
6262
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
6363

6464
await page.click('#card-modal');

0 commit comments

Comments
 (0)