fix(modal): fixing safe area in certain situations, adding some tests

This commit is contained in:
ShaneK
2026-01-30 07:01:34 -08:00
parent 1f68ad48f2
commit fcc50d6d16
3 changed files with 105 additions and 27 deletions

View File

@@ -75,6 +75,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
@State() private isSheetModal = false;
private currentBreakpoint?: number;
private wrapperEl?: HTMLElement;
private shadowEl?: HTMLElement;
private backdropEl?: HTMLIonBackdropElement;
private dragHandleEl?: HTMLButtonElement;
private sortedBreakpoints?: number[];
@@ -914,13 +915,9 @@ export class Modal implements ComponentInterface, OverlayInterface {
return;
}
// Phone-sized fullscreen modals inherit safe areas and use wrapper padding
if (!isTablet) {
this.applyFullscreenSafeArea();
return;
}
// Check if tablet modal is fullscreen via CSS custom properties
// Check if modal is fullscreen via CSS custom properties
// This applies to both phone and tablet sizes - custom modals may have
// non-fullscreen dimensions even on phones (e.g., --height: 70%)
const computedStyle = getComputedStyle(this.el);
const width = computedStyle.getPropertyValue('--width').trim();
const height = computedStyle.getPropertyValue('--height').trim();
@@ -928,9 +925,12 @@ export class Modal implements ComponentInterface, OverlayInterface {
if (isFullscreen) {
this.applyFullscreenSafeArea();
} else {
// Centered dialog doesn't touch edges
} else if (isTablet) {
// Centered dialog on tablet doesn't touch edges
this.zeroAllSafeAreas();
} else {
// Non-fullscreen modal on phone - use coordinate-based detection
// to determine which edges it touches (e.g., bottom-aligned custom modals)
}
}
@@ -953,19 +953,27 @@ export class Modal implements ComponentInterface, OverlayInterface {
}
/**
* Updates wrapper padding based on footer presence.
* Updates wrapper and shadow padding based on footer presence.
* Called initially and when footer is dynamically added/removed.
* Both elements must be styled identically to prevent visual mismatches.
*/
private updateFooterPadding() {
if (!this.wrapperEl) return;
const hasFooter = this.el.querySelector('ion-footer') !== null;
// Apply to both wrapper and shadow to keep them in sync
const elements = [this.wrapperEl, this.shadowEl].filter(Boolean) as HTMLElement[];
if (hasFooter) {
this.wrapperEl.style.removeProperty('padding-bottom');
this.wrapperEl.style.removeProperty('box-sizing');
elements.forEach((el) => {
el.style.removeProperty('padding-bottom');
el.style.removeProperty('box-sizing');
});
} else {
this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
this.wrapperEl.style.setProperty('box-sizing', 'border-box');
elements.forEach((el) => {
el.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
el.style.setProperty('box-sizing', 'border-box');
});
}
}
@@ -993,11 +1001,13 @@ export class Modal implements ComponentInterface, OverlayInterface {
this.footerObserver?.disconnect();
this.footerObserver = undefined;
// Clear wrapper styles that may have been set for safe-area handling
if (this.wrapperEl) {
this.wrapperEl.style.removeProperty('padding-bottom');
this.wrapperEl.style.removeProperty('box-sizing');
}
// Clear wrapper and shadow styles that may have been set for safe-area handling
[this.wrapperEl, this.shadowEl].forEach((el) => {
if (el) {
el.style.removeProperty('padding-bottom');
el.style.removeProperty('box-sizing');
}
});
// Clear safe-area CSS variable overrides
const style = this.el.style;
@@ -1613,7 +1623,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
part="backdrop"
/>
{mode === 'ios' && <div class="modal-shadow"></div>}
{mode === 'ios' && <div class="modal-shadow" ref={(el) => (this.shadowEl = el)}></div>}
<div
/*

View File

@@ -15,23 +15,66 @@
<style>
/**
* Simulate safe-area insets for testing.
* These values represent typical iPad safe areas.
* Values represent combined scenarios: top/bottom from portrait devices,
* left/right from landscape orientation or devices with side notches.
*/
:root {
--ion-safe-area-top: 44px;
--ion-safe-area-bottom: 34px;
--ion-safe-area-left: 0px;
--ion-safe-area-right: 0px;
--ion-safe-area-left: 44px;
--ion-safe-area-right: 44px;
}
.fullscreen-modal {
--width: 100%;
--height: 100%;
}
/* Visual indicators for safe areas */
.safe-area-indicator {
position: fixed;
background: rgba(255, 0, 0, 0.2);
pointer-events: none;
z-index: 99999;
}
.safe-area-top {
top: 0;
left: 0;
right: 0;
height: var(--ion-safe-area-top);
}
.safe-area-bottom {
bottom: 0;
left: 0;
right: 0;
height: var(--ion-safe-area-bottom);
}
.safe-area-left {
top: 0;
bottom: 0;
left: 0;
width: var(--ion-safe-area-left);
}
.safe-area-right {
top: 0;
bottom: 0;
right: 0;
width: var(--ion-safe-area-right);
}
</style>
</head>
<body>
<!-- Visual indicators for safe areas (red overlay) -->
<div class="safe-area-indicator safe-area-top"></div>
<div class="safe-area-indicator safe-area-bottom"></div>
<div class="safe-area-indicator safe-area-left"></div>
<div class="safe-area-indicator safe-area-right"></div>
<ion-app>
<div class="ion-page" id="main-page">
<ion-header>
@@ -41,7 +84,11 @@
</ion-header>
<ion-content class="ion-padding">
<p>Test safe-area handling in modals.</p>
<p>Test safe-area handling in modals. Red overlays indicate safe areas (top, bottom, left, right).</p>
<p>
<strong>Landscape simulation:</strong> Left and right safe areas are set to 44px to test devices with side
notches.
</p>
<ion-list>
<ion-item-group>

View File

@@ -16,12 +16,13 @@
/**
* Simulate safe-area insets for testing.
* These values represent typical Android edge-to-edge safe areas.
* Left/right values simulate landscape orientation or devices with side notches.
*/
:root {
--ion-safe-area-top: 44px;
--ion-safe-area-bottom: 34px;
--ion-safe-area-left: 0px;
--ion-safe-area-right: 0px;
--ion-safe-area-left: 44px;
--ion-safe-area-right: 44px;
}
/* Visual indicator for safe areas */
@@ -46,6 +47,20 @@
height: var(--ion-safe-area-bottom);
}
.safe-area-left {
top: 0;
bottom: 0;
left: 0;
width: var(--ion-safe-area-left);
}
.safe-area-right {
top: 0;
bottom: 0;
right: 0;
width: var(--ion-safe-area-right);
}
/* Position triggers at different locations */
.bottom-trigger {
position: fixed;
@@ -67,6 +82,8 @@
<!-- Visual indicators for safe areas -->
<div class="safe-area-indicator safe-area-top"></div>
<div class="safe-area-indicator safe-area-bottom"></div>
<div class="safe-area-indicator safe-area-left"></div>
<div class="safe-area-indicator safe-area-right"></div>
<div class="ion-page" id="main-page">
<ion-header>
@@ -77,7 +94,11 @@
<ion-content class="ion-padding">
<p>Test that popovers are <strong>positioned away from</strong> unsafe areas (shown in red).</p>
<p>The popover should be moved up/down to avoid overlapping the safe-area zones.</p>
<p>The popover should be moved up/down/left/right to avoid overlapping the safe-area zones.</p>
<p>
<strong>Landscape simulation:</strong> Left and right safe areas are set to 44px to test devices with side
notches.
</p>
<ion-list>
<ion-item>