diff --git a/core/src/components/content/content.tsx b/core/src/components/content/content.tsx
index 87e2c5938e..947c4104d5 100644
--- a/core/src/components/content/content.tsx
+++ b/core/src/components/content/content.tsx
@@ -203,8 +203,13 @@ export class Content implements ComponentInterface {
const parent = this.el.parentElement;
if (parent && !this.parentMutationObserver && win !== undefined && 'MutationObserver' in win) {
this.parentMutationObserver = new MutationObserver(() => {
+ const prevHasHeader = this.hasHeader;
+ const prevHasFooter = this.hasFooter;
this.updateSiblingDetection();
- forceUpdate(this);
+ // Only trigger re-render if header/footer detection actually changed
+ if (prevHasHeader !== this.hasHeader || prevHasFooter !== this.hasFooter) {
+ forceUpdate(this);
+ }
});
this.parentMutationObserver.observe(parent, { childList: true });
}
diff --git a/core/src/components/content/test/safe-area/content.e2e.ts b/core/src/components/content/test/safe-area/content.e2e.ts
new file mode 100644
index 0000000000..0255ac74bb
--- /dev/null
+++ b/core/src/components/content/test/safe-area/content.e2e.ts
@@ -0,0 +1,167 @@
+import { expect } from '@playwright/test';
+import { configs, test } from '@utils/test/playwright';
+
+/**
+ * Safe-area tests verify that ion-content correctly applies safe-area classes
+ * based on the presence/absence of sibling ion-header and ion-footer elements.
+ *
+ * These tests verify the FW-6830 feature: automatic safe-area handling for content.
+ */
+
+configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
+ test.describe(title('content: safe-area'), () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/src/components/content/test/safe-area', config);
+ });
+
+ test('content without header should have safe-area-top class', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
+ const content = page.locator('#content-no-header');
+ await expect(content).toHaveClass(/safe-area-top/);
+ await expect(content).not.toHaveClass(/safe-area-bottom/);
+ });
+
+ test('content without footer should have safe-area-bottom class', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
+ const content = page.locator('#content-no-footer');
+ await expect(content).not.toHaveClass(/safe-area-top/);
+ await expect(content).toHaveClass(/safe-area-bottom/);
+ });
+
+ test('content with both header and footer should not have safe-area classes', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
+ const content = page.locator('#content-with-both');
+ await expect(content).not.toHaveClass(/safe-area-top/);
+ await expect(content).not.toHaveClass(/safe-area-bottom/);
+ });
+
+ test('content without header or footer should have both safe-area classes', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
+ const content = page.locator('#content-no-both');
+ await expect(content).toHaveClass(/safe-area-top/);
+ await expect(content).toHaveClass(/safe-area-bottom/);
+ });
+
+ test('content with wrapped header should not have safe-area-top class', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
+ const content = page.locator('#content-wrapped-header');
+ // Wrapped header detection should find the ion-header inside my-header
+ await expect(content).not.toHaveClass(/safe-area-top/);
+ });
+
+ test('content with wrapped footer should not have safe-area-bottom class', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
+ const content = page.locator('#content-wrapped-footer');
+ // Wrapped footer detection should find the ion-footer inside my-footer
+ await expect(content).not.toHaveClass(/safe-area-bottom/);
+ });
+
+ test('nested content should not have safe-area classes', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
+ const nestedContent = page.locator('#content-nested');
+ // Nested content should not be treated as main content
+ await expect(nestedContent).not.toHaveClass(/safe-area-top/);
+ await expect(nestedContent).not.toHaveClass(/safe-area-bottom/);
+ });
+
+ test('outer content should still have safe-area classes', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
+ const outerContent = page.locator('#content-outer');
+ // Outer content has no sibling header/footer, so it should have safe-area classes
+ await expect(outerContent).toHaveClass(/safe-area-top/);
+ await expect(outerContent).toHaveClass(/safe-area-bottom/);
+ });
+
+ test('content inside modal should not have safe-area classes', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
+ // Open the modal
+ await page.evaluate(() => {
+ const modal = document.getElementById('test-modal') as HTMLIonModalElement;
+ modal.isOpen = true;
+ });
+
+ // Wait for modal to be presented
+ const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
+ await ionModalDidPresent.next();
+
+ const modalContent = page.locator('#content-in-modal');
+ // Content inside modal should not be treated as main content
+ await expect(modalContent).not.toHaveClass(/safe-area-top/);
+ await expect(modalContent).not.toHaveClass(/safe-area-bottom/);
+ });
+
+ test('dynamic header addition should update safe-area classes', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
+ const content = page.locator('#content-dynamic');
+
+ // Initially should have safe-area-top (no header)
+ await expect(content).toHaveClass(/safe-area-top/);
+
+ // Add header dynamically
+ await page.click('#add-header-btn');
+
+ // Wait for mutation observer to trigger and component to update
+ // Using expect with timeout instead of waitForTimeout for reliability
+ await expect(content).not.toHaveClass(/safe-area-top/, { timeout: 1000 });
+ });
+
+ test('dynamic header removal should update safe-area classes', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
+ const content = page.locator('#content-dynamic');
+
+ // Add header first
+ await page.click('#add-header-btn');
+ await expect(content).not.toHaveClass(/safe-area-top/, { timeout: 1000 });
+
+ // Remove header
+ await page.click('#remove-header-btn');
+
+ // Should have safe-area-top again
+ await expect(content).toHaveClass(/safe-area-top/, { timeout: 1000 });
+ });
+ });
+});
diff --git a/core/src/components/content/test/safe-area/index.html b/core/src/components/content/test/safe-area/index.html
new file mode 100644
index 0000000000..1390ac9e9c
--- /dev/null
+++ b/core/src/components/content/test/safe-area/index.html
@@ -0,0 +1,192 @@
+
+
+
+
+ Content - Safe Area
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Header
+
+
+
+ Content with both header and footer - should NOT have safe-area classes
+
+
+
+ Footer
+
+
+
+
+
+
+
+
+
+ Content without header or footer - should have both safe-area classes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Outer content
+
+
+ Nested content - should NOT have safe-area classes
+
+
+
+
+
+
+
+
+
+ Content inside modal - should NOT have safe-area classes
+
+
+
+
+
+
+
+ Content with dynamic header/footer
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/components/modal/modal.tsx b/core/src/components/modal/modal.tsx
index 00a240400d..bf57c36f07 100644
--- a/core/src/components/modal/modal.tsx
+++ b/core/src/components/modal/modal.tsx
@@ -419,6 +419,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
this.triggerController.removeClickListener();
this.cleanupViewTransitionListener();
this.cleanupParentRemovalObserver();
+ // Reset safe-area state to handle removal without dismiss (e.g., framework unmount)
+ this.resetSafeAreaState();
}
componentWillLoad() {
@@ -942,6 +944,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
this.updateFooterPadding();
// Watch for dynamic footer additions/removals (e.g., async data loading)
+ // Use subtree:true to support wrapped footers in framework components
+ // (e.g., ...)
if (!this.footerObserver && win !== undefined && 'MutationObserver' in win) {
this.footerObserver = new MutationObserver(() => this.updateFooterPadding());
this.footerObserver.observe(this.el, { childList: true, subtree: true });
@@ -977,6 +981,32 @@ export class Modal implements ComponentInterface, OverlayInterface {
style.setProperty('--ion-safe-area-right', '0px');
}
+ /**
+ * Resets all safe-area related state and styles.
+ * Called during dismiss and disconnectedCallback to ensure clean state
+ * for re-presentation of inline modals.
+ */
+ private resetSafeAreaState() {
+ this.skipSafeAreaCoordinateDetection = false;
+ this.cachedSafeAreas = undefined;
+ this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
+ 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 safe-area CSS variable overrides
+ const style = this.el.style;
+ style.removeProperty('--ion-safe-area-top');
+ style.removeProperty('--ion-safe-area-bottom');
+ style.removeProperty('--ion-safe-area-left');
+ style.removeProperty('--ion-safe-area-right');
+ }
+
/**
* Gets the root safe-area values from the document element.
* Uses cached values during gestures to avoid getComputedStyle calls.
@@ -1158,22 +1188,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
this.currentBreakpoint = undefined;
this.animation = undefined;
// Reset safe-area state for potential re-presentation
- this.skipSafeAreaCoordinateDetection = false;
- this.cachedSafeAreas = undefined;
- this.prevSafeAreaState = { top: false, bottom: false, left: false, right: false };
- this.footerObserver?.disconnect();
- this.footerObserver = undefined;
- // Clear 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 safe-area CSS variable overrides
- const style = this.el.style;
- style.removeProperty('--ion-safe-area-top');
- style.removeProperty('--ion-safe-area-bottom');
- style.removeProperty('--ion-safe-area-left');
- style.removeProperty('--ion-safe-area-right');
+ this.resetSafeAreaState();
unlock();
diff --git a/core/src/components/modal/test/safe-area/modal.e2e.ts b/core/src/components/modal/test/safe-area/modal.e2e.ts
index d317c85ae7..39bd2bb246 100644
--- a/core/src/components/modal/test/safe-area/modal.e2e.ts
+++ b/core/src/components/modal/test/safe-area/modal.e2e.ts
@@ -1,4 +1,5 @@
import { expect } from '@playwright/test';
+import type { E2EPage } from '@utils/test/playwright';
import { configs, test, Viewports } from '@utils/test/playwright';
/**
@@ -10,7 +11,7 @@ import { configs, test, Viewports } from '@utils/test/playwright';
*/
// Helper to get the modal wrapper's computed padding-bottom
-async function getWrapperPaddingBottom(page: any): Promise {
+async function getWrapperPaddingBottom(page: E2EPage): Promise {
const modal = page.locator('ion-modal');
return modal.evaluate((el: HTMLIonModalElement) => {
const wrapper = el.shadowRoot?.querySelector('.modal-wrapper');
@@ -20,7 +21,7 @@ async function getWrapperPaddingBottom(page: any): Promise {
}
// Helper to check if modal has a footer
-async function modalHasFooter(page: any): Promise {
+async function modalHasFooter(page: E2EPage): Promise {
const modal = page.locator('ion-modal');
return modal.evaluate((el: HTMLIonModalElement) => {
return el.querySelector('ion-footer') !== null;
@@ -41,7 +42,12 @@ configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, config
await page.goto('/src/components/modal/test/safe-area', config);
});
- test('fullscreen modal without footer should have wrapper padding', async ({ page }) => {
+ test('fullscreen modal without footer should have wrapper padding', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
await page.click('#fullscreen-no-footer');
@@ -55,7 +61,12 @@ configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, config
expect(paddingBottom).toBe('34px');
});
- test('fullscreen modal with footer should not have wrapper padding', async ({ page }) => {
+ test('fullscreen modal with footer should not have wrapper padding', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
await page.click('#fullscreen-modal');
@@ -69,7 +80,12 @@ configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, config
expect(paddingBottom).toBe('0px');
});
- test('default modal without footer should have wrapper padding on phone', async ({ page }) => {
+ test('default modal without footer should have wrapper padding on phone', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
await page.click('#default-no-footer');
@@ -89,7 +105,12 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await page.goto('/src/components/modal/test/safe-area', config);
});
- test('card modal without footer should have wrapper padding on phone', async ({ page }) => {
+ test('card modal without footer should have wrapper padding on phone', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
await page.click('#card-modal-no-footer');
@@ -100,7 +121,12 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
expect(paddingBottom).toBe('34px');
});
- test('card modal with footer should not have wrapper padding', async ({ page }) => {
+ test('card modal with footer should not have wrapper padding', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
await page.click('#card-modal');
@@ -123,7 +149,12 @@ configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, config
await page.goto('/src/components/modal/test/safe-area', config);
});
- test('default modal should not have wrapper padding on tablet', async ({ page }) => {
+ test('default modal should not have wrapper padding on tablet', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
await page.click('#default-modal');
@@ -134,7 +165,12 @@ configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, config
expect(paddingBottom).toBe('0px');
});
- test('fullscreen modal without footer should have wrapper padding on tablet', async ({ page }) => {
+ test('fullscreen modal without footer should have wrapper padding on tablet', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
await page.click('#fullscreen-no-footer');
@@ -144,7 +180,12 @@ configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, config
expect(paddingBottom).toBe('34px');
});
- test('fullscreen modal with footer should not have wrapper padding', async ({ page }) => {
+ test('fullscreen modal with footer should not have wrapper padding', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
await page.click('#fullscreen-modal');
@@ -163,7 +204,12 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await page.goto('/src/components/modal/test/safe-area', config);
});
- test('card modal should not have wrapper padding on tablet', async ({ page }) => {
+ test('card modal should not have wrapper padding on tablet', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
await page.click('#card-modal');
@@ -187,7 +233,12 @@ configs({ modes: ['ios', 'md'], directions: ['ltr'] }).forEach(({ title, config
await page.goto('/src/components/modal/test/safe-area', config);
});
- test('sheet modal should not have wrapper padding (footer handles safe-area)', async ({ page }) => {
+ test('sheet modal should not have wrapper padding (footer handles safe-area)', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
+ });
+
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
await page.click('#sheet-modal-full');
diff --git a/core/src/components/popover/animations/md.enter.ts b/core/src/components/popover/animations/md.enter.ts
index 31ec53ec07..e8a1e1adc5 100644
--- a/core/src/components/popover/animations/md.enter.ts
+++ b/core/src/components/popover/animations/md.enter.ts
@@ -47,7 +47,17 @@ export const mdEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
const padding = size === 'cover' ? 0 : POPOVER_MD_BODY_PADDING;
- const { originX, originY, top, left, bottom, checkSafeAreaTop, checkSafeAreaBottom } = calculateWindowAdjustment(
+ const {
+ originX,
+ originY,
+ top,
+ left,
+ bottom,
+ checkSafeAreaTop,
+ checkSafeAreaBottom,
+ checkSafeAreaLeft,
+ checkSafeAreaRight,
+ } = calculateWindowAdjustment(
side,
results.top,
results.left,
@@ -70,9 +80,12 @@ export const mdEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
*/
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}`;
@@ -80,6 +93,12 @@ export const mdEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
if (checkSafeAreaBottom && bottomValue !== undefined) {
bottomValue = `${bottom}px${safeAreaBottom}`;
}
+ if (checkSafeAreaLeft) {
+ leftValue = `${left}px${safeAreaLeft}`;
+ }
+ if (checkSafeAreaRight) {
+ leftValue = `${left}px${safeAreaRight}`;
+ }
const baseAnimation = createAnimation();
const backdropAnimation = createAnimation();
@@ -101,7 +120,7 @@ export const mdEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
.addElement(contentEl)
.beforeStyles({
top: `calc(${topValue} + var(--offset-y, 0px))`,
- left: `calc(${left}px + var(--offset-x, 0px))`,
+ left: `calc(${leftValue} + var(--offset-x, 0px))`,
'transform-origin': `${originY} ${originX}`,
})
.beforeAddWrite(() => {
diff --git a/core/src/components/popover/test/safe-area/popover.e2e.ts b/core/src/components/popover/test/safe-area/popover.e2e.ts
index b8d4272f87..13658a256d 100644
--- a/core/src/components/popover/test/safe-area/popover.e2e.ts
+++ b/core/src/components/popover/test/safe-area/popover.e2e.ts
@@ -9,7 +9,8 @@ import { configs, test } from '@utils/test/playwright';
* is enforced and apps can no longer opt out.
*/
-configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+// Tests that apply to both iOS and MD modes
+configs({ modes: ['ios', 'md'], 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);
@@ -41,6 +42,15 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
// This ensures the popover is positioned above the unsafe area
expect(bottomStyle).toContain('var(--ion-safe-area-bottom');
});
+ });
+});
+
+// iOS-specific tests
+configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => {
+ test.describe(title('popover: safe-area positioning - ios specific'), () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/src/components/popover/test/safe-area', config);
+ });
test('floating popover should not have safe-area adjustments', async ({ page }) => {
const ionPopoverDidPresent = await page.spyOnEvent('ionPopoverDidPresent');
@@ -64,36 +74,3 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
});
});
});
-
-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');
- });
- });
-});