From f694300f5555b4122e749d4ea1b30663665f9148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Rio?= Date: Thu, 16 Apr 2026 10:32:47 +0100 Subject: [PATCH 1/4] feat(tab-bar): Adding hide-on-scroll prop for ionic theme --- core/api.txt | 1 + core/src/components.d.ts | 11 +++ .../src/components/tab-bar/tab-bar.ionic.scss | 24 ++++++ core/src/components/tab-bar/tab-bar.tsx | 80 ++++++++++++++++++- packages/angular/src/directives/proxies.ts | 4 +- .../standalone/src/directives/proxies.ts | 4 +- 6 files changed, 118 insertions(+), 6 deletions(-) diff --git a/core/api.txt b/core/api.txt index e4e1ca4d603..43e1b6b7192 100644 --- a/core/api.txt +++ b/core/api.txt @@ -2400,6 +2400,7 @@ ion-tab,method,setActive,setActive() => Promise ion-tab-bar,shadow ion-tab-bar,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record | undefined,undefined,false,true ion-tab-bar,prop,expand,"compact" | "full",'full',false,false +ion-tab-bar,prop,hideOnScroll,boolean,false,false,false ion-tab-bar,prop,mode,"ios" | "md",undefined,false,false ion-tab-bar,prop,selectedTab,string | undefined,undefined,false,false ion-tab-bar,prop,shape,"rectangular" | "round" | "soft" | undefined,undefined,false,false diff --git a/core/src/components.d.ts b/core/src/components.d.ts index ecbdb7475c6..f5e9071baac 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -3970,6 +3970,11 @@ export namespace Components { * @default 'full' */ "expand": 'compact' | 'full'; + /** + * 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"`. + * @default false + */ + "hideOnScroll": boolean; /** * The mode determines the platform behaviors of the component. */ @@ -10057,6 +10062,11 @@ declare namespace LocalJSX { * @default 'full' */ "expand"?: 'compact' | 'full'; + /** + * 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"`. + * @default false + */ + "hideOnScroll"?: boolean; /** * The mode determines the platform behaviors of the component. */ @@ -11298,6 +11308,7 @@ declare namespace LocalJSX { interface IonTabBarAttributes { "color": Color; "selectedTab": string; + "hideOnScroll": boolean; "translucent": boolean; "expand": 'compact' | 'full'; "shape": 'soft' | 'round' | 'rectangular'; diff --git a/core/src/components/tab-bar/tab-bar.ionic.scss b/core/src/components/tab-bar/tab-bar.ionic.scss index 1253e49c334..a375f4bc7c0 100644 --- a/core/src/components/tab-bar/tab-bar.ionic.scss +++ b/core/src/components/tab-bar/tab-bar.ionic.scss @@ -70,10 +70,15 @@ position: absolute; + // stylelint-disable-next-line property-disallowed-list + left: 50%; + align-self: center; width: fit-content; + transform: translateX(calc(-50%)); + contain: content; } @@ -85,6 +90,25 @@ bottom: calc(globals.$ion-space-400 + var(--ion-safe-area-bottom, 0)); } +// Tab Bar Hide on Scroll +// -------------------------------------------------- + +:host(.tab-bar-hide-on-scroll) { + transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1); +} + +:host(.tab-bar-scroll-hidden) { + transform: translateY(calc(100% + var(--ion-safe-area-bottom, 0))) translateX(calc(-50%)); + + transition: transform 0.3s cubic-bezier(0.3, 1, 0.1, 1), opacity 0.3s cubic-bezier(0.3, 1, 0.1, 1); + + opacity: 0; +} + +:host([slot="top"].tab-bar-scroll-hidden) { + transform: translateY(-100%) translateX(calc(-50%)); +} + // Tab Bar Translucent // -------------------------------------------------- diff --git a/core/src/components/tab-bar/tab-bar.tsx b/core/src/components/tab-bar/tab-bar.tsx index c29ac90f08a..191acbb7987 100644 --- a/core/src/components/tab-bar/tab-bar.tsx +++ b/core/src/components/tab-bar/tab-bar.tsx @@ -1,5 +1,6 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; -import { Component, Element, Event, Host, Prop, State, Watch, h } from '@stencil/core'; +import { Component, Element, Event, Host, Prop, State, Watch, h, readTask, writeTask } from '@stencil/core'; +import { findIonContent, getScrollElement } from '@utils/content'; import type { KeyboardController } from '@utils/keyboard/keyboard-controller'; import { createKeyboardController } from '@utils/keyboard/keyboard-controller'; import { createColorClasses } from '@utils/theme'; @@ -26,11 +27,17 @@ export class TabBar implements ComponentInterface { private keyboardCtrl: KeyboardController | null = null; private keyboardCtrlPromise: Promise | null = null; private didLoad = false; + private scrollEl?: HTMLElement; + private contentScrollCallback?: () => void; + private lastScrollTop = 0; + private scrollDirectionChangeTop = 0; @Element() el!: HTMLElement; @State() keyboardVisible = false; + @State() scrollHidden = false; + /** * The color to use from your application's color palette. * Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. @@ -57,6 +64,13 @@ export class TabBar implements ComponentInterface { } } + /** + * 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"`. + */ + @Prop() hideOnScroll = false; + /** * If `true`, the tab bar will be translucent. * Only applies when the theme is `"ios"` and the device supports @@ -108,6 +122,8 @@ export class TabBar implements ComponentInterface { tab: this.selectedTab, }); } + + this.setupHideOnScroll(); } async connectedCallback() { @@ -150,6 +166,64 @@ export class TabBar implements ComponentInterface { this.keyboardCtrl.destroy(); this.keyboardCtrl = null; } + + this.destroyHideOnScroll(); + } + + private setupHideOnScroll() { + const theme = getIonTheme(this); + if (theme !== 'ionic' || !this.hideOnScroll || this.expand !== 'compact') { + return; + } + + const footerEl = this.el.closest('ion-footer'); + const pageEl = footerEl?.closest('ion-page, .ion-page') ?? this.el.closest('ion-page, .ion-page'); + const contentEl = pageEl ? findIonContent(pageEl) : null; + + if (!contentEl) { + return; + } + + this.initScrollListener(contentEl); + } + + private async initScrollListener(contentEl: HTMLElement) { + const scrollEl = (this.scrollEl = await getScrollElement(contentEl)); + + const scrollThreshold = 10; + + this.contentScrollCallback = () => { + readTask(() => { + const scrollTop = scrollEl.scrollTop; + const isScrollingDown = scrollTop > this.lastScrollTop; + + if (isScrollingDown !== this.lastScrollTop > this.scrollDirectionChangeTop) { + this.scrollDirectionChangeTop = this.lastScrollTop; + } + + const delta = Math.abs(scrollTop - this.scrollDirectionChangeTop); + + if (delta >= scrollThreshold) { + const shouldHide = isScrollingDown && scrollTop > 0; + if (shouldHide !== this.scrollHidden) { + writeTask(() => { + this.scrollHidden = shouldHide; + }); + } + } + + this.lastScrollTop = scrollTop; + }); + }; + + scrollEl.addEventListener('scroll', this.contentScrollCallback, { passive: true }); + } + + private destroyHideOnScroll() { + if (this.scrollEl && this.contentScrollCallback) { + this.scrollEl.removeEventListener('scroll', this.contentScrollCallback); + this.contentScrollCallback = undefined; + } } private getShape(): string | undefined { @@ -169,7 +243,7 @@ export class TabBar implements ComponentInterface { } render() { - const { color, translucent, keyboardVisible, expand } = this; + const { color, translucent, keyboardVisible, scrollHidden, expand, hideOnScroll } = this; const theme = getIonTheme(this); const shape = this.getShape(); const shouldHide = keyboardVisible && this.el.getAttribute('slot') !== 'top'; @@ -182,6 +256,8 @@ export class TabBar implements ComponentInterface { [theme]: true, 'tab-bar-translucent': translucent, 'tab-bar-hidden': shouldHide, + 'tab-bar-hide-on-scroll': hideOnScroll, + 'tab-bar-scroll-hidden': scrollHidden, [`tab-bar-${expand}`]: true, [`tab-bar-${shape}`]: shape !== undefined, })} diff --git a/packages/angular/src/directives/proxies.ts b/packages/angular/src/directives/proxies.ts index fecc4aed2a8..af9d5e35525 100644 --- a/packages/angular/src/directives/proxies.ts +++ b/packages/angular/src/directives/proxies.ts @@ -2370,14 +2370,14 @@ export declare interface IonTab extends Components.IonTab {} @ProxyCmp({ - inputs: ['color', 'expand', 'mode', 'selectedTab', 'shape', 'theme', 'translucent'] + inputs: ['color', 'expand', 'hideOnScroll', 'mode', 'selectedTab', 'shape', 'theme', 'translucent'] }) @Component({ selector: 'ion-tab-bar', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'expand', 'mode', 'selectedTab', 'shape', 'theme', 'translucent'], + inputs: ['color', 'expand', 'hideOnScroll', 'mode', 'selectedTab', 'shape', 'theme', 'translucent'], }) export class IonTabBar { protected el: HTMLIonTabBarElement; diff --git a/packages/angular/standalone/src/directives/proxies.ts b/packages/angular/standalone/src/directives/proxies.ts index 41e27889758..3bac5ccbe72 100644 --- a/packages/angular/standalone/src/directives/proxies.ts +++ b/packages/angular/standalone/src/directives/proxies.ts @@ -2113,14 +2113,14 @@ export declare interface IonTab extends Components.IonTab {} @ProxyCmp({ defineCustomElementFn: defineIonTabBar, - inputs: ['color', 'expand', 'mode', 'selectedTab', 'shape', 'theme', 'translucent'] + inputs: ['color', 'expand', 'hideOnScroll', 'mode', 'selectedTab', 'shape', 'theme', 'translucent'] }) @Component({ selector: 'ion-tab-bar', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'expand', 'mode', 'selectedTab', 'shape', 'theme', 'translucent'], + inputs: ['color', 'expand', 'hideOnScroll', 'mode', 'selectedTab', 'shape', 'theme', 'translucent'], standalone: true }) export class IonTabBar { From b7c8847eb4295dd446e0c1391047160fdaa83be1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20Rio?= Date: Thu, 16 Apr 2026 10:34:00 +0100 Subject: [PATCH 2/4] feat(tab-bar): Adding hide-on-scroll new prop sample --- .../tab-bar/test/hide-on-scroll/index.html | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 core/src/components/tab-bar/test/hide-on-scroll/index.html diff --git a/core/src/components/tab-bar/test/hide-on-scroll/index.html b/core/src/components/tab-bar/test/hide-on-scroll/index.html new file mode 100644 index 00000000000..7e75f033507 --- /dev/null +++ b/core/src/components/tab-bar/test/hide-on-scroll/index.html @@ -0,0 +1,74 @@ + + + + + Tab Bar - Hide on Scroll + + + + + + + + + + + +
+ +
+

Scroll down to hide the tab bar, scroll up to show it.

+

Requirements:

+
    +
  • Theme must be set to ionic
  • +
  • expand must be set to compact
  • +
  • A sibling ion-content must exist for scroll detection
  • +
+
+
+ + + + + + Home + + + + + Favorites + + + + + Search + + + + + Settings + + + +
+
+
+ + + + From 265accb5f806f4689329ed9cb7e428efaa786df5 Mon Sep 17 00:00:00 2001 From: ionitron Date: Thu, 16 Apr 2026 10:08:04 +0000 Subject: [PATCH 3/4] chore(): add updated snapshots --- ...ionic-md-ltr-light-Mobile-Chrome-linux.png | Bin 2689 -> 2104 bytes ...onic-md-ltr-light-Mobile-Firefox-linux.png | Bin 4198 -> 3209 bytes ...ionic-md-ltr-light-Mobile-Safari-linux.png | Bin 3583 -> 2693 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/core/src/components/tab-bar/test/expand/tab-bar.e2e.ts-snapshots/tab-bar-expand-compact-ionic-md-ltr-light-Mobile-Chrome-linux.png b/core/src/components/tab-bar/test/expand/tab-bar.e2e.ts-snapshots/tab-bar-expand-compact-ionic-md-ltr-light-Mobile-Chrome-linux.png index c4e13cf4adf9bef638dd84c8e19d81149c0e36f1..c57299ddb09ae1899a6a6a2cbdd535f8f0b7f49e 100644 GIT binary patch delta 2064 zcmbVN`#;kS8(!r})JWn(Hky!At=T-7vLY6umxhuWr9CPTA z$P}w=>9EC`SeiptPNjH7OygPq!u#WW-`5YURxUXT=;5n_wHSDb93&Fp?ksKzkE5@eTkg|*->Z*=jP!t zfnie>7~pP0gFt0iBqDrzp>b(rf|nf=9UW?PzL{?a&!4*_c7(p%1u^upd`>Ju&P5*Gd4OMU zQpPpfhJgonOF9x*_6a>yFcb>ihQk}H&`J^pluzw+R`NJ+3E$Gy$tFy_slbetcbtE^ z+33#UWba0!D^?Lh!v4LpGi9av<{`GfzKuNcDEm|Ejw4{`3A=j6#VRZuvNVzJ^M{v6 zX^L?d4}5r@n~pJQn<$KO`FcXf5e3^tnG-oy$I zJ^E)0mHUcsi{3F7aw`g(koTyqO$(V_jJLdv6*#oFwZU4?94!c|VI4b$^FmzKkxJME z@r0U3Jkb8G!H*?nSk-zKguP+1HuYt%nCt;F0|Nt#;K4y{(eFAEr0E31E%#vrGWlVZ z+q2(Hy~RG(=r~}KU!tOE(s-uDaX(UVpCw~84ro#fkw~MRiQ9YB1dq17 zpz>|vM!Cwy#szSsbb2pu59OjD(Iue=h<`;hb1yphZeyF3bf1~$%>Lj>{t;>8VqINI z_CXE|Yk*0UjE)BR`8|vq>po$?&5`z8S6?_Qd7U5XTFt+thJiTc`uh1v(?8nm)u=ak zLd0;3aIXULUjf$6vTRN@u7k&0k*|@brweti$%P!HJUTuD?Cl~RTsv(kDF8k(SR3=@ zagGtR+?wrB@b%mp_jVb}EB5q0%8s&(DrITpw*FGZsdouhw;su+E`J~6jJ3ypxT$C3 zNN`A+2iXO4(xCJ?g^e$$^@#|V+21bPw*K@z0;;eU4_>d-*S8*)NMDzgmVT44MMFb2 zil`qr*M5?2>s8(fBrQzH*M7#t#3=MRq{@g@hf|gt+3eNT#i_+fffKXpc!9!kFziam zX)>AYDB3^izA&&3YU6wp|@x! z?(^Sgu@+413v0$WfPr;1O$TIizueg&{0)63$P$Pe=}ZKJ!P4G5O#C=VmaGW1LeL1C zu>Y^V<>lq}2M;10xOU9f{C0WBP zq`BeGbhic;J+x$I8iS)P-E3nAsNERrPSrz(=g`VNJd6wOh!0UTkub?Www4L-fLk9$ zwWq{`0|S+P*C$|knnHxjZIpsEalqhVy*UZLChb+n5ohk@K2Bbo6tb|+uJhtaA|wD3#P(r~{9do9|1Z0Ls^d>XX2yQ;o9;(YX)caAq8ml+$G zSxrUGKatE(wEr48KT&G4pm{ZcxU<~VSjkJvt%Wy$s^~jKZ3OK7PYRy>XST`$yQ#)c z$3ydS!hm~S8^}fm@j$?gxkhJ+D6!)h_gQznrU&QJYPuI~=Y`N8Vddkxro!T^0>)>8 zP}1;T)N-M>H0Thftl#T<2qSw}O?|6iYO(mEO_zD{;EQkWc?ie)jHtV6h@QOUG?6_k!E)H_{1*4Xqwt@XtGQ~sAoEN& z-HQ>A(S2VFnXIdkRPP)3i0P0>v<76LCd4lTm{Q;KvRTQJbqD3^z`R`-Lwu26cKOk{K+7 zu-;}c_G~4~_-WLtQY9;FF9~}8e~Fc>&l!bLo10JKJn4l{(->U_%OxJ}fC3KbRt71db}Kd@S7*=T&5q~O{{`yR4zK_K delta 2654 zcmbW3S5y;-5{6M$Sb7&i5tI-C6)6S`O-ks58bp+`(wkCMLNO~PA`79Ih!jae2#~NY zB?8hfp_3pg1f&H7q$wp70TH~p_w~NrhdF1?oGB0Ue{<%o=o6_NWf^{58>lHfuK3%l zp$K~~`97ph`JU|SUV5&9BGQ{?31nShi<_GBIh$R(z2{9C-Y*y_S$@~HU!3qRsQ^Ok z81{|cY+7bP?iueGRL7KSI;GmO%MZ|Jz+iBbC%W!)Z6JxeQv|+!P8^+zJudkorn;aR5S?m`xM6&059z1w}gbPI$n0I!G(SQ8- zv3+PLG{BqIx_#haK2Vj3$APiy7d|*a zY`mWn*-;>lsBRi!?V1QuwR3o- z500Ae4xqmvmgiposk1o%XdIIr2y zzmK-z;pc~589X}qdC2NMBXCxUy*TiE5$G9s2`~D?vGg{m$f>ZR672?Pwq3d1j{Nm_ zq#V|1XWgyhuhciE-Di^Of{>jcN>?CY!eUS=8vimQI3&chDtK*_+*xG}rSk!DvWlP3 z<&knLKyxy%)TcVyqdibxiC@E~eR^uDC2&aAjc3;+_tZo`;4Pz8jA?`oT~d*;-JyhO zYq?r!`AmkBbF%4<;Z7|2( z+goE+DnOSqx&j=MH3tpW{q=|8xk{5rCiB6UL9AQV%`63jn>YzTj!;2MOAEO)vf#O& zob7`okd~=qLDUkXfW6RHeXEuR)Tuik(-S1y#2648*=J!+bu~H73gr@S@~wXR)|zyr zTeyXya`&ScjrV70S@8338EU2Dj2qGvzSaq_rPiTtz#xWXmtQ!W%y>&m?m{kjF2QpI zY*O;gBm~;&Eca_To#@?x=NH%4*M)?Hj=rx`o5hHavd?Ow=$+>&RM+p5$0 z`^37+GS+L#qh)OjPtSmSU&3p#wt7sy*$HRRgO9P-Vj-d7#)$_t_of<(^dc;m5)Zb{ zN4Fz(EOF}`&Gw`2h7Jm9=#abuA!8v9P%pk3c&<-1I^v8K)HoBTj`oWW2dFHUp7qoU@Pmax#!t*cYipy81U$EnH5MbcHD_Hb4!fGYM>SiuYs8NW4W zdd;xm{tWU5XRm&JXy>#!?ThD84wgH6H;^atkZ5247bxuu3ZN78Zuh?7oBK1(y}i9r z3%v^0Bf0LCqtWQi*^bTWCW#ShTU*=f*RN3m7n&Fw7{GJ*ajubz*dAs2Jksi)CX>E12v-2OXmPL{+7>Q|O)Y7?fMQiSL{CRDq^3gNQ)u*?> z#~heH5UpG%r<-Dy=X?I*a9^SmB)lfu`q$O2GC42 z`Sk3)3F|m3)HqWebm)7t^ z#&HO@c@BaV>AofqPVK#WCUNoXmD{PGvERRc$6j)G@tD(g^?m;@3qZG7#d|jgS$>KQ zVs_ruP~nL_Lbc5aD!)0?(nu!r{UUZf6&WrBhixwou&U!Zr*(a{LlH(&FnEaP3f3gF zA$M0kd>yYQl2gSD)TKR_7UNPXS#)I1OrP6VEAnmvqaT%>{OK@Xrk4j!i-d>Q$je$O zk7diYXZ=oEVGn5CLTR0%naDL2LwJ4}P}q|KJWmkfIIZ6(CNem)G9jGlw*T0DDI7OD z-Z#_oPG%sANO7|(W_#J%`m2gXh`NIoEj#Mu@5aZ0)2Fp z8f9!ni#bLzEg~~r{Vw)o;LFZp64802a{q93i!^)TN)3GVF*ZtL!gS?mO`de^8mZFB z_HqCiPhs!%t@8hv4XGhj>Vq*Z5kZDvkCiv?t!%qmE^w>&tjVR7xKtATMa7gQlpU2v z3Tz46*S%cL9c{j-0)8{B;y0=*y?B!euz|}Dqb)W;Wca@S^>F{O z7-iR;m|LC9{w_l8?qzcuRNuj-B6}^tb)GqZ%Vc_UI7uNW%#6g=yyJCz>5>T13LFY zu>=U%%05K%ol#Gjw^ecT~Wqeqx4j@p8b(0>ScOMemc9H&dDUELeJfAB3KW6a1 sMaw>1TwK3?ZBWw(IS$W%UPo>&Tj37HaxT*jPK3+G!U6h^*&oUO25TcGUjP6A diff --git a/core/src/components/tab-bar/test/expand/tab-bar.e2e.ts-snapshots/tab-bar-expand-compact-ionic-md-ltr-light-Mobile-Firefox-linux.png b/core/src/components/tab-bar/test/expand/tab-bar.e2e.ts-snapshots/tab-bar-expand-compact-ionic-md-ltr-light-Mobile-Firefox-linux.png index 87b1455219d09964b9a5abeb3885dcc790e38599..422536fbfa130e1902311e532af125cff2fd31ed 100644 GIT binary patch delta 3205 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 Date: Thu, 16 Apr 2026 12:29:58 +0100 Subject: [PATCH 4/4] fix(tab-bar): update position and animation based on requirements --- .../src/components/tab-bar/tab-bar.ionic.scss | 8 ++-- core/src/components/tab-bar/tab-bar.tsx | 48 ++++++++++++------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/core/src/components/tab-bar/tab-bar.ionic.scss b/core/src/components/tab-bar/tab-bar.ionic.scss index a375f4bc7c0..4e384416d25 100644 --- a/core/src/components/tab-bar/tab-bar.ionic.scss +++ b/core/src/components/tab-bar/tab-bar.ionic.scss @@ -94,15 +94,13 @@ // -------------------------------------------------- :host(.tab-bar-hide-on-scroll) { - transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1); + transition: transform 0.22s cubic-bezier(0.16, 1, 0.3, 1); } :host(.tab-bar-scroll-hidden) { - transform: translateY(calc(100% + var(--ion-safe-area-bottom, 0))) translateX(calc(-50%)); + transform: translateY(calc(100% + var(--ion-safe-area-bottom, 0) + globals.$ion-space-1000)) translateX(calc(-50%)); - transition: transform 0.3s cubic-bezier(0.3, 1, 0.1, 1), opacity 0.3s cubic-bezier(0.3, 1, 0.1, 1); - - opacity: 0; + transition: transform 0.35s cubic-bezier(0.3, 1, 0.1, 1); } :host([slot="top"].tab-bar-scroll-hidden) { diff --git a/core/src/components/tab-bar/tab-bar.tsx b/core/src/components/tab-bar/tab-bar.tsx index 191acbb7987..8fc1e5ecdd7 100644 --- a/core/src/components/tab-bar/tab-bar.tsx +++ b/core/src/components/tab-bar/tab-bar.tsx @@ -190,26 +190,15 @@ export class TabBar implements ComponentInterface { private async initScrollListener(contentEl: HTMLElement) { const scrollEl = (this.scrollEl = await getScrollElement(contentEl)); - const scrollThreshold = 10; - this.contentScrollCallback = () => { readTask(() => { const scrollTop = scrollEl.scrollTop; - const isScrollingDown = scrollTop > this.lastScrollTop; - - if (isScrollingDown !== this.lastScrollTop > this.scrollDirectionChangeTop) { - this.scrollDirectionChangeTop = this.lastScrollTop; - } - - const delta = Math.abs(scrollTop - this.scrollDirectionChangeTop); + const shouldHide = this.checkScrollStatus(scrollTop); - if (delta >= scrollThreshold) { - const shouldHide = isScrollingDown && scrollTop > 0; - if (shouldHide !== this.scrollHidden) { - writeTask(() => { - this.scrollHidden = shouldHide; - }); - } + if (shouldHide !== this.scrollHidden) { + writeTask(() => { + this.scrollHidden = shouldHide; + }); } this.lastScrollTop = scrollTop; @@ -226,6 +215,33 @@ export class TabBar implements ComponentInterface { } } + private checkScrollStatus(scrollTop: number): boolean { + // Always visible within the first 80px of scroll + const visibleZone = 80; + // Hides after 60px of continuous downward scrolling only, when scrolling up threashold should be 0px + const scrollThresholdHide = 60; + + if (scrollTop <= visibleZone) { + return false; + } + + const isScrollingDown = scrollTop > this.lastScrollTop; + const wasScrollingDown = this.lastScrollTop > this.scrollDirectionChangeTop; + + if (isScrollingDown !== wasScrollingDown) { + this.scrollDirectionChangeTop = this.lastScrollTop; + } + + const delta = Math.abs(scrollTop - this.scrollDirectionChangeTop); + const threshold = isScrollingDown ? scrollThresholdHide : 0; + + if (delta < threshold) { + return this.scrollHidden; + } + + return isScrollingDown; + } + private getShape(): string | undefined { const theme = getIonTheme(this); const { shape } = this;