fix(content): apply safe-area insets when header/footer absent

This commit is contained in:
ShaneK
2026-01-06 08:41:31 -08:00
parent 7c197c2c99
commit 4fe98a42ff
2 changed files with 51 additions and 1 deletions

View File

@@ -235,6 +235,23 @@
}
// 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.
// This prevents content from overlapping device safe areas (status bar, nav bar).
: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);
}
// Content: Fixed
// --------------------------------------------------

View File

@@ -36,6 +36,13 @@ 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;
private tabsElement: HTMLElement | null = null;
private tabsLoadCallback?: () => void;
@@ -134,6 +141,9 @@ export class Content implements ComponentInterface {
connectedCallback() {
this.isMainContent = this.el.closest('ion-menu, ion-popover, ion-modal') === null;
// Detect sibling header/footer for safe-area handling
this.detectSiblingElements();
/**
* The fullscreen content offsets need to be
* computed after the tab bar has loaded. Since
@@ -170,6 +180,27 @@ export class Content implements ComponentInterface {
}
}
/**
* Detects sibling ion-header and ion-footer elements.
* When these are absent, content needs to handle safe-area padding directly.
*/
private detectSiblingElements() {
// Check parent element for sibling header/footer.
const parent = this.el.parentElement;
if (parent) {
this.hasHeader = parent.querySelector(':scope > ion-header') !== null;
this.hasFooter = parent.querySelector(':scope > ion-footer') !== null;
}
// 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;
}
}
}
disconnectedCallback() {
this.onScrollEnd();
@@ -449,7 +480,7 @@ export class Content implements ComponentInterface {
}
render() {
const { fixedSlotPlacement, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
const { fixedSlotPlacement, hasFooter, hasHeader, inheritedAttributes, isMainContent, scrollX, scrollY, el } = this;
const rtl = isRTL(el) ? 'rtl' : 'ltr';
const mode = getIonMode(this);
const forceOverscroll = this.shouldForceOverscroll();
@@ -465,6 +496,8 @@ 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,
})}
style={{
'--offset-top': `${this.cTop}px`,