Skip to content

Commit f694300

Browse files
committed
feat(tab-bar): Adding hide-on-scroll prop for ionic theme
1 parent 8181725 commit f694300

6 files changed

Lines changed: 118 additions & 6 deletions

File tree

core/api.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2400,6 +2400,7 @@ ion-tab,method,setActive,setActive() => Promise<void>
24002400
ion-tab-bar,shadow
24012401
ion-tab-bar,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
24022402
ion-tab-bar,prop,expand,"compact" | "full",'full',false,false
2403+
ion-tab-bar,prop,hideOnScroll,boolean,false,false,false
24032404
ion-tab-bar,prop,mode,"ios" | "md",undefined,false,false
24042405
ion-tab-bar,prop,selectedTab,string | undefined,undefined,false,false
24052406
ion-tab-bar,prop,shape,"rectangular" | "round" | "soft" | undefined,undefined,false,false

core/src/components.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3970,6 +3970,11 @@ export namespace Components {
39703970
* @default 'full'
39713971
*/
39723972
"expand": 'compact' | 'full';
3973+
/**
3974+
* If `true`, the tab bar will be hidden when the user scrolls down and shown when the user scrolls up. Only applies when the theme is `"ionic"` and `expand` is `"compact"`.
3975+
* @default false
3976+
*/
3977+
"hideOnScroll": boolean;
39733978
/**
39743979
* The mode determines the platform behaviors of the component.
39753980
*/
@@ -10057,6 +10062,11 @@ declare namespace LocalJSX {
1005710062
* @default 'full'
1005810063
*/
1005910064
"expand"?: 'compact' | 'full';
10065+
/**
10066+
* If `true`, the tab bar will be hidden when the user scrolls down and shown when the user scrolls up. Only applies when the theme is `"ionic"` and `expand` is `"compact"`.
10067+
* @default false
10068+
*/
10069+
"hideOnScroll"?: boolean;
1006010070
/**
1006110071
* The mode determines the platform behaviors of the component.
1006210072
*/
@@ -11298,6 +11308,7 @@ declare namespace LocalJSX {
1129811308
interface IonTabBarAttributes {
1129911309
"color": Color;
1130011310
"selectedTab": string;
11311+
"hideOnScroll": boolean;
1130111312
"translucent": boolean;
1130211313
"expand": 'compact' | 'full';
1130311314
"shape": 'soft' | 'round' | 'rectangular';

core/src/components/tab-bar/tab-bar.ionic.scss

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,15 @@
7070

7171
position: absolute;
7272

73+
// stylelint-disable-next-line property-disallowed-list
74+
left: 50%;
75+
7376
align-self: center;
7477

7578
width: fit-content;
7679

80+
transform: translateX(calc(-50%));
81+
7782
contain: content;
7883
}
7984

@@ -85,6 +90,25 @@
8590
bottom: calc(globals.$ion-space-400 + var(--ion-safe-area-bottom, 0));
8691
}
8792

93+
// Tab Bar Hide on Scroll
94+
// --------------------------------------------------
95+
96+
:host(.tab-bar-hide-on-scroll) {
97+
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1);
98+
}
99+
100+
:host(.tab-bar-scroll-hidden) {
101+
transform: translateY(calc(100% + var(--ion-safe-area-bottom, 0))) translateX(calc(-50%));
102+
103+
transition: transform 0.3s cubic-bezier(0.3, 1, 0.1, 1), opacity 0.3s cubic-bezier(0.3, 1, 0.1, 1);
104+
105+
opacity: 0;
106+
}
107+
108+
:host([slot="top"].tab-bar-scroll-hidden) {
109+
transform: translateY(-100%) translateX(calc(-50%));
110+
}
111+
88112
// Tab Bar Translucent
89113
// --------------------------------------------------
90114

core/src/components/tab-bar/tab-bar.tsx

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ComponentInterface, EventEmitter } from '@stencil/core';
2-
import { Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core';
2+
import { Component, Element, Event, Host, Prop, State, Watch, h, readTask, writeTask } from '@stencil/core';
3+
import { findIonContent, getScrollElement } from '@utils/content';
34
import type { KeyboardController } from '@utils/keyboard/keyboard-controller';
45
import { createKeyboardController } from '@utils/keyboard/keyboard-controller';
56
import { createColorClasses } from '@utils/theme';
@@ -26,11 +27,17 @@ export class TabBar implements ComponentInterface {
2627
private keyboardCtrl: KeyboardController | null = null;
2728
private keyboardCtrlPromise: Promise<KeyboardController> | null = null;
2829
private didLoad = false;
30+
private scrollEl?: HTMLElement;
31+
private contentScrollCallback?: () => void;
32+
private lastScrollTop = 0;
33+
private scrollDirectionChangeTop = 0;
2934

3035
@Element() el!: HTMLElement;
3136

3237
@State() keyboardVisible = false;
3338

39+
@State() scrollHidden = false;
40+
3441
/**
3542
* The color to use from your application's color palette.
3643
* Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`.
@@ -57,6 +64,13 @@ export class TabBar implements ComponentInterface {
5764
}
5865
}
5966

67+
/**
68+
* If `true`, the tab bar will be hidden when the user scrolls down
69+
* and shown when the user scrolls up.
70+
* Only applies when the theme is `"ionic"` and `expand` is `"compact"`.
71+
*/
72+
@Prop() hideOnScroll = false;
73+
6074
/**
6175
* If `true`, the tab bar will be translucent.
6276
* Only applies when the theme is `"ios"` and the device supports
@@ -108,6 +122,8 @@ export class TabBar implements ComponentInterface {
108122
tab: this.selectedTab,
109123
});
110124
}
125+
126+
this.setupHideOnScroll();
111127
}
112128

113129
async connectedCallback() {
@@ -150,6 +166,64 @@ export class TabBar implements ComponentInterface {
150166
this.keyboardCtrl.destroy();
151167
this.keyboardCtrl = null;
152168
}
169+
170+
this.destroyHideOnScroll();
171+
}
172+
173+
private setupHideOnScroll() {
174+
const theme = getIonTheme(this);
175+
if (theme !== 'ionic' || !this.hideOnScroll || this.expand !== 'compact') {
176+
return;
177+
}
178+
179+
const footerEl = this.el.closest('ion-footer');
180+
const pageEl = footerEl?.closest('ion-page, .ion-page') ?? this.el.closest('ion-page, .ion-page');
181+
const contentEl = pageEl ? findIonContent(pageEl) : null;
182+
183+
if (!contentEl) {
184+
return;
185+
}
186+
187+
this.initScrollListener(contentEl);
188+
}
189+
190+
private async initScrollListener(contentEl: HTMLElement) {
191+
const scrollEl = (this.scrollEl = await getScrollElement(contentEl));
192+
193+
const scrollThreshold = 10;
194+
195+
this.contentScrollCallback = () => {
196+
readTask(() => {
197+
const scrollTop = scrollEl.scrollTop;
198+
const isScrollingDown = scrollTop > this.lastScrollTop;
199+
200+
if (isScrollingDown !== this.lastScrollTop > this.scrollDirectionChangeTop) {
201+
this.scrollDirectionChangeTop = this.lastScrollTop;
202+
}
203+
204+
const delta = Math.abs(scrollTop - this.scrollDirectionChangeTop);
205+
206+
if (delta >= scrollThreshold) {
207+
const shouldHide = isScrollingDown && scrollTop > 0;
208+
if (shouldHide !== this.scrollHidden) {
209+
writeTask(() => {
210+
this.scrollHidden = shouldHide;
211+
});
212+
}
213+
}
214+
215+
this.lastScrollTop = scrollTop;
216+
});
217+
};
218+
219+
scrollEl.addEventListener('scroll', this.contentScrollCallback, { passive: true });
220+
}
221+
222+
private destroyHideOnScroll() {
223+
if (this.scrollEl && this.contentScrollCallback) {
224+
this.scrollEl.removeEventListener('scroll', this.contentScrollCallback);
225+
this.contentScrollCallback = undefined;
226+
}
153227
}
154228

155229
private getShape(): string | undefined {
@@ -169,7 +243,7 @@ export class TabBar implements ComponentInterface {
169243
}
170244

171245
render() {
172-
const { color, translucent, keyboardVisible, expand } = this;
246+
const { color, translucent, keyboardVisible, scrollHidden, expand, hideOnScroll } = this;
173247
const theme = getIonTheme(this);
174248
const shape = this.getShape();
175249
const shouldHide = keyboardVisible && this.el.getAttribute('slot') !== 'top';
@@ -182,6 +256,8 @@ export class TabBar implements ComponentInterface {
182256
[theme]: true,
183257
'tab-bar-translucent': translucent,
184258
'tab-bar-hidden': shouldHide,
259+
'tab-bar-hide-on-scroll': hideOnScroll,
260+
'tab-bar-scroll-hidden': scrollHidden,
185261
[`tab-bar-${expand}`]: true,
186262
[`tab-bar-${shape}`]: shape !== undefined,
187263
})}

packages/angular/src/directives/proxies.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2370,14 +2370,14 @@ export declare interface IonTab extends Components.IonTab {}
23702370

23712371

23722372
@ProxyCmp({
2373-
inputs: ['color', 'expand', 'mode', 'selectedTab', 'shape', 'theme', 'translucent']
2373+
inputs: ['color', 'expand', 'hideOnScroll', 'mode', 'selectedTab', 'shape', 'theme', 'translucent']
23742374
})
23752375
@Component({
23762376
selector: 'ion-tab-bar',
23772377
changeDetection: ChangeDetectionStrategy.OnPush,
23782378
template: '<ng-content></ng-content>',
23792379
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
2380-
inputs: ['color', 'expand', 'mode', 'selectedTab', 'shape', 'theme', 'translucent'],
2380+
inputs: ['color', 'expand', 'hideOnScroll', 'mode', 'selectedTab', 'shape', 'theme', 'translucent'],
23812381
})
23822382
export class IonTabBar {
23832383
protected el: HTMLIonTabBarElement;

packages/angular/standalone/src/directives/proxies.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2113,14 +2113,14 @@ export declare interface IonTab extends Components.IonTab {}
21132113

21142114
@ProxyCmp({
21152115
defineCustomElementFn: defineIonTabBar,
2116-
inputs: ['color', 'expand', 'mode', 'selectedTab', 'shape', 'theme', 'translucent']
2116+
inputs: ['color', 'expand', 'hideOnScroll', 'mode', 'selectedTab', 'shape', 'theme', 'translucent']
21172117
})
21182118
@Component({
21192119
selector: 'ion-tab-bar',
21202120
changeDetection: ChangeDetectionStrategy.OnPush,
21212121
template: '<ng-content></ng-content>',
21222122
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
2123-
inputs: ['color', 'expand', 'mode', 'selectedTab', 'shape', 'theme', 'translucent'],
2123+
inputs: ['color', 'expand', 'hideOnScroll', 'mode', 'selectedTab', 'shape', 'theme', 'translucent'],
21242124
standalone: true
21252125
})
21262126
export class IonTabBar {

0 commit comments

Comments
 (0)