From 99dcf3810a0c32416996d1e992ddf63359965cfc Mon Sep 17 00:00:00 2001 From: Brandy Smith Date: Wed, 10 Dec 2025 15:56:59 -0500 Subject: [PATCH] fix(popover): recalculate the content dimensions after the header has fully loaded (#30853) Issue number: internal --------- ## What is the current behavior? A translucent header in a popover does not consistently render as translucent upon presenting due to the `offset-top` of the content being set to `0`. ## What is the new behavior? Watch the header for height changes using `ResizeObserver` and recalculate the content dimensions when the header height is greater than `0`. ## Does this introduce a breaking change? - [ ] Yes - [x] No --------- Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com> --- core/src/components.d.ts | 4 +++ core/src/components/content/content.tsx | 11 +++++++ core/src/components/popover/popover.tsx | 41 +++++++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 7721801eb6..5e47667392 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -868,6 +868,10 @@ export namespace Components { * Get the element where the actual scrolling takes place. This element can be used to subscribe to `scroll` events or manually modify `scrollTop`. However, it's recommended to use the API provided by `ion-content`: i.e. Using `ionScroll`, `ionScrollStart`, `ionScrollEnd` for scrolling events and `scrollToPoint()` to scroll the content into a certain point. */ "getScrollElement": () => Promise; + /** + * Recalculate content dimensions. Called by overlays (e.g., popover) when sibling elements like headers or footers have finished rendering and their heights are available, ensuring accurate offset-top calculations. + */ + "recalculateDimensions": () => Promise; /** * Scroll by a specified X/Y distance in the component. * @param x The amount to scroll by on the horizontal axis. diff --git a/core/src/components/content/content.tsx b/core/src/components/content/content.tsx index 74e44c6359..361939f7c2 100644 --- a/core/src/components/content/content.tsx +++ b/core/src/components/content/content.tsx @@ -254,6 +254,17 @@ export class Content implements ComponentInterface { } } + /** + * Recalculate content dimensions. Called by overlays (e.g., popover) when + * sibling elements like headers or footers have finished rendering and their + * heights are available, ensuring accurate offset-top calculations. + * @internal + */ + @Method() + async recalculateDimensions(): Promise { + readTask(() => this.readDimensions()); + } + private readDimensions() { const page = getPageElement(this.el); const top = Math.max(this.el.offsetTop, 0); diff --git a/core/src/components/popover/popover.tsx b/core/src/components/popover/popover.tsx index 9b07ab2f27..af0e613297 100644 --- a/core/src/components/popover/popover.tsx +++ b/core/src/components/popover/popover.tsx @@ -64,6 +64,7 @@ export class Popover implements ComponentInterface, PopoverInterface { private destroyTriggerInteraction?: () => void; private destroyKeyboardInteraction?: () => void; private destroyDismissInteraction?: () => void; + private headerResizeObserver?: ResizeObserver; private inline = false; private workingDelegate?: FrameworkDelegate; @@ -361,6 +362,11 @@ export class Popover implements ComponentInterface, PopoverInterface { if (destroyTriggerInteraction) { destroyTriggerInteraction(); } + + if (this.headerResizeObserver) { + this.headerResizeObserver.disconnect(); + this.headerResizeObserver = undefined; + } } componentWillLoad() { @@ -491,6 +497,8 @@ export class Popover implements ComponentInterface, PopoverInterface { inline ); + this.recalculateContentOnHeaderReady(); + if (!this.keyboardEvents) { this.configureKeyboardInteraction(); } @@ -540,6 +548,39 @@ export class Popover implements ComponentInterface, PopoverInterface { unlock(); } + /** + * Watch the header for height changes and trigger content dimension + * recalculation when the header has a height > 0. This sets the offset-top + * of the content to the height of the header correctly. + */ + private recalculateContentOnHeaderReady() { + const popoverContent = this.el.shadowRoot?.querySelector('.popover-content'); + if (!popoverContent) { + return; + } + + const contentContainer = this.usersElement || popoverContent; + + const header = contentContainer.querySelector('ion-header') as HTMLElement | null; + const contentElements = contentContainer.querySelectorAll('ion-content'); + + if (!header || contentElements.length === 0) { + return; + } + + this.headerResizeObserver = new ResizeObserver(async () => { + if (header.offsetHeight > 0) { + this.headerResizeObserver?.disconnect(); + this.headerResizeObserver = undefined; + for (const contentEl of contentElements) { + await contentEl.recalculateDimensions(); + } + } + }); + + this.headerResizeObserver.observe(header); + } + /** * Dismiss the popover overlay after it has been presented. * This is a no-op if the overlay has not been presented yet. If you want