diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 62cfd30764..f4e1bd51b4 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -6976,6 +6976,7 @@ declare namespace LocalJSX { */ "mode"?: "ios" | "md"; "onIonTabBarChanged"?: (event: IonTabBarCustomEvent) => void; + "onIonTabBarLoaded"?: (event: IonTabBarCustomEvent) => void; /** * The selected tab component */ diff --git a/core/src/components/content/content.tsx b/core/src/components/content/content.tsx index f0de55608e..41b5837609 100644 --- a/core/src/components/content/content.tsx +++ b/core/src/components/content/content.tsx @@ -1,6 +1,6 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; import { Build, Component, Element, Event, Host, Listen, Method, Prop, forceUpdate, h, readTask } from '@stencil/core'; -import { componentOnReady } from '@utils/helpers'; +import { componentOnReady, hasLazyBuild } from '@utils/helpers'; import { isPlatform } from '@utils/platform'; import { isRTL } from '@utils/rtl'; import { createColorClasses, hostContext } from '@utils/theme'; @@ -34,6 +34,9 @@ export class Content implements ComponentInterface { private isMainContent = true; private resizeTimeout: ReturnType | null = null; + private tabsElement: HTMLElement | null = null; + private tabsLoadCallback?: () => void; + // Detail is used in a hot loop in the scroll event, by allocating it here // V8 will be able to inline any read/write to it since it's a monomorphic class. // https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html @@ -115,15 +118,61 @@ export class Content implements ComponentInterface { connectedCallback() { this.isMainContent = this.el.closest('ion-menu, ion-popover, ion-modal') === null; + + /** + * The fullscreen content offsets need to be + * computed after the tab bar has loaded. Since + * lazy evaluation means components are not hydrated + * at the same time, we need to wait for the ionTabBarLoaded + * event to fire. This does not impact dist-custom-elements + * because there is no hydration there. + */ + if (hasLazyBuild(this.el)) { + /** + * We need to cache the reference to the tabs. + * If just the content is unmounted then we won't + * be able to query for the closest tabs on disconnectedCallback + * since the content has been removed from the DOM tree. + */ + const closestTabs = (this.tabsElement = this.el.closest('ion-tabs')); + if (closestTabs !== null) { + /** + * When adding and removing the event listener + * we need to make sure we pass the same function reference + * otherwise the event listener will not be removed properly. + * We can't only pass `this.resize` because "this" in the function + * context becomes a reference to IonTabs instead of IonContent. + * + * Additionally, we listen for ionTabBarLoaded on the IonTabs + * instance rather than the IonTabBar instance. It's possible for + * a tab bar to be conditionally rendered/mounted. Since ionTabBarLoaded + * bubbles, we can catch any instances of child tab bars loading by listening + * on IonTabs. + */ + this.tabsLoadCallback = () => this.resize(); + closestTabs.addEventListener('ionTabBarLoaded', this.tabsLoadCallback); + } + } } disconnectedCallback() { this.onScrollEnd(); - } - @Listen('appload', { target: 'window' }) - onAppLoad() { - this.resize(); + if (hasLazyBuild(this.el)) { + /** + * The event listener and tabs caches need to + * be cleared otherwise this will create a memory + * leak where the IonTabs instance can never be + * garbage collected. + */ + const { tabsElement, tabsLoadCallback } = this; + if (tabsElement !== null && tabsLoadCallback !== undefined) { + tabsElement.removeEventListener('ionTabBarLoaded', tabsLoadCallback); + } + + this.tabsElement = null; + this.tabsLoadCallback = undefined; + } } /** diff --git a/core/src/components/tab-bar/tab-bar.tsx b/core/src/components/tab-bar/tab-bar.tsx index 0589b773ad..7dea85e741 100644 --- a/core/src/components/tab-bar/tab-bar.tsx +++ b/core/src/components/tab-bar/tab-bar.tsx @@ -57,6 +57,14 @@ export class TabBar implements ComponentInterface { /** @internal */ @Event() ionTabBarChanged!: EventEmitter; + /** + * @internal + * This event is used in IonContent to correctly + * calculate the fullscreen content offsets + * when IonTabBar is used. + */ + @Event() ionTabBarLoaded!: EventEmitter; + componentWillLoad() { this.selectedTabChanged(); } @@ -82,6 +90,10 @@ export class TabBar implements ComponentInterface { } } + componentDidLoad() { + this.ionTabBarLoaded.emit(); + } + render() { const { color, translucent, keyboardVisible } = this; const mode = getIonMode(this);