mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Reverting changes to be focused on modal and popover safe area support
This commit is contained in:
@@ -235,38 +235,6 @@
|
||||
}
|
||||
|
||||
|
||||
// Content: Safe Area
|
||||
// --------------------------------------------------
|
||||
// When content has no sibling header, offset from top safe-area.
|
||||
// When content has no sibling footer/tab-bar, offset from bottom safe-area.
|
||||
// Left/right safe-areas always apply to main content (for landscape notched devices).
|
||||
// This prevents content from overlapping device safe areas (status bar, nav bar, notch).
|
||||
|
||||
:host(.safe-area-top) #background-content,
|
||||
:host(.safe-area-top) .inner-scroll {
|
||||
top: var(--ion-safe-area-top, 0px);
|
||||
}
|
||||
|
||||
:host(.safe-area-bottom) #background-content,
|
||||
:host(.safe-area-bottom) .inner-scroll {
|
||||
bottom: var(--ion-safe-area-bottom, 0px);
|
||||
}
|
||||
|
||||
:host(.safe-area-left) #background-content,
|
||||
:host(.safe-area-left) .inner-scroll {
|
||||
/* stylelint-disable property-disallowed-list */
|
||||
left: var(--ion-safe-area-left, 0px);
|
||||
/* stylelint-enable property-disallowed-list */
|
||||
}
|
||||
|
||||
:host(.safe-area-right) #background-content,
|
||||
:host(.safe-area-right) .inner-scroll {
|
||||
/* stylelint-disable property-disallowed-list */
|
||||
right: var(--ion-safe-area-right, 0px);
|
||||
/* stylelint-enable property-disallowed-list */
|
||||
}
|
||||
|
||||
|
||||
// Content: Fixed
|
||||
// --------------------------------------------------
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||
import { Build, Component, Element, Event, Host, Listen, Method, Prop, forceUpdate, h, readTask } from '@stencil/core';
|
||||
import { win } from '@utils/browser';
|
||||
import { componentOnReady, hasLazyBuild, inheritAriaAttributes } from '@utils/helpers';
|
||||
import type { Attributes } from '@utils/helpers';
|
||||
import { isPlatform } from '@utils/platform';
|
||||
@@ -37,19 +36,6 @@ export class Content implements ComponentInterface {
|
||||
private resizeTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
private inheritedAttributes: Attributes = {};
|
||||
|
||||
/**
|
||||
* Track whether this content has sibling header/footer elements.
|
||||
* When absent, we need to apply safe-area padding directly.
|
||||
*/
|
||||
private hasHeader = false;
|
||||
private hasFooter = false;
|
||||
|
||||
/** Watches for dynamic header/footer changes in parent element */
|
||||
private parentMutationObserver?: MutationObserver;
|
||||
|
||||
/** Watches for dynamic tab bar changes in ion-tabs */
|
||||
private tabsMutationObserver?: MutationObserver;
|
||||
|
||||
private tabsElement: HTMLElement | null = null;
|
||||
private tabsLoadCallback?: () => void;
|
||||
|
||||
@@ -146,13 +132,7 @@ export class Content implements ComponentInterface {
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// Content is "main" if not inside menu/popover/modal and not nested in another ion-content
|
||||
this.isMainContent =
|
||||
this.el.closest('ion-menu, ion-popover, ion-modal') === null &&
|
||||
this.el.parentElement?.closest('ion-content') === null;
|
||||
|
||||
// Detect sibling header/footer for safe-area handling
|
||||
this.detectSiblingElements();
|
||||
this.isMainContent = this.el.closest('ion-menu, ion-popover, ion-modal') === null;
|
||||
|
||||
/**
|
||||
* The fullscreen content offsets need to be
|
||||
@@ -184,109 +164,15 @@ export class Content implements ComponentInterface {
|
||||
* bubbles, we can catch any instances of child tab bars loading by listening
|
||||
* on IonTabs.
|
||||
*/
|
||||
this.tabsLoadCallback = () => {
|
||||
this.resize();
|
||||
// Re-detect footer when tab bar loads (it may not exist during initial detection)
|
||||
this.updateSiblingDetection();
|
||||
forceUpdate(this);
|
||||
};
|
||||
this.tabsLoadCallback = () => this.resize();
|
||||
closestTabs.addEventListener('ionTabBarLoaded', this.tabsLoadCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects sibling ion-header and ion-footer elements and sets up
|
||||
* a mutation observer to handle dynamic changes (e.g., conditional rendering).
|
||||
*/
|
||||
private detectSiblingElements() {
|
||||
this.updateSiblingDetection();
|
||||
|
||||
// Watch for dynamic header/footer changes (common in React conditional rendering)
|
||||
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();
|
||||
// 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 });
|
||||
}
|
||||
|
||||
// Watch for dynamic tab bar changes in ion-tabs (common in Angular conditional rendering)
|
||||
const tabs = this.el.closest('ion-tabs');
|
||||
if (tabs && !this.tabsMutationObserver && win !== undefined && 'MutationObserver' in win) {
|
||||
this.tabsMutationObserver = new MutationObserver(() => {
|
||||
const prevHasFooter = this.hasFooter;
|
||||
this.updateSiblingDetection();
|
||||
// Only trigger re-render if footer detection actually changed
|
||||
if (prevHasFooter !== this.hasFooter) {
|
||||
forceUpdate(this);
|
||||
}
|
||||
});
|
||||
this.tabsMutationObserver.observe(tabs, { childList: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates hasHeader/hasFooter based on current DOM state.
|
||||
* Checks both direct siblings and elements wrapped in custom components
|
||||
* (e.g., <my-header><ion-header>...</ion-header></my-header>).
|
||||
*/
|
||||
private updateSiblingDetection() {
|
||||
const parent = this.el.parentElement;
|
||||
if (parent) {
|
||||
// First check for direct ion-header/ion-footer siblings
|
||||
this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
|
||||
this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
|
||||
|
||||
// If not found, check if any sibling contains them (wrapped components)
|
||||
if (!this.hasHeader) {
|
||||
this.hasHeader = this.siblingContainsElement(parent, 'ion-header');
|
||||
}
|
||||
if (!this.hasFooter) {
|
||||
this.hasFooter = this.siblingContainsElement(parent, 'ion-footer');
|
||||
}
|
||||
}
|
||||
|
||||
// If no footer found, check if we're inside ion-tabs which has ion-tab-bar
|
||||
if (!this.hasFooter) {
|
||||
const tabs = this.el.closest('ion-tabs');
|
||||
if (tabs) {
|
||||
this.hasFooter = tabs.querySelector(':scope > ion-tab-bar') !== null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any sibling element of ion-content contains the specified element.
|
||||
* Only searches one level deep to avoid finding elements in nested pages.
|
||||
*/
|
||||
private siblingContainsElement(parent: Element, tagName: string): boolean {
|
||||
for (const sibling of parent.children) {
|
||||
// Skip ion-content itself
|
||||
if (sibling === this.el) continue;
|
||||
// Check if this sibling contains the target element as an immediate child
|
||||
if (sibling.querySelector(`:scope > ${tagName}`) !== null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.onScrollEnd();
|
||||
|
||||
// Clean up mutation observers to prevent memory leaks
|
||||
this.parentMutationObserver?.disconnect();
|
||||
this.parentMutationObserver = undefined;
|
||||
this.tabsMutationObserver?.disconnect();
|
||||
this.tabsMutationObserver = undefined;
|
||||
|
||||
if (hasLazyBuild(this.el)) {
|
||||
/**
|
||||
* The event listener and tabs caches need to
|
||||
@@ -563,7 +449,7 @@ export class Content implements ComponentInterface {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { fixedSlotPlacement, hasFooter, hasHeader, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
|
||||
const { fixedSlotPlacement, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
|
||||
const rtl = isRTL(el) ? 'rtl' : 'ltr';
|
||||
const mode = getIonMode(this);
|
||||
const forceOverscroll = this.shouldForceOverscroll();
|
||||
@@ -579,10 +465,6 @@ export class Content implements ComponentInterface {
|
||||
'content-sizing': hostContext('ion-popover', this.el),
|
||||
overscroll: forceOverscroll,
|
||||
[`content-${rtl}`]: true,
|
||||
'safe-area-top': isMainContent && !hasHeader,
|
||||
'safe-area-bottom': isMainContent && !hasFooter,
|
||||
'safe-area-left': isMainContent,
|
||||
'safe-area-right': isMainContent,
|
||||
})}
|
||||
style={{
|
||||
'--offset-top': `${this.cTop}px`,
|
||||
|
||||
@@ -1,270 +0,0 @@
|
||||
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.
|
||||
*
|
||||
* Safe-area class logic:
|
||||
* - safe-area-top: main content without header
|
||||
* - safe-area-bottom: main content without footer/tab-bar
|
||||
* - safe-area-left: always on main content (for landscape notched devices)
|
||||
* - safe-area-right: always on main content (for landscape notched devices)
|
||||
*
|
||||
* 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/);
|
||||
// Left/right always apply to main content
|
||||
await expect(content).toHaveClass(/safe-area-left/);
|
||||
await expect(content).toHaveClass(/safe-area-right/);
|
||||
});
|
||||
|
||||
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/);
|
||||
// Left/right always apply to main content
|
||||
await expect(content).toHaveClass(/safe-area-left/);
|
||||
await expect(content).toHaveClass(/safe-area-right/);
|
||||
});
|
||||
|
||||
test('content with both header and footer should not have top/bottom 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/);
|
||||
// Left/right still apply to main content even with header/footer
|
||||
await expect(content).toHaveClass(/safe-area-left/);
|
||||
await expect(content).toHaveClass(/safe-area-right/);
|
||||
});
|
||||
|
||||
test('content without header or footer should have all 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/);
|
||||
await expect(content).toHaveClass(/safe-area-left/);
|
||||
await expect(content).toHaveClass(/safe-area-right/);
|
||||
});
|
||||
|
||||
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/);
|
||||
// Left/right still apply to main content
|
||||
await expect(content).toHaveClass(/safe-area-left/);
|
||||
await expect(content).toHaveClass(/safe-area-right/);
|
||||
});
|
||||
|
||||
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/);
|
||||
// Left/right still apply to main content
|
||||
await expect(content).toHaveClass(/safe-area-left/);
|
||||
await expect(content).toHaveClass(/safe-area-right/);
|
||||
});
|
||||
|
||||
test('nested content should not have any 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 - no safe-area classes at all
|
||||
await expect(nestedContent).not.toHaveClass(/safe-area-top/);
|
||||
await expect(nestedContent).not.toHaveClass(/safe-area-bottom/);
|
||||
await expect(nestedContent).not.toHaveClass(/safe-area-left/);
|
||||
await expect(nestedContent).not.toHaveClass(/safe-area-right/);
|
||||
});
|
||||
|
||||
test('outer content should have all 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 all safe-area classes
|
||||
await expect(outerContent).toHaveClass(/safe-area-top/);
|
||||
await expect(outerContent).toHaveClass(/safe-area-bottom/);
|
||||
await expect(outerContent).toHaveClass(/safe-area-left/);
|
||||
await expect(outerContent).toHaveClass(/safe-area-right/);
|
||||
});
|
||||
|
||||
test('content inside modal should not have any safe-area classes', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://outsystemsrd.atlassian.net/browse/FW-6830',
|
||||
});
|
||||
|
||||
// Set up event spy BEFORE opening modal
|
||||
const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent');
|
||||
|
||||
// Open the modal
|
||||
await page.evaluate(() => {
|
||||
const modal = document.getElementById('test-modal') as HTMLIonModalElement;
|
||||
modal.isOpen = true;
|
||||
});
|
||||
|
||||
// Wait for modal to be presented
|
||||
await ionModalDidPresent.next();
|
||||
|
||||
const modalContent = page.locator('#content-in-modal');
|
||||
// Content inside modal should not be treated as main content - no safe-area classes at all
|
||||
await expect(modalContent).not.toHaveClass(/safe-area-top/);
|
||||
await expect(modalContent).not.toHaveClass(/safe-area-bottom/);
|
||||
await expect(modalContent).not.toHaveClass(/safe-area-left/);
|
||||
await expect(modalContent).not.toHaveClass(/safe-area-right/);
|
||||
});
|
||||
|
||||
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) and left/right (always on main content)
|
||||
await expect(content).toHaveClass(/safe-area-top/);
|
||||
await expect(content).toHaveClass(/safe-area-left/);
|
||||
await expect(content).toHaveClass(/safe-area-right/);
|
||||
|
||||
// Add header dynamically (use evaluate to avoid pointer-events issues in Firefox)
|
||||
await page.evaluate(() => (window as any).addHeader());
|
||||
|
||||
// Wait for mutation observer to trigger and component to update
|
||||
await expect(content).not.toHaveClass(/safe-area-top/, { timeout: 1000 });
|
||||
// Left/right should remain
|
||||
await expect(content).toHaveClass(/safe-area-left/);
|
||||
await expect(content).toHaveClass(/safe-area-right/);
|
||||
});
|
||||
|
||||
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 (use evaluate to avoid pointer-events issues in Firefox)
|
||||
await page.evaluate(() => (window as any).addHeader());
|
||||
await expect(content).not.toHaveClass(/safe-area-top/, { timeout: 1000 });
|
||||
// Left/right should remain throughout
|
||||
await expect(content).toHaveClass(/safe-area-left/);
|
||||
await expect(content).toHaveClass(/safe-area-right/);
|
||||
|
||||
// Remove header
|
||||
await page.evaluate(() => (window as any).removeHeader());
|
||||
|
||||
// Should have safe-area-top again, left/right should remain
|
||||
await expect(content).toHaveClass(/safe-area-top/, { timeout: 1000 });
|
||||
await expect(content).toHaveClass(/safe-area-left/);
|
||||
await expect(content).toHaveClass(/safe-area-right/);
|
||||
});
|
||||
|
||||
test('content inside ion-tabs with tab bar should not have safe-area-bottom', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/30900',
|
||||
});
|
||||
|
||||
const content = page.locator('#content-dynamic-tabs');
|
||||
// Tab bar is present, so content should not have safe-area-bottom
|
||||
await expect(content).not.toHaveClass(/safe-area-bottom/);
|
||||
// But left/right should still apply (main content)
|
||||
await expect(content).toHaveClass(/safe-area-left/);
|
||||
await expect(content).toHaveClass(/safe-area-right/);
|
||||
});
|
||||
|
||||
test('dynamic tab bar removal should update safe-area classes', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/30900',
|
||||
});
|
||||
|
||||
const content = page.locator('#content-dynamic-tabs');
|
||||
|
||||
// Initially tab bar is present, so no safe-area-bottom
|
||||
await expect(content).not.toHaveClass(/safe-area-bottom/);
|
||||
// Left/right should be present throughout
|
||||
await expect(content).toHaveClass(/safe-area-left/);
|
||||
await expect(content).toHaveClass(/safe-area-right/);
|
||||
|
||||
// Remove tab bar
|
||||
await page.evaluate(() => (window as any).removeTabBar());
|
||||
|
||||
// Should have safe-area-bottom now, left/right remain
|
||||
await expect(content).toHaveClass(/safe-area-bottom/, { timeout: 1000 });
|
||||
await expect(content).toHaveClass(/safe-area-left/);
|
||||
await expect(content).toHaveClass(/safe-area-right/);
|
||||
});
|
||||
|
||||
test('dynamic tab bar addition should update safe-area classes', async ({ page }, testInfo) => {
|
||||
testInfo.annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/ionic-team/ionic-framework/issues/30900',
|
||||
});
|
||||
|
||||
const content = page.locator('#content-dynamic-tabs');
|
||||
|
||||
// Remove tab bar first
|
||||
await page.evaluate(() => (window as any).removeTabBar());
|
||||
await expect(content).toHaveClass(/safe-area-bottom/, { timeout: 1000 });
|
||||
// Left/right should be present throughout
|
||||
await expect(content).toHaveClass(/safe-area-left/);
|
||||
await expect(content).toHaveClass(/safe-area-right/);
|
||||
|
||||
// Add tab bar back
|
||||
await page.evaluate(() => (window as any).addTabBar());
|
||||
|
||||
// Should not have safe-area-bottom anymore, left/right remain
|
||||
await expect(content).not.toHaveClass(/safe-area-bottom/, { timeout: 1000 });
|
||||
await expect(content).toHaveClass(/safe-area-left/);
|
||||
await expect(content).toHaveClass(/safe-area-right/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,227 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Content - Safe Area</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="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 (typical phone in landscape) */
|
||||
:root {
|
||||
--ion-safe-area-top: 44px;
|
||||
--ion-safe-area-bottom: 34px;
|
||||
--ion-safe-area-left: 44px;
|
||||
--ion-safe-area-right: 44px;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
border: 2px solid #ccc;
|
||||
margin: 10px;
|
||||
height: 200px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.test-section .ion-page {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Custom wrapper component for testing wrapped header/footer detection */
|
||||
my-header,
|
||||
my-footer {
|
||||
display: contents;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ion-app>
|
||||
<!-- Test 1: Content without header - should have safe-area-top -->
|
||||
<div id="test-no-header" class="test-section">
|
||||
<div class="ion-page">
|
||||
<ion-content id="content-no-header">
|
||||
<p>Content without header - should have safe-area-top class</p>
|
||||
</ion-content>
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-title>Footer</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test 2: Content without footer - should have safe-area-bottom -->
|
||||
<div id="test-no-footer" class="test-section">
|
||||
<div class="ion-page">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Header</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content id="content-no-footer">
|
||||
<p>Content without footer - should have safe-area-bottom class</p>
|
||||
</ion-content>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test 3: Content with both header and footer - should NOT have safe-area classes -->
|
||||
<div id="test-with-both" class="test-section">
|
||||
<div class="ion-page">
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Header</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content id="content-with-both">
|
||||
<p>Content with both header and footer - should NOT have safe-area classes</p>
|
||||
</ion-content>
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-title>Footer</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test 4: Content without both header and footer - should have both safe-area classes -->
|
||||
<div id="test-no-both" class="test-section">
|
||||
<div class="ion-page">
|
||||
<ion-content id="content-no-both">
|
||||
<p>Content without header or footer - should have both safe-area classes</p>
|
||||
</ion-content>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test 5: Content with wrapped header component - should NOT have safe-area-top -->
|
||||
<div id="test-wrapped-header" class="test-section">
|
||||
<div class="ion-page">
|
||||
<my-header>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Wrapped Header</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
</my-header>
|
||||
<ion-content id="content-wrapped-header">
|
||||
<p>Content with wrapped header - should NOT have safe-area-top class</p>
|
||||
</ion-content>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test 6: Content with wrapped footer component - should NOT have safe-area-bottom -->
|
||||
<div id="test-wrapped-footer" class="test-section">
|
||||
<div class="ion-page">
|
||||
<ion-content id="content-wrapped-footer">
|
||||
<p>Content with wrapped footer - should NOT have safe-area-bottom class</p>
|
||||
</ion-content>
|
||||
<my-footer>
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-title>Wrapped Footer</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
</my-footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test 7: Nested content - should NOT have safe-area classes (inner content) -->
|
||||
<div id="test-nested" class="test-section">
|
||||
<div class="ion-page">
|
||||
<ion-content id="content-outer">
|
||||
<p>Outer content</p>
|
||||
<div style="height: 100px; border: 1px solid blue">
|
||||
<ion-content id="content-nested">
|
||||
<p>Nested content - should NOT have safe-area classes</p>
|
||||
</ion-content>
|
||||
</div>
|
||||
</ion-content>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test 8: Content inside modal - should NOT have safe-area classes -->
|
||||
<ion-modal id="test-modal" is-open="false">
|
||||
<ion-content id="content-in-modal">
|
||||
<p>Content inside modal - should NOT have safe-area classes</p>
|
||||
</ion-content>
|
||||
</ion-modal>
|
||||
|
||||
<!-- Test 9: Dynamic header/footer - for testing mutation observer -->
|
||||
<div id="test-dynamic" class="test-section">
|
||||
<div class="ion-page" id="dynamic-page">
|
||||
<ion-content id="content-dynamic">
|
||||
<p>Content with dynamic header/footer</p>
|
||||
<button id="add-header-btn" onclick="addHeader()">Add Header</button>
|
||||
<button id="remove-header-btn" onclick="removeHeader()">Remove Header</button>
|
||||
</ion-content>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test 10: Dynamic tab bar - for testing ion-tabs mutation observer -->
|
||||
<div id="test-dynamic-tabs" class="test-section">
|
||||
<ion-tabs id="dynamic-tabs">
|
||||
<div class="ion-page" id="dynamic-tabs-page">
|
||||
<ion-content id="content-dynamic-tabs">
|
||||
<p>Content with dynamic tab bar</p>
|
||||
</ion-content>
|
||||
</div>
|
||||
<ion-tab-bar id="dynamic-tab-bar" slot="bottom">
|
||||
<ion-tab-button tab="tab1">
|
||||
<ion-label>Tab 1</ion-label>
|
||||
</ion-tab-button>
|
||||
</ion-tab-bar>
|
||||
</ion-tabs>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Expose functions globally for e2e tests
|
||||
window.addHeader = function addHeader() {
|
||||
const page = document.getElementById('dynamic-page');
|
||||
const content = document.getElementById('content-dynamic');
|
||||
if (!page.querySelector('ion-header')) {
|
||||
const header = document.createElement('ion-header');
|
||||
header.innerHTML = '<ion-toolbar><ion-title>Dynamic Header</ion-title></ion-toolbar>';
|
||||
page.insertBefore(header, content);
|
||||
}
|
||||
};
|
||||
|
||||
window.removeHeader = function removeHeader() {
|
||||
const page = document.getElementById('dynamic-page');
|
||||
const header = page.querySelector('ion-header');
|
||||
if (header) {
|
||||
header.remove();
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to open modal for testing
|
||||
window.openModal = function openModal() {
|
||||
document.getElementById('test-modal').isOpen = true;
|
||||
};
|
||||
|
||||
window.addTabBar = function addTabBar() {
|
||||
const tabs = document.getElementById('dynamic-tabs');
|
||||
if (!tabs.querySelector('ion-tab-bar')) {
|
||||
const tabBar = document.createElement('ion-tab-bar');
|
||||
tabBar.id = 'dynamic-tab-bar';
|
||||
tabBar.slot = 'bottom';
|
||||
tabBar.innerHTML = '<ion-tab-button tab="tab1"><ion-label>Tab 1</ion-label></ion-tab-button>';
|
||||
tabs.appendChild(tabBar);
|
||||
}
|
||||
};
|
||||
|
||||
window.removeTabBar = function removeTabBar() {
|
||||
const tabBar = document.getElementById('dynamic-tab-bar');
|
||||
if (tabBar) {
|
||||
tabBar.remove();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user