Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6976,6 +6976,7 @@ declare namespace LocalJSX {
*/
"mode"?: "ios" | "md";
"onIonTabBarChanged"?: (event: IonTabBarCustomEvent<TabBarChangedEventDetail>) => void;
"onIonTabBarLoaded"?: (event: IonTabBarCustomEvent<void>) => void;
/**
* The selected tab component
*/
Expand Down
59 changes: 54 additions & 5 deletions core/src/components/content/content.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -34,6 +34,9 @@ export class Content implements ComponentInterface {
private isMainContent = true;
private resizeTimeout: ReturnType<typeof setTimeout> | 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
Expand Down Expand Up @@ -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' })

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

appload does not always run after all components have been hydrated, so this listener wasn't actually helping at all.

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;
}
}

/**
Expand Down
12 changes: 12 additions & 0 deletions core/src/components/tab-bar/tab-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ export class TabBar implements ComponentInterface {
/** @internal */
@Event() ionTabBarChanged!: EventEmitter<TabBarChangedEventDetail>;

/**
* @internal
* This event is used in IonContent to correctly
* calculate the fullscreen content offsets
* when IonTabBar is used.
*/
@Event() ionTabBarLoaded!: EventEmitter<void>;

componentWillLoad() {
this.selectedTabChanged();
}
Expand All @@ -82,6 +90,10 @@ export class TabBar implements ComponentInterface {
}
}

componentDidLoad() {
this.ionTabBarLoaded.emit();
}

render() {
const { color, translucent, keyboardVisible } = this;
const mode = getIonMode(this);
Expand Down