diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 0137e730f4..9335a45882 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -2707,36 +2707,6 @@ declare global { } -import { - TabHighlight as IonTabHighlight -} from './components/tab-highlight/tab-highlight'; - -declare global { - interface HTMLIonTabHighlightElement extends IonTabHighlight, HTMLElement { - } - var HTMLIonTabHighlightElement: { - prototype: HTMLIonTabHighlightElement; - new (): HTMLIonTabHighlightElement; - }; - interface HTMLElementTagNameMap { - "ion-tab-highlight": HTMLIonTabHighlightElement; - } - interface ElementTagNameMap { - "ion-tab-highlight": HTMLIonTabHighlightElement; - } - namespace JSX { - interface IntrinsicElements { - "ion-tab-highlight": JSXElements.IonTabHighlightAttributes; - } - } - namespace JSXElements { - export interface IonTabHighlightAttributes extends HTMLAttributes { - selectedTab?: HTMLIonTabElement; - } - } -} - - import { Tab as IonTab } from './components/tab/tab'; @@ -2805,6 +2775,7 @@ declare global { highlight?: boolean; layout?: string; placement?: string; + scrollable?: Boolean; selectedTab?: HTMLIonTabElement; tabs?: HTMLIonTabElement[]; translucent?: boolean; @@ -2838,6 +2809,7 @@ declare global { namespace JSXElements { export interface IonTabsAttributes extends HTMLAttributes { name?: string; + scrollable?: boolean; tabbarHidden?: boolean; tabbarHighlight?: boolean; tabbarLayout?: string; diff --git a/packages/core/src/components/scroll/scroll.tsx b/packages/core/src/components/scroll/scroll.tsx index 9c8e81cff0..e5813a0c36 100644 --- a/packages/core/src/components/scroll/scroll.tsx +++ b/packages/core/src/components/scroll/scroll.tsx @@ -148,7 +148,7 @@ export class Scroll { if (easedT < 1) { // do not use DomController here // must use nativeRaf in order to fire in the next frame - this.dom.raf(step); + self.dom.raf(step); } else { stopScroll = true; diff --git a/packages/core/src/components/tab-button/readme.md b/packages/core/src/components/tab-button/readme.md index 53a86e0031..a3e455c10f 100644 --- a/packages/core/src/components/tab-button/readme.md +++ b/packages/core/src/components/tab-button/readme.md @@ -31,6 +31,12 @@ any ## Events +#### ionTabButtonDidLoad + + +#### ionTabButtonDidUnload + + #### ionTabbarClick diff --git a/packages/core/src/components/tab-button/tab-button.tsx b/packages/core/src/components/tab-button/tab-button.tsx index 8088f68825..79f06ce7d9 100644 --- a/packages/core/src/components/tab-button/tab-button.tsx +++ b/packages/core/src/components/tab-button/tab-button.tsx @@ -1,4 +1,4 @@ -import { Component, Event, EventEmitter, Listen, Prop } from '@stencil/core'; +import {Component, Element, Event, EventEmitter, Listen, Prop} from '@stencil/core'; @Component({ @@ -6,10 +6,22 @@ import { Component, Event, EventEmitter, Listen, Prop } from '@stencil/core'; }) export class TabButton { + @Element() el: HTMLElement; + @Prop() selected = false; @Prop() tab: HTMLIonTabElement; @Event() ionTabbarClick: EventEmitter; + @Event() ionTabButtonDidLoad: EventEmitter; + @Event() ionTabButtonDidUnload: EventEmitter; + + componentDidLoad() { + this.ionTabButtonDidLoad.emit({ button: this }); + } + + componentDidUnload() { + this.ionTabButtonDidUnload.emit({ button: this }); + } @Listen('click') protected onClick(ev: UIEvent) { @@ -21,7 +33,7 @@ export class TabButton { const selected = this.selected; const tab = this.tab; const hasTitle = !!tab.title; - const hasIcon = !! tab.icon; + const hasIcon = !!tab.icon; const hasTitleOnly = (hasTitle && !hasIcon); const hasIconOnly = (hasIcon && !hasTitle); const hasBadge = !!tab.badge; diff --git a/packages/core/src/components/tab-highlight/readme.md b/packages/core/src/components/tab-highlight/readme.md deleted file mode 100644 index 5828817aea..0000000000 --- a/packages/core/src/components/tab-highlight/readme.md +++ /dev/null @@ -1,25 +0,0 @@ -# ion-tab-highlight - - - - - - -## Properties - -#### selectedTab - -any - - -## Attributes - -#### selectedTab - -any - - - ----------------------------------------------- - -*Built by [StencilJS](https://stenciljs.com/)* diff --git a/packages/core/src/components/tab-highlight/tab-highlight.tsx b/packages/core/src/components/tab-highlight/tab-highlight.tsx deleted file mode 100644 index 79a0958b90..0000000000 --- a/packages/core/src/components/tab-highlight/tab-highlight.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Component, Element, Listen, Prop, PropDidChange, State } from '@stencil/core'; -import { DomController } from '../../index'; -import { getParentElement } from '../../utils/helpers'; - - -@Component({ - tag: 'ion-tab-highlight' -}) -export class TabHighlight { - - @Element() private el: HTMLElement; - - @State() animated = false; - @State() transform = ''; - - @Prop({ context: 'dom' }) dom: DomController; - - @Prop() selectedTab: HTMLIonTabElement; - @PropDidChange('selectedTab') - selectedTabChanged() { - this.updateTransform(); - } - - @Listen('window:resize') - onResize() { - this.updateTransform(); - } - - componentDidLoad() { - this.updateTransform(); - } - - protected updateTransform() { - this.dom.read(() => { - const btn = this.getSelectedButton(); - this.transform = (btn) - ? `translate3d(${btn.offsetLeft}px,0,0) scaleX(${btn.offsetWidth})` - : ''; - if (!this.animated) { - setTimeout(() => this.animated = true, 80); - } - }); - } - - private getSelectedButton(): HTMLIonTabButtonElement { - const parent = getParentElement(this.el) as HTMLElement; - if (!parent) { - return null; - } - return Array.from(parent.querySelectorAll('ion-tab-button')) - .find(btn => btn.selected); - } - - hostData() { - return { - style: { - 'transform': this.transform - }, - class: { - 'animated': this.animated, - } - }; - } -} diff --git a/packages/core/src/components/tabbar/readme.md b/packages/core/src/components/tabbar/readme.md index 0ed636e12e..855fd4da55 100644 --- a/packages/core/src/components/tabbar/readme.md +++ b/packages/core/src/components/tabbar/readme.md @@ -22,6 +22,11 @@ string string +#### scrollable + +any + + #### selectedTab any @@ -54,6 +59,11 @@ string string +#### scrollable + +any + + #### selectedTab any diff --git a/packages/core/src/components/tabbar/tabbar.tsx b/packages/core/src/components/tabbar/tabbar.tsx index 7ce1667ba3..68743ad001 100644 --- a/packages/core/src/components/tabbar/tabbar.tsx +++ b/packages/core/src/components/tabbar/tabbar.tsx @@ -1,6 +1,6 @@ -import { Component, Listen, Prop, State } from '@stencil/core'; -import { createThemedClasses } from '../../utils/theme'; - +import {Component, Element, Listen, Prop, PropDidChange, State} from '@stencil/core'; +import {createThemedClasses} from '../../utils/theme'; +import {DomController} from "../../index"; @Component({ tag: 'ion-tabbar', @@ -12,11 +12,27 @@ export class Tabbar { mode: string; color: string; + @Element() el: HTMLElement; + + @State() canScrollLeft: boolean = false; + @State() canScrollRight: boolean = false; + @State() hidden = false; + @Prop({ context: 'dom' }) dom: DomController; @Prop() placement = 'bottom'; - @Prop() tabs: HTMLIonTabElement[]; @Prop() selectedTab: HTMLIonTabElement; + @Prop() scrollable:Boolean; + @Prop() tabs: HTMLIonTabElement[]; + + private scrollEl: HTMLIonScrollElement; + + @PropDidChange('selectedTab') + selectedTabChanged() { + this.scrollable && this.scrollToSelectedButton(); + this.highlight && this.updateHighlight(); + } + @Prop() layout: string = 'icon-top'; @Prop() highlight: boolean = false; @Prop() translucent: boolean = false; @@ -33,6 +49,46 @@ export class Tabbar { } } + @Listen('window:resize') + onResize() { + this.highlight && this.updateHighlight(); + } + + @Listen('ionTabButtonDidLoad') + @Listen('ionTabButtonDidUnload') + onTabButtonLoad() { + this.scrollable && this.updateBoundaries(); + this.highlight && this.updateHighlight() + } + + protected analyzeTabs() { + const tabs: HTMLIonTabButtonElement[] = Array.from(document.querySelectorAll('ion-tab-button')), + scrollLeft: number = this.scrollEl.scrollLeft, + tabsWidth: number = this.scrollEl.clientWidth; + let previous: {tab: HTMLIonTabButtonElement, amount: number}, + next: {tab: HTMLIonTabButtonElement, amount: number}; + + tabs.forEach((tab: HTMLIonTabButtonElement) => { + const left: number = tab.offsetLeft, + right: number = left + tab.offsetWidth; + + if (left < scrollLeft) { + previous = {tab, amount: left}; + } + + if (!next && right > (tabsWidth + scrollLeft)) { + let amount = right - tabsWidth; + next = {tab, amount}; + } + }); + + return {previous, next}; + } + + private getSelectedButton(): HTMLIonTabButtonElement { + return Array.from(this.el.querySelectorAll('ion-tab-button')) + .find(btn => btn.selected); + } hostData() { const themedClasses = this.translucent ? createThemedClasses(this.mode, this.color, 'tabbar-translucent') : {}; @@ -43,6 +99,7 @@ export class Tabbar { const hostClasses = { ...themedClasses, 'tabbar-hidden': this.hidden, + 'scrollable': this.scrollable, [layoutClass]: true, [placementClass]: true }; @@ -54,16 +111,89 @@ export class Tabbar { } render() { - const selectedTab = this.selectedTab; - const dom = this.tabs.map(tab => ( - - - )); - if (this.highlight) { - dom.push(); + const selectedTab = this.selectedTab, + ionTabbarHighlight = this.highlight ?
as HTMLElement : null, + tabButtons = this.tabs.map(tab => ); + + + if (this.scrollable) { + return [ + this.scrollByTab('left')} fill='clear' class={{inactive: !this.canScrollLeft}}> + + , + { + this.scrollEl = scrollEl; + }}> + {tabButtons} + {ionTabbarHighlight} + , + this.scrollByTab('right')} fill='clear' class={{inactive: !this.canScrollRight}}> + + + ] + } else { + return [ + ...tabButtons, + ionTabbarHighlight + ] } - return dom; + } + + protected scrollToSelectedButton() { + this.dom.read(() => { + const activeTabButton: HTMLIonTabButtonElement = this.getSelectedButton(); + + if (activeTabButton) { + const scrollLeft: number = this.scrollEl.scrollLeft, + tabsWidth: number = this.scrollEl.clientWidth, + left: number = activeTabButton.offsetLeft, + right: number = left + activeTabButton.offsetWidth; + + let amount; + + if (right > (tabsWidth + scrollLeft)) { + amount = right - tabsWidth; + } else if (left < scrollLeft) { + amount = left; + } + + if (amount !== undefined) { + this.scrollEl.scrollToPoint(amount, 0, 250).then(() => { + this.updateBoundaries(); + }); + } + } + }); + } + + scrollByTab(direction: 'left' | 'right') { + this.dom.read(() => { + const {previous, next} = this.analyzeTabs(), + info = direction === 'right' ? next : previous, + amount = info && info.amount; + + if (info) { + this.scrollEl.scrollToPoint(amount, 0, 250).then(() => { + this.updateBoundaries(); + }); + } + }); + } + + updateBoundaries() { + this.canScrollLeft = this.scrollEl.scrollLeft != 0; + this.canScrollRight = this.scrollEl.scrollLeft < (this.scrollEl.scrollWidth - this.scrollEl.offsetWidth); + } + + updateHighlight() { + this.dom.read(() => { + const btn = this.getSelectedButton(), + ionTabbarHighlight:HTMLElement = this.highlight && this.el.querySelector('div.tabbar-highlight') as HTMLElement; + + if (btn && ionTabbarHighlight) { + ionTabbarHighlight.style.transform = `translate3d(${btn.offsetLeft}px,0,0) scaleX(${btn.offsetWidth})`; + } + }); } } diff --git a/packages/core/src/components/tabs/readme.md b/packages/core/src/components/tabs/readme.md index 7090d4081d..cc65d97de2 100644 --- a/packages/core/src/components/tabs/readme.md +++ b/packages/core/src/components/tabs/readme.md @@ -95,6 +95,11 @@ components to switch to `TabsRoot3`: string +#### scrollable + +boolean + + #### tabbarHidden boolean @@ -127,6 +132,11 @@ boolean string +#### scrollable + +boolean + + #### tabbarHidden boolean diff --git a/packages/core/src/components/tabs/tabs.ios.scss b/packages/core/src/components/tabs/tabs.ios.scss index f8e34283ee..78d62cfc26 100644 --- a/packages/core/src/components/tabs/tabs.ios.scss +++ b/packages/core/src/components/tabs/tabs.ios.scss @@ -20,7 +20,7 @@ border-bottom: $tabs-ios-border; } -.tabbar-ios > ion-tab-highlight { +.tabbar-ios .tabbar-highlight { background: $tabs-ios-tab-color-active; } diff --git a/packages/core/src/components/tabs/tabs.md.scss b/packages/core/src/components/tabs/tabs.md.scss index 69f4de4740..239939d99d 100644 --- a/packages/core/src/components/tabs/tabs.md.scss +++ b/packages/core/src/components/tabs/tabs.md.scss @@ -1,7 +1,6 @@ @import "./tabs"; @import "./tabs.md.vars"; - .tabbar-md { height: $tabs-md-tab-height; @@ -11,7 +10,7 @@ contain: strict; } -.tabbar-md > ion-tab-highlight { +.tabbar-md .tabbar-highlight { background: $tabs-md-tab-color-active; } @@ -38,16 +37,30 @@ fill: $tabs-md-tab-icon-color; } +.tabbar-md.scrollable ion-scroll { + max-width: 650px; + margin: 0 8px; +} + +.tabbar-md.scrollable ion-tab-button { + overflow: hidden; + flex: 1 0 auto; +} + .tabbar-md .tab-selected { color: $tabs-md-tab-text-color-active; fill: $tabs-md-tab-icon-color-active; } - // Material Design Tab Button Text // -------------------------------------------------- +.tabbar-md.placement-top .tab-selected .tab-button-icon, +.tabbar-md.placement-top .tab-selected .tab-button-text { + transform: inherit; +} + .tabbar-md .tab-button-text { @include margin($tabs-md-tab-text-margin-top, $tabs-md-tab-text-margin-end, $tabs-md-tab-text-margin-bottom, $tabs-md-tab-text-margin-start); @@ -74,7 +87,6 @@ @include margin(-2px, null, null, null); } - // Material Design Tab Button Icon // -------------------------------------------------- @@ -109,7 +121,6 @@ @include transform(translate3d($tabs-md-tab-icon-left-transform-x-active, $tabs-md-tab-icon-left-transform-y-active, $tabs-md-tab-icon-left-transform-z-active)); } - // Material Design Tab with Icon or Title only // -------------------------------------------------- @@ -139,13 +150,12 @@ fill: $color-contrast; } - .tabbar-md-#{$color-name} ion-tab-highlight { + .tabbar-md-#{$color-name} .tabbar-highlight { background: $color-contrast; } } - // Material Design Tabbar Color Generation // -------------------------------------------------- diff --git a/packages/core/src/components/tabs/tabs.scss b/packages/core/src/components/tabs/tabs.scss index 39fcec3b43..29251866ff 100644 --- a/packages/core/src/components/tabs/tabs.scss +++ b/packages/core/src/components/tabs/tabs.scss @@ -45,6 +45,7 @@ ion-tabbar { display: flex; justify-content: center; + align-items: center; order: 1; @@ -165,11 +166,10 @@ ion-tab-button { @include position(null, calc(50% - 50px), null, null); } - // Tab Highlight // -------------------------------------------------- -ion-tab-highlight { +.tabbar-highlight { @include position(null, null, 0, 0); @include transform-origin(0, 0); @@ -191,11 +191,27 @@ ion-tab-highlight { } } -.placement-top > ion-tab-highlight { +.placement-top .tabbar-highlight { bottom: 0; } -.placement-bottom > ion-tab-highlight { +.placement-bottom .tabbar-highlight { top: 0; } +// Overflow Scrolling +// -------------------------------------------------- + +ion-tabbar.scrollable ion-scroll { + overflow: hidden; +} + +ion-tabbar.scrollable .scroll-inner { + position: relative; + display: flex; + flex-direction: row; +} + +ion-tabbar.scrollable ion-button.inactive { + visibility: hidden; +} diff --git a/packages/core/src/components/tabs/tabs.tsx b/packages/core/src/components/tabs/tabs.tsx index 17381fd31c..ef9a6008d5 100644 --- a/packages/core/src/components/tabs/tabs.tsx +++ b/packages/core/src/components/tabs/tabs.tsx @@ -55,6 +55,8 @@ export class Tabs { */ @Prop() translucent: boolean = false; + @Prop() scrollable: boolean = false; + /** * @output {any} Emitted when the tab changes. */ @@ -102,7 +104,6 @@ export class Tabs { } selectedTab.selected = true; - console.log('HEY'); // The same selected was selected // we need to set root in the nested ion-nav if it exist if (this.selectedTab === selectedTab) { @@ -165,7 +166,6 @@ export class Tabs { @Method() getRoutes(): RouterEntries { - debugger; const a = this.tabs.map(t => { return { path: t.getPath(), @@ -231,7 +231,8 @@ export class Tabs { highlight={this.tabbarHighlight} placement={this.tabbarPlacement} layout={this.tabbarLayout} - translucent={this.translucent}> + translucent={this.translucent} + scrollable={this.scrollable}> ); } diff --git a/packages/core/src/components/tabs/test/scroll/index.html b/packages/core/src/components/tabs/test/scroll/index.html new file mode 100644 index 0000000000..78278f9552 --- /dev/null +++ b/packages/core/src/components/tabs/test/scroll/index.html @@ -0,0 +1,34 @@ + + + + + Tab - Scroll + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/core/stencil.config.js b/packages/core/stencil.config.js index f0968c106d..c6e93dc8ea 100644 --- a/packages/core/stencil.config.js +++ b/packages/core/stencil.config.js @@ -36,7 +36,7 @@ exports.config = { { components: ['ion-spinner'] }, { components: ['ion-split-pane'] }, { components: ['ion-range', 'ion-range-knob']}, - { components: ['ion-tabs', 'ion-tab', 'ion-tabbar', 'ion-tab-button', 'ion-tab-highlight'] }, + { components: ['ion-tabs', 'ion-tab', 'ion-tabbar', 'ion-tab-button'] }, { components: ['ion-toggle'] }, { components: ['ion-nav'] }, { components: ['ion-toast', 'ion-toast-controller'] },