feat(tabs): scrollable tabs

This commit is contained in:
Ross Gerbasi
2017-12-18 21:01:09 -06:00
committed by Adam Bradley
parent a881eb1b82
commit ec2bf8bfbe
15 changed files with 264 additions and 152 deletions

View File

@ -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 { import {
Tab as IonTab Tab as IonTab
} from './components/tab/tab'; } from './components/tab/tab';
@ -2805,6 +2775,7 @@ declare global {
highlight?: boolean; highlight?: boolean;
layout?: string; layout?: string;
placement?: string; placement?: string;
scrollable?: Boolean;
selectedTab?: HTMLIonTabElement; selectedTab?: HTMLIonTabElement;
tabs?: HTMLIonTabElement[]; tabs?: HTMLIonTabElement[];
translucent?: boolean; translucent?: boolean;
@ -2838,6 +2809,7 @@ declare global {
namespace JSXElements { namespace JSXElements {
export interface IonTabsAttributes extends HTMLAttributes { export interface IonTabsAttributes extends HTMLAttributes {
name?: string; name?: string;
scrollable?: boolean;
tabbarHidden?: boolean; tabbarHidden?: boolean;
tabbarHighlight?: boolean; tabbarHighlight?: boolean;
tabbarLayout?: string; tabbarLayout?: string;

View File

@ -148,7 +148,7 @@ export class Scroll {
if (easedT < 1) { if (easedT < 1) {
// do not use DomController here // do not use DomController here
// must use nativeRaf in order to fire in the next frame // must use nativeRaf in order to fire in the next frame
this.dom.raf(step); self.dom.raf(step);
} else { } else {
stopScroll = true; stopScroll = true;

View File

@ -31,6 +31,12 @@ any
## Events ## Events
#### ionTabButtonDidLoad
#### ionTabButtonDidUnload
#### ionTabbarClick #### ionTabbarClick

View File

@ -1,4 +1,4 @@
import { Component, Event, EventEmitter, Listen, Prop } from '@stencil/core'; import {Component, Element, Event, EventEmitter, Listen, Prop} from '@stencil/core';
@Component({ @Component({
@ -6,10 +6,22 @@ import { Component, Event, EventEmitter, Listen, Prop } from '@stencil/core';
}) })
export class TabButton { export class TabButton {
@Element() el: HTMLElement;
@Prop() selected = false; @Prop() selected = false;
@Prop() tab: HTMLIonTabElement; @Prop() tab: HTMLIonTabElement;
@Event() ionTabbarClick: EventEmitter; @Event() ionTabbarClick: EventEmitter;
@Event() ionTabButtonDidLoad: EventEmitter;
@Event() ionTabButtonDidUnload: EventEmitter;
componentDidLoad() {
this.ionTabButtonDidLoad.emit({ button: this });
}
componentDidUnload() {
this.ionTabButtonDidUnload.emit({ button: this });
}
@Listen('click') @Listen('click')
protected onClick(ev: UIEvent) { protected onClick(ev: UIEvent) {
@ -21,7 +33,7 @@ export class TabButton {
const selected = this.selected; const selected = this.selected;
const tab = this.tab; const tab = this.tab;
const hasTitle = !!tab.title; const hasTitle = !!tab.title;
const hasIcon = !! tab.icon; const hasIcon = !!tab.icon;
const hasTitleOnly = (hasTitle && !hasIcon); const hasTitleOnly = (hasTitle && !hasIcon);
const hasIconOnly = (hasIcon && !hasTitle); const hasIconOnly = (hasIcon && !hasTitle);
const hasBadge = !!tab.badge; const hasBadge = !!tab.badge;

View File

@ -1,25 +0,0 @@
# ion-tab-highlight
<!-- Auto Generated Below -->
## Properties
#### selectedTab
any
## Attributes
#### selectedTab
any
----------------------------------------------
*Built by [StencilJS](https://stenciljs.com/)*

View File

@ -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,
}
};
}
}

View File

@ -22,6 +22,11 @@ string
string string
#### scrollable
any
#### selectedTab #### selectedTab
any any
@ -54,6 +59,11 @@ string
string string
#### scrollable
any
#### selectedTab #### selectedTab
any any

View File

@ -1,6 +1,6 @@
import { Component, Listen, Prop, State } from '@stencil/core'; import {Component, Element, Listen, Prop, PropDidChange, State} from '@stencil/core';
import { createThemedClasses } from '../../utils/theme'; import {createThemedClasses} from '../../utils/theme';
import {DomController} from "../../index";
@Component({ @Component({
tag: 'ion-tabbar', tag: 'ion-tabbar',
@ -12,11 +12,27 @@ export class Tabbar {
mode: string; mode: string;
color: string; color: string;
@Element() el: HTMLElement;
@State() canScrollLeft: boolean = false;
@State() canScrollRight: boolean = false;
@State() hidden = false; @State() hidden = false;
@Prop({ context: 'dom' }) dom: DomController;
@Prop() placement = 'bottom'; @Prop() placement = 'bottom';
@Prop() tabs: HTMLIonTabElement[];
@Prop() selectedTab: 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() layout: string = 'icon-top';
@Prop() highlight: boolean = false; @Prop() highlight: boolean = false;
@Prop() translucent: 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() { hostData() {
const themedClasses = this.translucent ? createThemedClasses(this.mode, this.color, 'tabbar-translucent') : {}; const themedClasses = this.translucent ? createThemedClasses(this.mode, this.color, 'tabbar-translucent') : {};
@ -43,6 +99,7 @@ export class Tabbar {
const hostClasses = { const hostClasses = {
...themedClasses, ...themedClasses,
'tabbar-hidden': this.hidden, 'tabbar-hidden': this.hidden,
'scrollable': this.scrollable,
[layoutClass]: true, [layoutClass]: true,
[placementClass]: true [placementClass]: true
}; };
@ -54,16 +111,89 @@ export class Tabbar {
} }
render() { render() {
const selectedTab = this.selectedTab; const selectedTab = this.selectedTab,
const dom = this.tabs.map(tab => ( ionTabbarHighlight = this.highlight ? <div class='animated tabbar-highlight'/> as HTMLElement : null,
<ion-tab-button tabButtons = this.tabs.map(tab => <ion-tab-button tab={tab} selected={selectedTab === tab}/>);
tab={tab}
selected={selectedTab === tab}>
</ion-tab-button> if (this.scrollable) {
)); return [
if (this.highlight) { <ion-button onClick={() => this.scrollByTab('left')} fill='clear' class={{inactive: !this.canScrollLeft}}>
dom.push(<ion-tab-highlight selectedTab={selectedTab}></ion-tab-highlight>); <ion-icon name='arrow-dropleft'/>
</ion-button>,
<ion-scroll
ref={(scrollEl: HTMLIonScrollElement) => {
this.scrollEl = scrollEl;
}}>
{tabButtons}
{ionTabbarHighlight}
</ion-scroll>,
<ion-button onClick={() => this.scrollByTab('right')} fill='clear' class={{inactive: !this.canScrollRight}}>
<ion-icon name='arrow-dropright'/>
</ion-button>
]
} 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})`;
}
});
} }
} }

View File

@ -95,6 +95,11 @@ components to switch to `TabsRoot3`:
string string
#### scrollable
boolean
#### tabbarHidden #### tabbarHidden
boolean boolean
@ -127,6 +132,11 @@ boolean
string string
#### scrollable
boolean
#### tabbarHidden #### tabbarHidden
boolean boolean

View File

@ -20,7 +20,7 @@
border-bottom: $tabs-ios-border; border-bottom: $tabs-ios-border;
} }
.tabbar-ios > ion-tab-highlight { .tabbar-ios .tabbar-highlight {
background: $tabs-ios-tab-color-active; background: $tabs-ios-tab-color-active;
} }

View File

@ -1,7 +1,6 @@
@import "./tabs"; @import "./tabs";
@import "./tabs.md.vars"; @import "./tabs.md.vars";
.tabbar-md { .tabbar-md {
height: $tabs-md-tab-height; height: $tabs-md-tab-height;
@ -11,7 +10,7 @@
contain: strict; contain: strict;
} }
.tabbar-md > ion-tab-highlight { .tabbar-md .tabbar-highlight {
background: $tabs-md-tab-color-active; background: $tabs-md-tab-color-active;
} }
@ -38,16 +37,30 @@
fill: $tabs-md-tab-icon-color; 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 { .tabbar-md .tab-selected {
color: $tabs-md-tab-text-color-active; color: $tabs-md-tab-text-color-active;
fill: $tabs-md-tab-icon-color-active; fill: $tabs-md-tab-icon-color-active;
} }
// Material Design Tab Button Text // 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 { .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); @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); @include margin(-2px, null, null, null);
} }
// Material Design Tab Button Icon // 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)); @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 // Material Design Tab with Icon or Title only
// -------------------------------------------------- // --------------------------------------------------
@ -139,13 +150,12 @@
fill: $color-contrast; fill: $color-contrast;
} }
.tabbar-md-#{$color-name} ion-tab-highlight { .tabbar-md-#{$color-name} .tabbar-highlight {
background: $color-contrast; background: $color-contrast;
} }
} }
// Material Design Tabbar Color Generation // Material Design Tabbar Color Generation
// -------------------------------------------------- // --------------------------------------------------

View File

@ -45,6 +45,7 @@ ion-tabbar {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center;
order: 1; order: 1;
@ -165,11 +166,10 @@ ion-tab-button {
@include position(null, calc(50% - 50px), null, null); @include position(null, calc(50% - 50px), null, null);
} }
// Tab Highlight // Tab Highlight
// -------------------------------------------------- // --------------------------------------------------
ion-tab-highlight { .tabbar-highlight {
@include position(null, null, 0, 0); @include position(null, null, 0, 0);
@include transform-origin(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; bottom: 0;
} }
.placement-bottom > ion-tab-highlight { .placement-bottom .tabbar-highlight {
top: 0; 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;
}

View File

@ -55,6 +55,8 @@ export class Tabs {
*/ */
@Prop() translucent: boolean = false; @Prop() translucent: boolean = false;
@Prop() scrollable: boolean = false;
/** /**
* @output {any} Emitted when the tab changes. * @output {any} Emitted when the tab changes.
*/ */
@ -102,7 +104,6 @@ export class Tabs {
} }
selectedTab.selected = true; selectedTab.selected = true;
console.log('HEY');
// The same selected was selected // The same selected was selected
// we need to set root in the nested ion-nav if it exist // we need to set root in the nested ion-nav if it exist
if (this.selectedTab === selectedTab) { if (this.selectedTab === selectedTab) {
@ -165,7 +166,6 @@ export class Tabs {
@Method() @Method()
getRoutes(): RouterEntries { getRoutes(): RouterEntries {
debugger;
const a = this.tabs.map(t => { const a = this.tabs.map(t => {
return { return {
path: t.getPath(), path: t.getPath(),
@ -231,7 +231,8 @@ export class Tabs {
highlight={this.tabbarHighlight} highlight={this.tabbarHighlight}
placement={this.tabbarPlacement} placement={this.tabbarPlacement}
layout={this.tabbarLayout} layout={this.tabbarLayout}
translucent={this.translucent}> translucent={this.translucent}
scrollable={this.scrollable}>
</ion-tabbar> </ion-tabbar>
); );
} }

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Tab - 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="/dist/ionic.js"></script>
</head>
<body>
<ion-app>
<ion-tabs scrollable="true" tabbar-placement="top" tabbar-highlight="true" class="tabbar-md-light">
<ion-tab title="Plain List" icon="star"></ion-tab>
<ion-tab title="Schedule Long Text" icon="globe"></ion-tab>
<ion-tab title="Stopwatch" icon="logo-facebook"></ion-tab>
<ion-tab title="Messages" icon="chatboxes"></ion-tab>
<ion-tab title="Messages 2" icon="chatboxes"></ion-tab>
<ion-tab title="Messages 3" icon="chatboxes"></ion-tab>
<ion-tab title="Messages 4" icon="chatboxes"></ion-tab>
<ion-tab title="Messages 5" icon="chatboxes"></ion-tab>
<ion-tab title="Messages 6" icon="chatboxes"></ion-tab>
<ion-tab title="Messages 7" icon="chatboxes"></ion-tab>
<ion-tab title="Messages 8" icon="chatboxes"></ion-tab>
<ion-tab title="Messages 9" icon="chatboxes"></ion-tab>
<ion-tab title="Messages 10" icon="chatboxes"></ion-tab>
<ion-tab title="Messages 11" icon="chatboxes"></ion-tab>
<ion-tab title="Messages 12" icon="chatboxes"></ion-tab>
</ion-tabs>
<ion-nav-controller></ion-nav-controller>
</ion-app>
</body>
</html>

View File

@ -36,7 +36,7 @@ exports.config = {
{ components: ['ion-spinner'] }, { components: ['ion-spinner'] },
{ components: ['ion-split-pane'] }, { components: ['ion-split-pane'] },
{ components: ['ion-range', 'ion-range-knob']}, { 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-toggle'] },
{ components: ['ion-nav'] }, { components: ['ion-nav'] },
{ components: ['ion-toast', 'ion-toast-controller'] }, { components: ['ion-toast', 'ion-toast-controller'] },