mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
fix(modal): dynamically handle safe-area insets for edge-to-edge mode
This commit is contained in:
@@ -102,6 +102,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
private cachedOriginalParent?: HTMLElement;
|
||||
// Cached ion-page ancestor for child route passthrough
|
||||
private cachedPageParent?: HTMLElement | null;
|
||||
// Whether to skip coordinate-based safe-area detection (for fullscreen phone modals)
|
||||
private skipSafeAreaCoordinateDetection = false;
|
||||
|
||||
lastFocus?: HTMLElement;
|
||||
animation?: Animation;
|
||||
@@ -877,42 +879,61 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
const isCardModal = presentingElement !== undefined;
|
||||
const isTablet = window.innerWidth >= 768;
|
||||
|
||||
// Sheet modals: always touch bottom, top depends on breakpoint
|
||||
// Sheet modals always touch bottom edge, never top/left/right
|
||||
if (isSheetModal) {
|
||||
style.setProperty('--ion-safe-area-top', '0px');
|
||||
// Don't override bottom - sheet always touches bottom
|
||||
style.setProperty('--ion-safe-area-left', '0px');
|
||||
style.setProperty('--ion-safe-area-right', '0px');
|
||||
return;
|
||||
}
|
||||
|
||||
// Card modals are inset from edges (rounded corners), no safe areas needed
|
||||
// Card modals are inset from all edges
|
||||
if (isCardModal) {
|
||||
style.setProperty('--ion-safe-area-top', '0px');
|
||||
style.setProperty('--ion-safe-area-bottom', '0px');
|
||||
style.setProperty('--ion-safe-area-left', '0px');
|
||||
style.setProperty('--ion-safe-area-right', '0px');
|
||||
this.zeroAllSafeAreas();
|
||||
return;
|
||||
}
|
||||
|
||||
// Phone modals are fullscreen, need all safe areas
|
||||
// Phone-sized fullscreen modals inherit safe areas and use wrapper padding
|
||||
if (!isTablet) {
|
||||
// Don't set any overrides - inherit from :root
|
||||
this.applyFullscreenSafeArea();
|
||||
return;
|
||||
}
|
||||
|
||||
// Default tablet modal: centered dialog, no safe areas needed
|
||||
// Check for fullscreen override via CSS custom properties
|
||||
// Check if tablet modal is fullscreen via CSS custom properties
|
||||
const computedStyle = getComputedStyle(this.el);
|
||||
const width = computedStyle.getPropertyValue('--width').trim();
|
||||
const height = computedStyle.getPropertyValue('--height').trim();
|
||||
const isFullscreen = width === '100%' && height === '100%';
|
||||
|
||||
if (width === '100%' && height === '100%') {
|
||||
// Fullscreen modal - need safe areas, don't override
|
||||
return;
|
||||
if (isFullscreen) {
|
||||
this.applyFullscreenSafeArea();
|
||||
} else {
|
||||
// Centered dialog doesn't touch edges
|
||||
this.zeroAllSafeAreas();
|
||||
}
|
||||
}
|
||||
|
||||
// Centered dialog - zero out all safe areas
|
||||
/**
|
||||
* Applies safe-area handling for fullscreen modals.
|
||||
* Adds wrapper padding when no footer is present to prevent
|
||||
* content from overlapping system navigation areas.
|
||||
*/
|
||||
private applyFullscreenSafeArea() {
|
||||
this.skipSafeAreaCoordinateDetection = true;
|
||||
|
||||
const hasFooter = this.el.querySelector('ion-footer') !== null;
|
||||
if (!hasFooter && this.wrapperEl) {
|
||||
this.wrapperEl.style.setProperty('padding-bottom', 'var(--ion-safe-area-bottom, 0px)');
|
||||
this.wrapperEl.style.setProperty('box-sizing', 'border-box');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all safe-area CSS variables to 0px for modals that
|
||||
* don't touch screen edges.
|
||||
*/
|
||||
private zeroAllSafeAreas() {
|
||||
const style = this.el.style;
|
||||
style.setProperty('--ion-safe-area-top', '0px');
|
||||
style.setProperty('--ion-safe-area-bottom', '0px');
|
||||
style.setProperty('--ion-safe-area-left', '0px');
|
||||
@@ -921,22 +942,27 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
|
||||
/**
|
||||
* Updates safe-area CSS variable overrides based on whether the modal
|
||||
* is touching each edge of the viewport. This is called after animation
|
||||
* is touching each edge of the viewport. Called after animation
|
||||
* and during gestures to handle dynamic position changes.
|
||||
*/
|
||||
private updateSafeAreaOverrides() {
|
||||
if (this.skipSafeAreaCoordinateDetection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wrapper = this.wrapperEl;
|
||||
if (!wrapper) return;
|
||||
if (!wrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
const threshold = 2; // Account for subpixel rendering
|
||||
const threshold = 2;
|
||||
|
||||
const touchingTop = rect.top <= threshold;
|
||||
const touchingBottom = rect.bottom >= window.innerHeight - threshold;
|
||||
const touchingLeft = rect.left <= threshold;
|
||||
const touchingRight = rect.right >= window.innerWidth - threshold;
|
||||
|
||||
// Remove override when touching edge (allow inheritance), set to 0 when not touching
|
||||
const style = this.el.style;
|
||||
touchingTop ? style.removeProperty('--ion-safe-area-top') : style.setProperty('--ion-safe-area-top', '0px');
|
||||
touchingBottom
|
||||
@@ -1058,6 +1084,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
|
||||
}
|
||||
this.currentBreakpoint = undefined;
|
||||
this.animation = undefined;
|
||||
// Reset safe-area detection flag for potential re-presentation
|
||||
this.skipSafeAreaCoordinateDetection = false;
|
||||
|
||||
unlock();
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
|
||||
top,
|
||||
left,
|
||||
bottom,
|
||||
checkSafeAreaTop,
|
||||
checkSafeAreaBottom,
|
||||
checkSafeAreaLeft,
|
||||
checkSafeAreaRight,
|
||||
arrowTop,
|
||||
@@ -118,15 +120,27 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
|
||||
baseEl.classList.add('popover-bottom');
|
||||
}
|
||||
|
||||
if (bottom !== undefined) {
|
||||
contentEl.style.setProperty('bottom', `${bottom}px`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe area CSS variable adjustments.
|
||||
* When the popover is positioned near an edge, we add the corresponding
|
||||
* safe-area inset to ensure the popover doesn't overlap with system UI
|
||||
* (status bars, home indicators, navigation bars on Android API 36+, etc.)
|
||||
*/
|
||||
const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
|
||||
const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
|
||||
const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
|
||||
const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
|
||||
|
||||
let topValue = `${top}px`;
|
||||
let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
|
||||
let leftValue = `${left}px`;
|
||||
|
||||
if (checkSafeAreaTop) {
|
||||
topValue = `${top}px${safeAreaTop}`;
|
||||
}
|
||||
if (checkSafeAreaBottom && bottomValue !== undefined) {
|
||||
bottomValue = `${bottom}px${safeAreaBottom}`;
|
||||
}
|
||||
if (checkSafeAreaLeft) {
|
||||
leftValue = `${left}px${safeAreaLeft}`;
|
||||
}
|
||||
@@ -134,7 +148,11 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
|
||||
leftValue = `${left}px${safeAreaRight}`;
|
||||
}
|
||||
|
||||
contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
|
||||
if (bottomValue !== undefined) {
|
||||
contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
|
||||
}
|
||||
|
||||
contentEl.style.setProperty('top', `calc(${topValue} + var(--offset-y, 0))`);
|
||||
contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
|
||||
contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export const mdEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
|
||||
|
||||
const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
|
||||
|
||||
const { originX, originY, top, left, bottom } = calculateWindowAdjustment(
|
||||
const { originX, originY, top, left, bottom, checkSafeAreaTop, checkSafeAreaBottom } = calculateWindowAdjustment(
|
||||
side,
|
||||
results.top,
|
||||
results.left,
|
||||
@@ -62,6 +62,25 @@ export const mdEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
|
||||
results.referenceCoordinates
|
||||
);
|
||||
|
||||
/**
|
||||
* Safe area CSS variable adjustments.
|
||||
* When the popover is positioned near an edge, we add the corresponding
|
||||
* safe-area inset to ensure the popover doesn't overlap with system UI
|
||||
* (status bars, home indicators, navigation bars on Android API 36+, etc.)
|
||||
*/
|
||||
const safeAreaTop = ' + var(--ion-safe-area-top, 0)';
|
||||
const safeAreaBottom = ' + var(--ion-safe-area-bottom, 0)';
|
||||
|
||||
let topValue = `${top}px`;
|
||||
let bottomValue = bottom !== undefined ? `${bottom}px` : undefined;
|
||||
|
||||
if (checkSafeAreaTop) {
|
||||
topValue = `${top}px${safeAreaTop}`;
|
||||
}
|
||||
if (checkSafeAreaBottom && bottomValue !== undefined) {
|
||||
bottomValue = `${bottom}px${safeAreaBottom}`;
|
||||
}
|
||||
|
||||
const baseAnimation = createAnimation();
|
||||
const backdropAnimation = createAnimation();
|
||||
const wrapperAnimation = createAnimation();
|
||||
@@ -81,13 +100,13 @@ export const mdEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
|
||||
contentAnimation
|
||||
.addElement(contentEl)
|
||||
.beforeStyles({
|
||||
top: `calc(${top}px + var(--offset-y, 0px))`,
|
||||
top: `calc(${topValue} + var(--offset-y, 0px))`,
|
||||
left: `calc(${left}px + var(--offset-x, 0px))`,
|
||||
'transform-origin': `${originY} ${originX}`,
|
||||
})
|
||||
.beforeAddWrite(() => {
|
||||
if (bottom !== undefined) {
|
||||
contentEl.style.setProperty('bottom', `${bottom}px`);
|
||||
if (bottomValue !== undefined) {
|
||||
contentEl.style.setProperty('bottom', `calc(${bottomValue})`);
|
||||
}
|
||||
})
|
||||
.fromTo('transform', 'scale(0.8)', 'scale(1)');
|
||||
|
||||
179
core/src/components/popover/test/safe-area/index.html
Normal file
179
core/src/components/popover/test/safe-area/index.html
Normal file
@@ -0,0 +1,179 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Popover - Safe Area</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet" />
|
||||
<script src="../../../../../scripts/testing/scripts.js"></script>
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
<style>
|
||||
/**
|
||||
* Simulate safe-area insets for testing.
|
||||
* These values represent typical Android edge-to-edge safe areas.
|
||||
*/
|
||||
:root {
|
||||
--ion-safe-area-top: 44px;
|
||||
--ion-safe-area-bottom: 34px;
|
||||
--ion-safe-area-left: 0px;
|
||||
--ion-safe-area-right: 0px;
|
||||
}
|
||||
|
||||
/* Visual indicator 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);
|
||||
}
|
||||
|
||||
/* Position triggers at different locations */
|
||||
.bottom-trigger {
|
||||
position: fixed;
|
||||
bottom: 100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.near-bottom-trigger {
|
||||
position: fixed;
|
||||
bottom: 200px;
|
||||
right: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<!-- 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="ion-page" id="main-page">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Popover - Safe Area Positioning</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<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>
|
||||
|
||||
<ion-list>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<h2>Small Popover (Center)</h2>
|
||||
<p>Floating popover - positioned in center, no adjustment needed</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" id="small-popover-trigger">Present</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<h2>Large Popover</h2>
|
||||
<p>Tall content that may extend toward bottom safe area</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" id="large-popover-trigger">Present</ion-button>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
|
||||
<ion-button class="bottom-trigger" id="bottom-trigger"> Trigger Near Bottom </ion-button>
|
||||
|
||||
<ion-button class="near-bottom-trigger" id="near-bottom-trigger"> Near Bottom Right </ion-button>
|
||||
|
||||
<!-- Small popover -->
|
||||
<ion-popover trigger="small-popover-trigger" trigger-action="click">
|
||||
<ion-content class="ion-padding">
|
||||
<ion-list>
|
||||
<ion-item><ion-label>Option 1</ion-label></ion-item>
|
||||
<ion-item><ion-label>Option 2</ion-label></ion-item>
|
||||
<ion-item><ion-label>Option 3</ion-label></ion-item>
|
||||
</ion-list>
|
||||
</ion-content>
|
||||
</ion-popover>
|
||||
|
||||
<!-- Large popover with many items -->
|
||||
<ion-popover trigger="large-popover-trigger" trigger-action="click">
|
||||
<ion-content>
|
||||
<ion-list id="large-list"></ion-list>
|
||||
</ion-content>
|
||||
</ion-popover>
|
||||
|
||||
<!-- Popover triggered from near bottom -->
|
||||
<ion-popover trigger="bottom-trigger" trigger-action="click">
|
||||
<ion-content>
|
||||
<ion-list id="bottom-list"></ion-list>
|
||||
</ion-content>
|
||||
</ion-popover>
|
||||
|
||||
<!-- Popover triggered from near bottom right -->
|
||||
<ion-popover trigger="near-bottom-trigger" trigger-action="click">
|
||||
<ion-content>
|
||||
<ion-list id="near-bottom-list"></ion-list>
|
||||
</ion-content>
|
||||
</ion-popover>
|
||||
</ion-content>
|
||||
</div>
|
||||
</ion-app>
|
||||
|
||||
<script>
|
||||
// Generate list items for popovers
|
||||
function generateItems(listId, count) {
|
||||
const list = document.getElementById(listId);
|
||||
if (!list) return;
|
||||
|
||||
for (let i = 1; i <= count; i++) {
|
||||
const item = document.createElement('ion-item');
|
||||
const label = document.createElement('ion-label');
|
||||
label.textContent = `Item ${i}`;
|
||||
item.appendChild(label);
|
||||
list.appendChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
generateItems('large-list', 15);
|
||||
generateItems('bottom-list', 10);
|
||||
generateItems('near-bottom-list', 8);
|
||||
|
||||
// Log positioning info for debugging
|
||||
document.querySelectorAll('ion-popover').forEach((popover) => {
|
||||
popover.addEventListener('ionPopoverDidPresent', () => {
|
||||
const content = popover.shadowRoot.querySelector('.popover-content');
|
||||
if (content) {
|
||||
const rect = content.getBoundingClientRect();
|
||||
const bottomSafeArea =
|
||||
parseInt(getComputedStyle(document.documentElement).getPropertyValue('--ion-safe-area-bottom')) || 0;
|
||||
console.log('Popover position:', {
|
||||
top: rect.top,
|
||||
bottom: rect.bottom,
|
||||
windowHeight: window.innerHeight,
|
||||
bottomSafeArea,
|
||||
distanceFromBottom: window.innerHeight - rect.bottom,
|
||||
overlapsBottomSafeArea: rect.bottom > window.innerHeight - bottomSafeArea,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
99
core/src/components/popover/test/safe-area/popover.e2e.ts
Normal file
99
core/src/components/popover/test/safe-area/popover.e2e.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { configs, test } from '@utils/test/playwright';
|
||||
|
||||
/**
|
||||
* Safe-area tests verify that popovers are correctly positioned
|
||||
* to avoid overlapping with safe-area zones (status bars, navigation bars, etc.)
|
||||
*
|
||||
* This is especially important for Android API 36+ where edge-to-edge mode
|
||||
* is enforced and apps can no longer opt out.
|
||||
*/
|
||||
|
||||
configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('popover: safe-area positioning'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/popover/test/safe-area', config);
|
||||
});
|
||||
|
||||
test('popover pinned to bottom should account for safe-area-bottom in position', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/30900',
|
||||
});
|
||||
|
||||
// Use a smaller viewport to force the popover to be constrained
|
||||
await page.setViewportSize({ width: 375, height: 500 });
|
||||
|
||||
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
|
||||
|
||||
// Click the trigger near the bottom of the screen
|
||||
await page.click('#bottom-trigger');
|
||||
await ionPopoverDidPresent.next();
|
||||
|
||||
// Target the specific popover that was presented (the one with trigger="bottom-trigger")
|
||||
const popover = page.locator('ion-popover[trigger="bottom-trigger"]');
|
||||
const popoverContent = popover.locator('.popover-content');
|
||||
|
||||
// Get the computed bottom style - should include safe-area calc
|
||||
const bottomStyle = await popoverContent.evaluate((el) => el.style.bottom);
|
||||
|
||||
// The bottom should include the safe-area-bottom CSS variable
|
||||
// This ensures the popover is positioned above the unsafe area
|
||||
expect(bottomStyle).toContain('var(--ion-safe-area-bottom');
|
||||
});
|
||||
|
||||
test('floating popover should not have safe-area adjustments', async ({ page }) => {
|
||||
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
|
||||
|
||||
await page.click('#small-popover-trigger');
|
||||
await ionPopoverDidPresent.next();
|
||||
|
||||
// Target the specific popover
|
||||
const popover = page.locator('ion-popover[trigger="small-popover-trigger"]');
|
||||
const popoverContent = popover.locator('.popover-content');
|
||||
|
||||
// Get the computed top and bottom styles
|
||||
const topStyle = await popoverContent.evaluate((el) => el.style.top);
|
||||
const bottomStyle = await popoverContent.evaluate((el) => el.style.bottom);
|
||||
|
||||
// A floating popover in the middle shouldn't have safe-area adjustments
|
||||
// The top should be a simple calc without safe-area
|
||||
expect(topStyle).not.toContain('var(--ion-safe-area-top');
|
||||
// The bottom should not be set for a floating popover
|
||||
expect(bottomStyle).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
|
||||
test.describe(title('popover: safe-area positioning - md mode'), () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/src/components/popover/test/safe-area', config);
|
||||
});
|
||||
|
||||
test('popover pinned to bottom should account for safe-area-bottom in position', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/30900',
|
||||
});
|
||||
|
||||
// Use a smaller viewport to force the popover to be constrained
|
||||
await page.setViewportSize({ width: 375, height: 500 });
|
||||
|
||||
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
|
||||
|
||||
await page.click('#bottom-trigger');
|
||||
await ionPopoverDidPresent.next();
|
||||
|
||||
// Target the specific popover
|
||||
const popover = page.locator('ion-popover[trigger="bottom-trigger"]');
|
||||
const popoverContent = popover.locator('.popover-content');
|
||||
|
||||
// Get the computed bottom style - should include safe-area calc
|
||||
const bottomStyle = await popoverContent.evaluate((el) => el.style.bottom);
|
||||
|
||||
// The bottom should include the safe-area-bottom CSS variable
|
||||
expect(bottomStyle).toContain('var(--ion-safe-area-bottom');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -30,6 +30,8 @@ export interface PopoverStyles {
|
||||
bottom?: number;
|
||||
originX: string;
|
||||
originY: string;
|
||||
checkSafeAreaTop: boolean;
|
||||
checkSafeAreaBottom: boolean;
|
||||
checkSafeAreaLeft: boolean;
|
||||
checkSafeAreaRight: boolean;
|
||||
arrowTop: number;
|
||||
@@ -829,6 +831,8 @@ export const calculateWindowAdjustment = (
|
||||
let bottom;
|
||||
let originX = contentOriginX;
|
||||
let originY = contentOriginY;
|
||||
let checkSafeAreaTop = false;
|
||||
let checkSafeAreaBottom = false;
|
||||
let checkSafeAreaLeft = false;
|
||||
let checkSafeAreaRight = false;
|
||||
const triggerTop = triggerCoordinates
|
||||
@@ -874,17 +878,32 @@ export const calculateWindowAdjustment = (
|
||||
* We chose 12 here so that the popover position looks a bit nicer as
|
||||
* it is not right up against the edge of the screen.
|
||||
*/
|
||||
top = Math.max(12, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
|
||||
top = Math.max(bodyPadding, triggerTop - contentHeight - triggerHeight - (arrowHeight - 1));
|
||||
arrowTop = top + contentHeight;
|
||||
originY = 'bottom';
|
||||
addPopoverBottomClass = true;
|
||||
|
||||
/**
|
||||
* If the popover is positioned near the top edge, account for safe area.
|
||||
* This ensures the popover doesn't overlap with status bars or notches.
|
||||
*/
|
||||
if (top <= bodyPadding + safeAreaMargin) {
|
||||
checkSafeAreaTop = true;
|
||||
top = bodyPadding;
|
||||
}
|
||||
|
||||
/**
|
||||
* If not enough room for popover to appear
|
||||
* above trigger, then cut it off.
|
||||
*/
|
||||
} else {
|
||||
bottom = bodyPadding;
|
||||
/**
|
||||
* When the popover is pinned to the bottom, account for safe area.
|
||||
* This ensures the popover doesn't overlap with home indicators
|
||||
* or navigation bars (e.g., Android API 36+ edge-to-edge).
|
||||
*/
|
||||
checkSafeAreaBottom = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -894,6 +913,8 @@ export const calculateWindowAdjustment = (
|
||||
bottom,
|
||||
originX,
|
||||
originY,
|
||||
checkSafeAreaTop,
|
||||
checkSafeAreaBottom,
|
||||
checkSafeAreaLeft,
|
||||
checkSafeAreaRight,
|
||||
arrowTop,
|
||||
|
||||
Reference in New Issue
Block a user