Skip to content

Commit fee45ab

Browse files
joselrioIonitron
andauthored
feat(tab-bar): add hideOnScroll for ionic theme (#31085)
Issue number: internal --------- <!-- 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. --> This pull request introduces a new feature to the `ion-tab-bar` component that allows it to automatically hide when the user scrolls down and reappear when scrolling up. ## What is the current behavior? - Missing `hide-on-scroll` feature ## What is the new behavior? - New Hide-on-Scroll Feature for Tab Bar ## What was done? *Component Logic & API:* - Added a new `hideOnScroll` property to `ion-tab-bar`, enabling automatic hide/show behavior based on scroll direction when the theme is `"ionic"` and `expand` is `"compact"`. The scroll listener is attached to the nearest `ion-content` for scroll detection. (`core/src/components/tab-bar/tab-bar.tsx`, `core/api.txt`) [[1]](diffhunk://#diff-4524448d8146967e9d7ca6409b76b56db889c6bc42c7d4c911d8b3f1a80d5dbaR67-R73) [[2]](diffhunk://#diff-4524448d8146967e9d7ca6409b76b56db889c6bc42c7d4c911d8b3f1a80d5dbaR125-R126) [[3]](diffhunk://#diff-4524448d8146967e9d7ca6409b76b56db889c6bc42c7d4c911d8b3f1a80d5dbaR169-R226) [[4]](diffhunk://#diff-4524448d8146967e9d7ca6409b76b56db889c6bc42c7d4c911d8b3f1a80d5dbaL172-R246) [[5]](diffhunk://#diff-4524448d8146967e9d7ca6409b76b56db889c6bc42c7d4c911d8b3f1a80d5dbaR259-R260) [[6]](diffhunk://#diff-5e21becb925e949068c3cbefae90d040401eaf39874bfd1336d93a3cd8d64809R2403) *Styling:* - Updated SCSS to handle the hide/show animation and positioning for the tab bar when `hideOnScroll` is enabled, including transitions and transforms for both bottom and top slots. (`core/src/components/tab-bar/tab-bar.ionic.scss`) [[1]](diffhunk://#diff-fe63470b419227d79e943ff7370a337e9f273fbcd76d1d420916292e13126db7R73-R81) [[2]](diffhunk://#diff-fe63470b419227d79e943ff7370a337e9f273fbcd76d1d420916292e13126db7R93-R111) *Framework Integrations:* - Updated Angular proxies to support the new `hideOnScroll` input, ensuring compatibility with Angular apps. (`packages/angular/src/directives/proxies.ts`, `packages/angular/standalone/src/directives/proxies.ts`) [[1]](diffhunk://#diff-1fd76195dbb521bd6fc15558ddb677c06cd4a94d7541ecdea25409b875ea71daL2373-R2380) [[2]](diffhunk://#diff-23bd4dda0490183f78acea6eb747bfab3091a16ce2e1d4710f4c194669597628L2116-R2123) *Testing & Documentation:* - Added a new test page demonstrating the hide-on-scroll behavior, along with usage requirements and visual confirmation. (`core/src/components/tab-bar/test/hide-on-scroll/index.html`) ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information - This prop will only affect Ionic theme; - `ion-tab-bar` expand property must be set to compact; - It should be used inside an `ion-footer`; --------- Co-authored-by: ionitron <hi@ionicframework.com>
1 parent a81ced2 commit fee45ab

16 files changed

Lines changed: 240 additions & 32 deletions

core/api.txt

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

core/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
"fs-extra": "^9.0.1",
6969
"jest": "^29.7.0",
7070
"jest-cli": "^29.7.0",
71-
"outsystems-design-tokens": "^1.3.7",
71+
"outsystems-design-tokens": "^1.3.8",
7272
"playwright-core": "^1.58.2",
7373
"prettier": "^2.8.8",
7474
"rollup": "^2.26.4",

core/src/components.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3980,6 +3980,11 @@ export namespace Components {
39803980
* @default 'full'
39813981
*/
39823982
"expand": 'compact' | 'full';
3983+
/**
3984+
* 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"`.
3985+
* @default false
3986+
*/
3987+
"hideOnScroll": boolean;
39833988
/**
39843989
* The mode determines the platform behaviors of the component.
39853990
*/
@@ -10077,6 +10082,11 @@ declare namespace LocalJSX {
1007710082
* @default 'full'
1007810083
*/
1007910084
"expand"?: 'compact' | 'full';
10085+
/**
10086+
* 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"`.
10087+
* @default false
10088+
*/
10089+
"hideOnScroll"?: boolean;
1008010090
/**
1008110091
* The mode determines the platform behaviors of the component.
1008210092
*/
@@ -11320,6 +11330,7 @@ declare namespace LocalJSX {
1132011330
interface IonTabBarAttributes {
1132111331
"color": Color;
1132211332
"selectedTab": string;
11333+
"hideOnScroll": boolean;
1132311334
"translucent": boolean;
1132411335
"expand": 'compact' | 'full';
1132511336
"shape": 'soft' | 'round' | 'rectangular';

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@use "../../themes/ionic/ionic.globals.scss" as globals;
2+
@use "../../themes/mixins" as mixins;
23
@use "./tab-bar.common";
34

45
:host {
@@ -67,13 +68,16 @@
6768
/* Compact */
6869
:host(.tab-bar-compact) {
6970
@include globals.padding(globals.$ion-space-100, globals.$ion-space-400);
71+
@include mixins.position-horizontal(50%, null);
7072

7173
position: absolute;
7274

7375
align-self: center;
7476

7577
width: fit-content;
7678

79+
transform: translateX(calc(-50%));
80+
7781
contain: content;
7882
}
7983

@@ -85,6 +89,23 @@
8589
bottom: calc(globals.$ion-space-400 + var(--ion-safe-area-bottom, 0));
8690
}
8791

92+
// Tab Bar Hide on Scroll
93+
// --------------------------------------------------
94+
95+
:host(.tab-bar-hide-on-scroll) {
96+
transition: transform globals.$ion-transition-time-200 globals.$ion-transition-curve-spring;
97+
}
98+
99+
:host(.tab-bar-scroll-hidden) {
100+
transform: translateY(calc(100% + var(--ion-safe-area-bottom, 0) + globals.$ion-space-1000)) translateX(calc(-50%));
101+
102+
transition: transform globals.$ion-transition-time-350 globals.$ion-transition-curve-base;
103+
}
104+
105+
:host([slot="top"].tab-bar-scroll-hidden) {
106+
transform: translateY(-100%) translateX(calc(-50%));
107+
}
108+
88109
// Tab Bar Translucent
89110
// --------------------------------------------------
90111

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

Lines changed: 94 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,80 @@ 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+
this.contentScrollCallback = () => {
194+
readTask(() => {
195+
const scrollTop = scrollEl.scrollTop;
196+
const shouldHide = this.checkScrollStatus(scrollTop);
197+
198+
if (shouldHide !== this.scrollHidden) {
199+
writeTask(() => {
200+
this.scrollHidden = shouldHide;
201+
});
202+
}
203+
204+
this.lastScrollTop = scrollTop;
205+
});
206+
};
207+
208+
scrollEl.addEventListener('scroll', this.contentScrollCallback, { passive: true });
209+
}
210+
211+
private destroyHideOnScroll() {
212+
if (this.scrollEl && this.contentScrollCallback) {
213+
this.scrollEl.removeEventListener('scroll', this.contentScrollCallback);
214+
this.contentScrollCallback = undefined;
215+
}
216+
}
217+
218+
private checkScrollStatus(scrollTop: number): boolean {
219+
// Always visible within the first 80px of scroll
220+
const visibleZone = 80;
221+
// Hides after 60px of continuous downward scrolling only, when scrolling up threashold should be 0px
222+
const scrollThresholdHide = 60;
223+
224+
if (scrollTop <= visibleZone) {
225+
return false;
226+
}
227+
228+
const isScrollingDown = scrollTop > this.lastScrollTop;
229+
const wasScrollingDown = this.lastScrollTop > this.scrollDirectionChangeTop;
230+
231+
if (isScrollingDown !== wasScrollingDown) {
232+
this.scrollDirectionChangeTop = this.lastScrollTop;
233+
}
234+
235+
const delta = Math.abs(scrollTop - this.scrollDirectionChangeTop);
236+
const threshold = isScrollingDown ? scrollThresholdHide : 0;
237+
238+
if (delta < threshold) {
239+
return this.scrollHidden;
240+
}
241+
242+
return isScrollingDown;
153243
}
154244

155245
private getShape(): string | undefined {
@@ -169,7 +259,7 @@ export class TabBar implements ComponentInterface {
169259
}
170260

171261
render() {
172-
const { color, translucent, keyboardVisible, expand } = this;
262+
const { color, translucent, keyboardVisible, scrollHidden, expand, hideOnScroll } = this;
173263
const theme = getIonTheme(this);
174264
const shape = this.getShape();
175265
const shouldHide = keyboardVisible && this.el.getAttribute('slot') !== 'top';
@@ -182,6 +272,8 @@ export class TabBar implements ComponentInterface {
182272
[theme]: true,
183273
'tab-bar-translucent': translucent,
184274
'tab-bar-hidden': shouldHide,
275+
'tab-bar-hide-on-scroll': hideOnScroll,
276+
'tab-bar-scroll-hidden': scrollHidden,
185277
[`tab-bar-${expand}`]: true,
186278
[`tab-bar-${shape}`]: shape !== undefined,
187279
})}

core/src/components/tab-bar/test/expand/tab-bar.e2e.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ configs({ directions: ['ltr'], modes: ['ionic-md'] }).forEach(({ title, screensh
5252
.container {
5353
padding: 20px 10px;
5454
/* Size is needed because tab bar compact has position absolute and will not capture correctly. */
55-
width: 225px;
55+
width: 100%;
5656
height: 96px;
5757
}
5858
</style>
245 Bytes
Loading
Loading
362 Bytes
Loading

0 commit comments

Comments
 (0)