Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2400,6 +2400,7 @@ ion-tab,method,setActive,setActive() => Promise<void>
ion-tab-bar,shadow
ion-tab-bar,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | 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
Expand Down
11 changes: 11 additions & 0 deletions core/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -11298,6 +11308,7 @@ declare namespace LocalJSX {
interface IonTabBarAttributes {
"color": Color;
"selectedTab": string;
"hideOnScroll": boolean;
"translucent": boolean;
"expand": 'compact' | 'full';
"shape": 'soft' | 'round' | 'rectangular';
Expand Down
24 changes: 24 additions & 0 deletions core/src/components/tab-bar/tab-bar.ionic.scss
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,15 @@

position: absolute;

// stylelint-disable-next-line property-disallowed-list
left: 50%;
Comment thread
joselrio marked this conversation as resolved.
Outdated

align-self: center;

width: fit-content;

transform: translateX(calc(-50%));

contain: content;
}

Expand All @@ -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);
Comment thread
joselrio marked this conversation as resolved.
Outdated
}

: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);
Comment thread
joselrio marked this conversation as resolved.
Outdated

opacity: 0;
}

:host([slot="top"].tab-bar-scroll-hidden) {
transform: translateY(-100%) translateX(calc(-50%));
}

// Tab Bar Translucent
// --------------------------------------------------

Expand Down
80 changes: 78 additions & 2 deletions core/src/components/tab-bar/tab-bar.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -26,11 +27,17 @@ export class TabBar implements ComponentInterface {
private keyboardCtrl: KeyboardController | null = null;
private keyboardCtrlPromise: Promise<KeyboardController> | 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"`.
Expand All @@ -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
Expand Down Expand Up @@ -108,6 +122,8 @@ export class TabBar implements ComponentInterface {
tab: this.selectedTab,
});
}

this.setupHideOnScroll();
}

async connectedCallback() {
Expand Down Expand Up @@ -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 {
Expand All @@ -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';
Expand All @@ -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,
})}
Expand Down
74 changes: 74 additions & 0 deletions core/src/components/tab-bar/test/hide-on-scroll/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="en" dir="ltr" mode="ionic">
<head>
<meta charset="UTF-8" />
<title>Tab Bar - Hide on Scroll</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
/>
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<link rel="stylesheet" href="../../../../../css/ionic.bundle.css" />
<link rel="stylesheet" href="../../../../../scripts/testing/styles.css" />
</head>

<body>
<ion-app>
<ion-tabs>
<div class="ion-page">
<ion-content>
<div class="spacer">
<p>Scroll down to hide the tab bar, scroll up to show it.</p>
<p><strong>Requirements:</strong></p>
<ul>
<li>Theme must be set to <code>ionic</code></li>
<li><code>expand</code> must be set to <code>compact</code></li>
<li>A sibling <code>ion-content</code> must exist for scroll detection</li>
</ul>
</div>
</ion-content>

<ion-footer>
<ion-tab-bar slot="bottom" hide-on-scroll="true" selected-tab="1" expand="compact">
<ion-tab-button tab="1">
<ion-icon name="home-outline"></ion-icon>
<ion-label>Home</ion-label>
</ion-tab-button>

<ion-tab-button tab="2">
<ion-icon name="heart-outline"></ion-icon>
<ion-label>Favorites</ion-label>
</ion-tab-button>

<ion-tab-button tab="3">
<ion-icon name="search-outline"></ion-icon>
<ion-label>Search</ion-label>
</ion-tab-button>

<ion-tab-button tab="4">
<ion-icon name="settings-outline"></ion-icon>
<ion-label>Settings</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-footer>
</div>
</ion-tabs>
</ion-app>

<style>
.spacer {
padding: 16px;
height: 2000px;
background: linear-gradient(to bottom, #e0f7fa, #80deea, #26c6da, #00acc1, #00838f);
}

.spacer p {
margin-bottom: 12px;
font-size: 16px;
color: #333;
}
</style>
</body>
</html>
4 changes: 2 additions & 2 deletions packages/angular/src/directives/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Comment thread
joselrio marked this conversation as resolved.
})
@Component({
selector: 'ion-tab-bar',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// 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;
Expand Down
4 changes: 2 additions & 2 deletions packages/angular/standalone/src/directives/proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<ng-content></ng-content>',
// 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 {
Expand Down
Loading