mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-06 22:29:44 +08:00
fix(content): fullscreen offset is computed correctly with tab bar (#28245)
Issue number: resolves #21130 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> IonContent sets `--offset-top` and `--offset-bottom` variables to allow the content to scroll under headers, footers, and tab bars. This is essential to creating the translucency effect on these components. IonContent does this by computing its offsetHeight and offsetTop coordinates which take into account the dimensions of headers, footers, and tab bars. Occasionally, this code will run before the IonTabBar has been hydrated which means that the offset will be wrong because the IonTabBar will have a dimension of 0x0 prior to hydration. This impacts Ionic Angular devs who are using the lazy loaded build of Ionic. React and Vue devs are not impacted because they are using the dist-custom-elements build of Ionic which does not have hydration. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - IonContent will re-run the offset computation code whenever the `ionTabBarLoaded` event is emitted. This event is emitted at most once per IonTabBar instance. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> Dev build: `7.4.2-dev.11695831341.191bdf12` Note: I did not write a test since this is fixing a race condition. I wasn't able to find a non-flaky way of testing this. You can test this in an Ionic Angular Tabs starter application with the dev build. The `--offset-bottom` variable on `ion-content` should be large enough such that the content will scroll under the tab bar. The translucency effect won't work just yet, but that is being fixed in https://github.com/ionic-team/ionic-framework/pull/28246.
This commit is contained in:
1
core/src/components.d.ts
vendored
1
core/src/components.d.ts
vendored
@ -6976,6 +6976,7 @@ declare namespace LocalJSX {
|
|||||||
*/
|
*/
|
||||||
"mode"?: "ios" | "md";
|
"mode"?: "ios" | "md";
|
||||||
"onIonTabBarChanged"?: (event: IonTabBarCustomEvent<TabBarChangedEventDetail>) => void;
|
"onIonTabBarChanged"?: (event: IonTabBarCustomEvent<TabBarChangedEventDetail>) => void;
|
||||||
|
"onIonTabBarLoaded"?: (event: IonTabBarCustomEvent<void>) => void;
|
||||||
/**
|
/**
|
||||||
* The selected tab component
|
* The selected tab component
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
import type { ComponentInterface, EventEmitter } from '@stencil/core';
|
||||||
import { Build, Component, Element, Event, Host, Listen, Method, Prop, forceUpdate, h, readTask } 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 { isPlatform } from '@utils/platform';
|
||||||
import { isRTL } from '@utils/rtl';
|
import { isRTL } from '@utils/rtl';
|
||||||
import { createColorClasses, hostContext } from '@utils/theme';
|
import { createColorClasses, hostContext } from '@utils/theme';
|
||||||
@ -34,6 +34,9 @@ export class Content implements ComponentInterface {
|
|||||||
private isMainContent = true;
|
private isMainContent = true;
|
||||||
private resizeTimeout: ReturnType<typeof setTimeout> | null = null;
|
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
|
// 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.
|
// 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
|
// https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html
|
||||||
@ -115,15 +118,61 @@ export class Content implements ComponentInterface {
|
|||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.isMainContent = this.el.closest('ion-menu, ion-popover, ion-modal') === null;
|
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() {
|
disconnectedCallback() {
|
||||||
this.onScrollEnd();
|
this.onScrollEnd();
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Listen('appload', { target: 'window' })
|
this.tabsElement = null;
|
||||||
onAppLoad() {
|
this.tabsLoadCallback = undefined;
|
||||||
this.resize();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -57,6 +57,14 @@ export class TabBar implements ComponentInterface {
|
|||||||
/** @internal */
|
/** @internal */
|
||||||
@Event() ionTabBarChanged!: EventEmitter<TabBarChangedEventDetail>;
|
@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() {
|
componentWillLoad() {
|
||||||
this.selectedTabChanged();
|
this.selectedTabChanged();
|
||||||
}
|
}
|
||||||
@ -82,6 +90,10 @@ export class TabBar implements ComponentInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidLoad() {
|
||||||
|
this.ionTabBarLoaded.emit();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { color, translucent, keyboardVisible } = this;
|
const { color, translucent, keyboardVisible } = this;
|
||||||
const mode = getIonMode(this);
|
const mode = getIonMode(this);
|
||||||
|
|||||||
Reference in New Issue
Block a user