feat(tabs): adds tabs

This commit is contained in:
Manu Mtz.-Almeida
2017-11-02 23:42:46 +01:00
parent 99f4f71d93
commit 67b9be4045
27 changed files with 1158 additions and 490 deletions

View File

@ -1751,6 +1751,7 @@ declare global {
canGoBack?: any,
canSwipeBack?: any,
getFirstView?: any,
resize?: any,
root?: any,
delegate?: any
}
@ -2782,40 +2783,70 @@ declare global {
}
}
import { TabBar as IonTabBar } from './components/tabs/tab-bar';
import { PageTab as PageTab } from './components/tabs/page-tab';
interface HTMLIonTabBarElement extends IonTabBar, HTMLElement {
interface HTMLPageTabElement extends PageTab, HTMLElement {
}
declare var HTMLIonTabBarElement: {
prototype: HTMLIonTabBarElement;
new (): HTMLIonTabBarElement;
declare var HTMLPageTabElement: {
prototype: HTMLPageTabElement;
new (): HTMLPageTabElement;
};
declare global {
interface HTMLElementTagNameMap {
"ion-tab-bar": HTMLIonTabBarElement;
"page-tab": HTMLPageTabElement;
}
interface ElementTagNameMap {
"ion-tab-bar": HTMLIonTabBarElement;
"page-tab": HTMLPageTabElement;
}
namespace JSX {
interface IntrinsicElements {
"ion-tab-bar": JSXElements.IonTabBarAttributes;
"page-tab": JSXElements.PageTabAttributes;
}
}
namespace JSXElements {
export interface IonTabBarAttributes extends HTMLAttributes {
export interface PageTabAttributes extends HTMLAttributes {
mode?: string,
color?: string,
tabs?: any,
onTabSelected?: any,
selectedIndex?: number,
tabsLayout?: string
}
}
}
import { TabButton as IonTabButton } from './components/tabs/tab-button';
import { TabBar as IonTabbar } from './components/tabs/tab-bar';
interface HTMLIonTabbarElement extends IonTabbar, HTMLElement {
}
declare var HTMLIonTabbarElement: {
prototype: HTMLIonTabbarElement;
new (): HTMLIonTabbarElement;
};
declare global {
interface HTMLElementTagNameMap {
"ion-tabbar": HTMLIonTabbarElement;
}
interface ElementTagNameMap {
"ion-tabbar": HTMLIonTabbarElement;
}
namespace JSX {
interface IntrinsicElements {
"ion-tabbar": JSXElements.IonTabbarAttributes;
}
}
namespace JSXElements {
export interface IonTabbarAttributes extends HTMLAttributes {
mode?: string,
color?: string,
placement?: string,
tabs?: any,
selectedTab?: any,
layout?: string,
highlight?: boolean
}
}
}
import { TabbarButton as IonTabButton } from './components/tabs/tab-button';
interface HTMLIonTabButtonElement extends IonTabButton, HTMLElement {
}
@ -2840,10 +2871,8 @@ declare global {
mode?: string,
color?: string,
tab?: any,
layout?: string,
selectedIndex?: number,
index?: number
selected?: boolean,
tab?: any
}
}
}
@ -2873,6 +2902,7 @@ declare global {
mode?: string,
color?: string,
selectedTab?: any
}
}
}
@ -2902,16 +2932,23 @@ declare global {
mode?: string,
color?: string,
root?: string,
_setActive?: any,
resize?: any,
goToRoot?: any,
getActive?: any,
getNav?: any,
btnId?: string,
root?: any,
rootParams?: any,
tabTitle?: string,
tabIcon?: string,
tabBadge?: string,
tabBadgeStyle?: string,
urlPath?: string,
title?: string,
icon?: string,
badge?: string,
badgeStyle?: string,
enabled?: boolean,
shown?: boolean,
show?: boolean,
tabsHideOnSubPages?: boolean,
onSelected?: any
selected?: boolean
}
}
}
@ -2941,10 +2978,18 @@ declare global {
mode?: string,
color?: string,
tabsLayout?: string,
tabsPlacement?: string,
tabsHighlight?: boolean,
ionChange?: any
select?: any,
getByIndex?: any,
getSelected?: any,
getIndex?: any,
getTabs?: any,
previousTab?: any,
resize?: any,
name?: string,
tabbarHidden?: boolean,
tabbarLayout?: string,
tabbarPlacement?: string,
tabbarHighlight?: boolean
}
}
}

View File

@ -224,7 +224,6 @@ sub {
ion-app,
ion-nav,
ion-tab,
ion-tabs,
ion-page,
.ion-page {
@ -240,21 +239,37 @@ ion-page,
contain: strict;
}
ion-nav,
ion-tab,
ion-tabs {
overflow: hidden;
display: flex;
flex-direction: column;
}
.tabs-inner {
position: relative;
flex: 1;
contain: strict;
}
ion-tab {
@include position(0, null, null, 0);
position: absolute;
z-index: -1;
display: none;
width: 100%;
height: 100%;
contain: strict;
}
ion-tab.show-tab {
z-index: $z-index-page-container;
display: block;
}
// Page Container Structure
// --------------------------------------------------

View File

@ -19,6 +19,8 @@ ion-backdrop {
background-color: $backdrop-color;
opacity: .01;
transform: translateZ(0);
contain: strict;
}
ion-backdrop.backdrop-no-tappable {

View File

@ -428,7 +428,7 @@ export class Menu {
protected hostData() {
const typeClass = 'menu-type-' + this.type;
return {
role: 'navigation',
role: 'complementary',
class: {
'menu-enabled': this.isActive(),
'menu-side-right': this.isRightSide,

View File

@ -36,7 +36,6 @@ export class IonNav implements Nav {
componentDidLoad() {
componentDidLoadImpl(this);
}
getViews(): ViewController[] {
@ -118,6 +117,11 @@ export class IonNav implements Nav {
return getFirstView(this);
}
@Method()
resize() {
console.log('resize content');
}
@Listen('navInit')
navInitialized(event: CustomEvent) {
navInitializedImpl(this, event);
@ -134,7 +138,6 @@ export function componentDidLoadImpl(nav: Nav) {
if (nav.root) {
nav.setRoot(nav.root);
}
}
export function pushImpl(nav: Nav, component: any, data: any, opts: NavOptions) {

View File

@ -17,17 +17,30 @@ export class PageOne {
}
protected render() {
return [<ion-header>
<ion-toolbar>
<ion-title>Page One</ion-title>
</ion-toolbar>
</ion-header>,
<ion-content>
Page One Content
<div>
<ion-button onClick={() => this.nextPage()}>Go to Page Two</ion-button>
</div>
</ion-content>
return [
<ion-header>
<ion-toolbar>
<ion-title>Page One</ion-title>
</ion-toolbar>
</ion-header>,
<ion-content>
<div>
Page One Content
<ion-button onClick={() => this.nextPage()}>Go to Page Two</ion-button>
<f></f>
<f></f>
<f></f>
<f></f>
<f></f>
<f></f>
<f></f>
<f></f>
<f></f>
<f></f>
<f></f>
<f></f>
</div>
</ion-content>
];
}
}

View File

@ -27,7 +27,7 @@ export class PageTwo {
<ion-title>Page Two</ion-title>
</ion-navbar>
</ion-header>,
<ion-content>
<ion-content fullscreen={true}>
Page Two Content
<div>
<ion-button onClick={() => this.nextPage()}>Go to Page Three</ion-button>

View File

@ -12,5 +12,18 @@
<ion-app>
<ion-nav root="page-one"></ion-nav>
</ion-app>
<style>
f {
display: block;
margin: 15px auto;
max-width: 150px;
height: 150px;
background: blue;
}
f:last-of-type {
background: yellow;
}
</style>
</body>
</html>

View File

@ -396,7 +396,7 @@ export class PickerColumnCmp {
}
}
hostData() {
protected hostData() {
return {
class: {
'picker-opts-left': this.col.align === 'left',
@ -405,7 +405,7 @@ export class PickerColumnCmp {
style: {
'max-width': this.col.columnWidth
}
}
};
}
protected render() {
@ -424,7 +424,7 @@ export class PickerColumnCmp {
if (col.prefix) {
results.push(
<div class="picker-prefix" style={{width: col.prefixWidth}}>
<div class='picker-prefix' style={{width: col.prefixWidth}}>
{col.prefix}
</div>
);
@ -445,7 +445,7 @@ export class PickerColumnCmp {
'attachTo': 'parent',
'block': this.activeBlock
}}></ion-gesture>,
<div class="picker-opts" style={{maxWidth: col.optionsWidth}}>
<div class='picker-opts' style={{maxWidth: col.optionsWidth}}>
{options.map((o, index) =>
<button
class={{'picker-opt': true, 'picker-opt-disabled': o.disabled}}
@ -459,7 +459,7 @@ export class PickerColumnCmp {
if (col.suffix) {
results.push(
<div class="picker-suffix" style={{width: col.suffixWidth}}>
<div class='picker-suffix' style={{width: col.suffixWidth}}>
{col.suffix}
</div>
);
@ -472,4 +472,4 @@ export class PickerColumnCmp {
export const PICKER_OPT_SELECTED = 'picker-opt-selected';
export const DECELERATION_FRICTION = 0.97;
export const FRAME_MS = (1000 / 60);
export const MAX_PICKER_SPEED = 60;
export const MAX_PICKER_SPEED = 60;

View File

@ -51,4 +51,4 @@
</ion-page>
</ion-app>
</body>
</html>
</html>

View File

@ -0,0 +1,71 @@
import { Component, Element } from '@stencil/core';
import { HTMLIonTabsElement } from '../../index';
@Component({
tag: 'page-tab'
})
export class PageTab {
@Element() element: HTMLElement;
getTabs() {
return this.element.closest('ion-tabs') as HTMLIonTabsElement;
}
setLayout(value: string) {
this.getTabs().tabbarLayout = value;
}
setPlacement(value: string) {
this.getTabs().tabbarPlacement = value;
}
setHidden(value: boolean) {
this.getTabs().tabbarHidden = value;
}
setHighlight(value: boolean) {
this.getTabs().tabbarHighlight = value;
}
protected render() {
return [
<ion-header>
<ion-toolbar>
<ion-title>Tab page</ion-title>
</ion-toolbar>
</ion-header>,
<ion-content>
<p>
<h2>Set tabbar layout</h2>
<ion-button onClick={() => this.setLayout('icon-top')}>icon-top</ion-button>
<ion-button onClick={() => this.setLayout('icon-start')}>icon-start</ion-button>
<ion-button onClick={() => this.setLayout('icon-end')}>icon-end</ion-button>
<ion-button onClick={() => this.setLayout('icon-bottom')}>icon-bottom</ion-button>
<ion-button onClick={() => this.setLayout('icon-hide')}>icon-hide</ion-button>
<ion-button onClick={() => this.setLayout('title-hide')}>title-hide</ion-button>
</p>
<p>
<h2>Set tabbar placement</h2>
<ion-button onClick={() => this.setPlacement('top')}>top</ion-button>
<ion-button onClick={() => this.setPlacement('bottom')}>bottom</ion-button>
</p>
<p>
<h2>Set tabbar hidden</h2>
<ion-button onClick={() => this.setHidden(true)}>hidden</ion-button>
<ion-button onClick={() => this.setHidden(false)}>visible</ion-button>
</p>
<p>
<h2>Set tabbar highlight</h2>
<ion-button onClick={() => this.setHighlight(true)}>enabled</ion-button>
<ion-button onClick={() => this.setHighlight(false)}>disabled</ion-button>
</p>
<f></f>
<f></f>
<f></f>
<f></f>
<f></f>
</ion-content>
];
}
}

View File

@ -1,69 +1,58 @@
import { Component, Prop } from '@stencil/core';
import { Component, Listen, Prop, State } from '@stencil/core';
import { HTMLIonTabElement } from '../../index';
@Component({
tag: 'ion-tab-bar',
tag: 'ion-tabbar',
host: {
theme: 'tabbar'
}
})
export class TabBar {
/**
* @input {string} The color to use from your Sass `$colors` map.
* Default options are: `"primary"`, `"secondary"`, `"danger"`, `"light"`, and `"dark"`.
* For more information, see [Theming your App](/docs/theming/theming-your-app).
*/
@Prop() color: string;
@State() hidden = false;
/**
* @input {string} The mode determines which platform styles to use.
* Possible values are: `"ios"`, `"md"`, or `"wp"`.
* For more information, see [Platform Styles](/docs/theming/platform-specific-styles).
*/
@Prop() mode: 'ios' | 'md' | 'wp';
@Prop() placement = 'bottom';
@Prop() tabs: HTMLIonTabElement[];
@Prop() selectedTab: HTMLIonTabElement;
@Prop() layout: string = 'icon-top';
@Prop() highlight: boolean = false;
@Prop() tabs: any;
@Listen('body:keyboardWillHide')
protected onKeyboardWillHide() {
setTimeout(() => this.hidden = false, 50);
}
@Prop() onTabSelected: Function;
@Prop() selectedIndex: number = 0;
/**
* @input {string} Set the tabbar layout: `icon-top`, `icon-start`, `icon-end`, `icon-bottom`, `icon-hide`, `title-hide`.
*/
@Prop() tabsLayout: string = 'icon-top';
/*
hostData() {
return {
'role': 'tablist'
class: {
'tabbar': true
}
@Listen('body:keyboardWillShow')
protected onKeyboardWillShow() {
if (this.placement === 'bottom') {
this.hidden = true;
}
}
handleTabButtonClick(tab, index) {
this.onTabSelected && this.onTabSelected(tab, index);
protected hostData() {
const layoutClass = `layout-${this.layout}`;
const placementClass = `placement-${this.placement}`;
return {
'role': 'tablist',
'class': {
'tabbar-hidden': this.hidden,
[layoutClass]: true,
[placementClass]: true
}
};
}
protected render() {
return (
<div class="tabbar" role="tablist">
{this.tabs.map((tab, index) => {
return (
<ion-tab-button role="tab"
tab={tab}
selectedIndex={this.selectedIndex}
index={index}
onClick={this.handleTabButtonClick.bind(this, tab, index)}
layout={this.tabsLayout}></ion-tab-button>
)
})}
</div>
)
const selectedTab = this.selectedTab;
const dom = this.tabs.map(tab => (
<ion-tab-button
tab={tab}
selected={selectedTab === tab}>
</ion-tab-button>
));
if (this.highlight) {
dom.push(<ion-tab-highlight selectedTab={selectedTab}></ion-tab-highlight>);
}
return dom;
}
*/
}

View File

@ -1,78 +1,62 @@
import { Component, Prop } from '@stencil/core';
import { Component, Event, EventEmitter, Listen, Prop } from '@stencil/core';
import { HTMLIonTabElement } from '../../index';
@Component({
tag: 'ion-tab-button',
host: {
theme: 'tab-button'
}
tag: 'ion-tab-button'
})
export class TabButton {
@Prop() tab: any;
export class TabbarButton {
@Prop() layout: string;
@Prop() selected: boolean = false;
@Prop() tab: HTMLIonTabElement;
@Prop() selectedIndex: number;
@Event() ionTabbarClick: EventEmitter;
@Prop() index: number;
@Listen('click')
protected onClick(ev: UIEvent) {
this.ionTabbarClick.emit(this.tab);
ev.stopPropagation();
}
hostData() {
protected hostData() {
const selected = this.selected;
const tab = this.tab;
if (!tab) return {};
// attr.id
// attr.aria-controls
const hasTitle = !!tab.tabTitle;
const hasIcon = !!tab.tabIcon && this.layout !== 'icon-hide';
const hasTitle = !!tab.title;
const hasIcon = !! tab.icon;
const hasTitleOnly = (hasTitle && !hasIcon);
const hasIconOnly = (hasIcon && !hasTitle);
const hasBadge = !!tab.tabBadge;
// class.disable-hover
// class.tab-disabled
// class.tab-hidden
const hasBadge = !!tab.badge;
return {
'aria-selected': this.selectedIndex === this.index,
'role': 'tab',
'id': tab.btnId,
'aria-selected': selected,
class: {
'tab-selected': selected,
'has-title': hasTitle,
'has-icon': hasIcon,
'has-title-only': hasTitleOnly,
'has-icon-only': hasIconOnly,
'has-badge': hasBadge
'has-badge': hasBadge,
'tab-disabled': !tab.enabled,
'tab-hidden': tab.hidden,
}
};
}
protected render() {
if (!this.tab) {
return null;
}
const items = [];
const tab = this.tab;
// TODO: Apply these on host?
/*
let { id, ariaControls, ariaSelected, hasTitle, hasIcon, hasTitleOnly,
iconOnly, hasBadge, disableHover, tabDisabled, tabHidden } = {};
*/
const items = [];
if (tab.tabIcon) {
items.push(<ion-icon class='tab-button-icon' name={tab.tabIcon}></ion-icon>);
if (tab.icon) {
items.push(<ion-icon class='tab-button-icon' name={tab.icon}></ion-icon>);
}
if (tab.tabTitle) {
items.push(<span class='tab-button-text'>{tab.tabTitle}</span>);
if (tab.title) {
items.push(<span class='tab-button-text'>{tab.title}</span>);
}
if (tab.tabBadge) {
items.push(<ion-badge class='tab-badge' color={tab.tabBadgeStyle}>{tab.tabBadge}</ion-badge>);
if (tab.badge) {
items.push(<ion-badge class='tab-badge' color={tab.badgeStyle}>{tab.badge}</ion-badge>);
}
items.push(<div class='button-effect'></div>);
return (items);
return items;
}
}

View File

@ -1,13 +1,61 @@
import { Component } from '@stencil/core';
import { Component, Element, Listen, Prop, PropDidChange, State } from '@stencil/core';
import { getParentElement } from '../../utils/helpers';
import { HTMLIonTabButtonElement, HTMLIonTabElement } from '../../index';
@Component({
tag: 'ion-tab-highlight'
})
export class TabHighlight {
protected render() {
return (
<div></div>
);
@Element() private el: HTMLElement;
@State() animated = false;
@State() transform = '';
@Prop() selectedTab: HTMLIonTabElement;
@PropDidChange('selectedTab')
selectedTabChanged() {
this.updateTransform();
}
@Listen('window:resize')
onResize() {
this.updateTransform();
}
protected ionViewDidLoad() {
this.updateTransform();
}
protected updateTransform() {
Context.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);
}
protected hostData() {
return {
style: {
'transform': this.transform
},
class: {
'animated': this.animated,
}
};
}
}

View File

@ -1,108 +1,280 @@
import { Component, Event, EventEmitter, Prop, State } from '@stencil/core';
import { Component, Element, Event, EventEmitter, Method, Prop, PropDidChange, State } from '@stencil/core';
import { HTMLIonNavElement, StencilElement } from '../../index';
import { ViewController } from '../../navigation/nav-interfaces';
/**
* @name Tab
* @description
* The Tab component, written `<ion-tab>`, is styled based on the mode and should
* be used in conjunction with the [Tabs](../Tabs/) component.
*
* Each `ion-tab` is a declarative component for a [NavController](../../../navigation/NavController/).
* Basically, each tab is a `NavController`. For more information on using
* navigation controllers take a look at the [NavController API Docs](../../../navigation/NavController/).
*
* See the [Tabs API Docs](../Tabs/) for more details on configuring Tabs.
*
* @usage
*
* To add a basic tab, you can use the following markup where the `root` property
* is the page you want to load for that tab, `tabTitle` is the optional text to
* display on the tab, and `tabIcon` is the optional [icon](../../icon/Icon/).
*
* ```html
* <ion-tabs>
* <ion-tab [root]="chatRoot" tabTitle="Chat" tabIcon="chat"></ion-tab>
* </ion-tabs>
* ```
*
* Then, in your class you can set `chatRoot` to an imported class:
*
* ```ts
* import { ChatPage } from '../chat/chat';
*
* export class Tabs {
* // here we'll set the property of chatRoot to
* // the imported class of ChatPage
* chatRoot = ChatPage;
*
* constructor() {
*
* }
* }
* ```
*
* You can also pass some parameters to the root page of the tab through
* `rootParams`. Below we pass `chatParams` to the Chat tab:
*
* ```html
* <ion-tabs>
* <ion-tab [root]="chatRoot" [rootParams]="chatParams" tabTitle="Chat" tabIcon="chat"></ion-tab>
* </ion-tabs>
* ```
*
* ```ts
* export class Tabs {
* chatRoot = ChatPage;
*
* // set some user information on chatParams
* chatParams = {
* user1: 'admin',
* user2: 'ionic'
* };
*
* constructor() {
*
* }
* }
* ```
*
* And in `ChatPage` you can get the data from `NavParams`:
*
* ```ts
* export class ChatPage {
* constructor(navParams: NavParams) {
* console.log('Passed params', navParams.data);
* }
* }
* ```
*
* Sometimes you may want to call a method instead of navigating to a new
* page. You can use the `(ionSelect)` event to call a method on your class when
* the tab is selected. Below is an example of presenting a modal from one of
* the tabs.
*
* ```html
* <ion-tabs>
* <ion-tab (ionSelect)="chat()" tabTitle="Show Modal"></ion-tab>
* </ion-tabs>pop
* ```
*
* ```ts
* export class Tabs {
* constructor(public modalCtrl: ModalController) {
*
* }
*
* chat() {
* let modal = this.modalCtrl.create(ChatPage);
* modal.present();
* }
* }
* ```
*
*
* @demo /docs/demos/src/tabs/
* @see {@link /docs/components#tabs Tabs Component Docs}
* @see {@link ../../tabs/Tabs Tabs API Docs}
* @see {@link ../../nav/Nav Nav API Docs}
* @see {@link ../../nav/NavController NavController API Docs}
*/
@Component({
tag: 'ion-tab',
host: {
theme: 'tab'
}
})
export class Tab {
/**
* @input {Page} Set the root component for this tab.
*/
@Prop() root: string;
private nav: Promise<HTMLIonNavElement>;
private resolveNav: (el: HTMLIonNavElement) => void;
@Element() el: HTMLElement;
@State() init = false;
@State() active = false;
/**
* @input {object} Any nav-params to pass to the root componentof this tab.
* @input {string} Set the root page for this tab.
*/
@Prop() btnId: string;
/**
* @input {Page} Set the root page for this tab.
*/
@Prop() root: any;
/**
* @input {object} Any nav-params to pass to the root page of this tab.
*/
@Prop() rootParams: any;
/**
* @input {boolean} If true, the tab is selected
* @input {string} The URL path name to represent this tab within the URL.
*/
@State() isSelected: Boolean = false;
@Prop() urlPath: string;
/**
* @input {string} The title of the tab button.
*/
@Prop() tabTitle: string;
@Prop() title: string;
/**
* @input {string} The icon for the tab button.
*/
@Prop() tabIcon: string;
@Prop() icon: string;
/**
* @input {string} The badge for the tab button.
*/
@Prop() tabBadge: string;
@Prop() badge: string;
/**
* @input {string} The badge color for the tab button.
*/
@Prop() tabBadgeStyle: string;
@Prop() badgeStyle: string = 'default';
/**
* TODO why isn't this disabled like other components?
* @input {boolean} If true, enable the tab. If false,
* the user cannot interact with the tab.
* the user cannot interact with this element.
* Default: `true`.
*/
@Prop() enabled: boolean = true;
@Prop() enabled = true;
/**
* @input {boolean} If true, the tab button is visible within the
* tabbar. Default: `true`.
*/
@Prop() shown: boolean = true;
@Prop() show = true;
/**
* @input {boolean} If true, hide the tabs on child pages.
*/
@Prop() tabsHideOnSubPages: boolean = false;
@Prop() tabsHideOnSubPages = false;
/**
* @input {Tab} Emitted when the current tab is selected.
*/
@Prop() onSelected: Function;
/**
* @output {TabEvent} Emitted after the tab has loaded.
*/
@Event() ionTabDidLoad: EventEmitter;
hostData() {
return {
style: {
display: !this.isSelected && 'none' || ''
},
'role': 'tabpanel'
};
constructor() {
this.nav = new Promise((resolve) => this.resolveNav = resolve);
}
@Prop({ mutable: true }) selected = false;
@PropDidChange('selected')
selectedChanged(selected: boolean) {
if (selected) {
this.ionSelect.emit(this.el);
}
}
/**
* @output {Tab} Emitted when the current tab is selected.
*/
@Event() ionSelect: EventEmitter;
@Event() ionTabDidLoad: EventEmitter;
@Event() ionTabDidUnload: EventEmitter;
protected ionViewDidLoad() {
setTimeout(() => {
this.ionTabDidLoad.emit({ tab: this });
}, 0);
this.ionTabDidLoad.emit(this.el);
}
protected ionViewDidUnload() {
this.ionTabDidLoad.emit({ tab: this });
this.ionTabDidUnload.emit(this.el);
}
protected componentDidUpdate() {
if (this.init && this.resolveNav) {
const nav = this.el.querySelector('ion-nav') as any as StencilElement;
nav.componentOnReady(this.resolveNav);
this.resolveNav = null;
}
}
@Method()
_setActive(active: boolean): Promise<any> {
if (this.active === active) {
return Promise.resolve();
}
this.active = active;
this.selected = active;
const needLifecycle = this.init;
if (active) {
this.init = true;
if (!needLifecycle) {
return this.nav.then(nav => nav.setRoot(this.root, this.rootParams));
}
}
if (needLifecycle) {
if (active) {
// lifecycle didEnter
} else {
// lifecycle didLeave
}
}
return this.nav;
}
@Method()
resize() {
this.nav.then(nav => nav.resize());
}
@Method()
goToRoot(opts: any = {}) {
return this.nav.then(nav => nav.setRoot(this.root, this.rootParams, opts));
}
@Method()
getActive(): Promise<ViewController> {
return this.nav.then(nav => nav.getActive());
}
@Method()
getNav(): Promise<HTMLIonNavElement> {
return this.nav;
}
protected hostData() {
const visible = this.active && this.selected;
return {
'aria-hidden': !visible ? 'true' : null,
'aria-labelledby': this.btnId,
'role': 'tabpanel',
class: {
'show-tab': this.active
}
};
}
protected render() {
const RootComponent = this.root;
return [
<RootComponent />,
<div class='nav-decor'></div>
];
}
}
export interface TabEvent extends Event {
detail: {
tab: Tab
if (this.init) {
return <ion-nav></ion-nav>;
}
return null;
}
}

View File

@ -23,7 +23,7 @@ $tabs-ios-tab-padding-start: $tabs-ios-tab-padding-end !default;
$tabs-ios-tab-max-width: 240px !default;
/// @prop - Minimum height of the tab button
$tabs-ios-tab-min-height: 49px !default;
$tabs-ios-tab-height: 50px !default;
/// @prop - Text color of the inactive tab button
$tabs-ios-tab-text-color: $tabs-ios-tab-color-inactive !default;
@ -44,120 +44,124 @@ $tabs-ios-tab-font-size: 10px !default;
$tabs-ios-tab-icon-size: 30px !default;
.tabs-ios .tabbar {
.tabbar-ios {
justify-content: center;
height: $tabs-ios-tab-height;
border-top: $tabs-ios-border;
background: $tabs-ios-background;
contain: strict;
}
.tabs-ios[tabsPlacement=top] .tabbar {
.tabbar-ios.placement-top {
border-top: 0;
border-bottom: $tabs-ios-border;
}
.tabs-ios .tab-button {
.tabbar-ios > ion-tab-highlight {
background: $tabs-ios-tab-color-active;
}
.tabbar-ios > ion-tab-button {
@include padding($tabs-ios-tab-padding-top, $tabs-ios-tab-padding-end, $tabs-ios-tab-padding-bottom, $tabs-ios-tab-padding-start);
max-width: $tabs-ios-tab-max-width;
min-height: $tabs-ios-tab-min-height;
min-height: 100%;
font-size: $tabs-ios-tab-font-size;
color: $tabs-ios-tab-text-color;
fill: $tabs-ios-tab-icon-color;
}
.tabs-ios .tab-button:hover:not(.disable-hover),
.tabs-ios .tab-button[aria-selected=true] {
.tabbar-ios ion-tab-button:hover:not(.disable-hover),
.tabbar-ios .tab-selected {
color: $tabs-ios-tab-text-color-active;
fill: $tabs-ios-tab-icon-color-active;
}
.tabs-ios .tab-button[aria-selected=true] .tab-button-icon {
color: $tabs-ios-tab-icon-color-active;
}
.tabs-ios .tab-button-text {
.tabbar-ios .tab-button-text {
@include margin(0, null, 1px, null);
min-height: $tabs-ios-tab-font-size + 1;
}
.tabs-ios .has-title-only .tab-button-text {
.tabbar-ios .has-title-only .tab-button-text {
font-size: $tabs-ios-tab-font-size + 2;
}
.tabs-ios .tab-button-icon {
.tabbar-ios .tab-button-icon {
@include margin(4px, null, 1px, null);
min-width: $tabs-ios-tab-icon-size + 5;
height: $tabs-ios-tab-icon-size;
font-size: $tabs-ios-tab-icon-size;
color: $tabs-ios-tab-icon-color;
}
.tabs-ios .tab-button-icon::before {
.tabbar-ios .tab-button-icon::before {
vertical-align: top;
}
.tabs-ios[tabsLayout=icon-end] .tab-button .tab-button-text,
.tabs-ios[tabsLayout=icon-start] .tab-button .tab-button-text {
font-size: 1.4rem;
line-height: 1.1;
}
.tabbar-ios.layout-icon-end .tab-button-text,
.tabbar-ios.layout-icon-start .tab-button-text,
.tabbar-ios.layout-icon-hide .tab-button-text,
.tabbar-ios .has-title-only .tab-button-text {
.tabs-ios[tabsLayout=icon-end] .tab-button ion-icon,
.tabs-ios[tabsLayout=icon-start] .tab-button ion-icon {
min-width: 24px;
height: 26px;
font-size: 24px;
}
.tabs-ios[tabsLayout=icon-hide] .tab-button,
.tabs-ios .tab-button.has-title-only {
min-height: $tabs-ios-tab-min-height - 8;
}
.tabs-ios[tabsLayout=icon-hide] .tab-button .tab-button-text,
.tabs-ios .tab-button.has-title-only .tab-button-text {
@include margin(2px, 0);
font-size: 1.4rem;
line-height: 1.1;
}
.tabs-ios[tabsLayout=title-hide] .tab-button,
.tabs-ios .tab-button.icon-only {
min-height: $tabs-ios-tab-min-height - 8;
.tabbar-ios.layout-icon-end ion-icon,
.tabbar-ios.layout-icon-start ion-icon {
@include margin(2px, null, 1px, null);
min-width: 24px;
height: 26px;
font-size: 24px;
}
.tabbar-ios.layout-title-hide ion-icon {
@include margin(0);
}
// .tabs-ios[tabsLayout=icon-hide] ion-tab-button,
// .tabs-ios ion-tab-button.has-title-only {
// min-height: $tabs-ios-tab-min-height - 8;
// }
// .tabs-ios[tabsLayout=title-hide] ion-tab-button,
// .tabs-ios ion-tab-button.icon-only {
// min-height: $tabs-ios-tab-min-height - 8;
// }
// iOS Tabbar Color Mixin
// --------------------------------------------------
@mixin tabbar-ios($color-name, $color-base, $color-contrast) {
.tabs-ios-#{$color-name} .tabbar {
.tabbar-ios-#{$color-name} {
border-color: darken($color-base, 10%);
background-color: $color-base;
}
.tabs-ios-#{$color-name} .tab-button,
.tabs-ios-#{$color-name} .tab-button-icon,
.tabs-ios-#{$color-name} .tab-button:hover:not(.disable-hover),
.tabs-ios-#{$color-name} .tab-button:hover:not(.disable-hover) .tab-button-icon {
color: rgba($color-contrast, .7);
background-color: $color-base;
fill: rgba($color-contrast, .7);
}
.tabs-ios-#{$color-name} .tab-button[aria-selected=true],
.tabs-ios-#{$color-name} .tab-button[aria-selected=true] .tab-button-icon {
.tabbar-ios-#{$color-name} .tab-selected {
color: $color-contrast;
fill: $color-contrast;
}
}
// iOS Tabbar Color Generation
// --------------------------------------------------

View File

@ -6,19 +6,19 @@
// --------------------------------------------------
/// @prop - Padding top on the tab button
$tabs-md-tab-padding-top: 0 !default;
$tabs-md-tab-padding-top: .8rem !default;
/// @prop - Padding end on the tab button
$tabs-md-tab-padding-end: $tabs-md-tab-padding-top !default;
$tabs-md-tab-padding-end: 1.2rem !default;
/// @prop - Padding bottom on the tab button
$tabs-md-tab-padding-bottom: $tabs-md-tab-padding-top !default;
$tabs-md-tab-padding-bottom: 1rem !default;
/// @prop - Padding start on the tab button
$tabs-md-tab-padding-start: $tabs-md-tab-padding-end !default;
$tabs-md-tab-padding-start: 1.2rem, !default;
/// @prop - Minimum height of the tab button
$tabs-md-tab-min-height: 5.6rem !default;
$tabs-md-tab-height: 5.6rem !default;
/// @prop - Font size of the inactive tab button text
$tabs-md-tab-font-size: 1.2rem !default;
@ -41,18 +41,6 @@ $tabs-md-tab-icon-color: rgba($tabs-md-tab-color-inactive,
/// @prop - Icon color of the active tab button
$tabs-md-tab-icon-color-active: $tabs-md-tab-color-active !default;
/// @prop - Padding top of the active tab button
$tabs-md-tab-padding-active-top: 0 !default;
/// @prop - Padding end of the active tab button
$tabs-md-tab-padding-active-end: $tabs-md-tab-padding-active-top !default;
/// @prop - Padding bottom of the active tab button
$tabs-md-tab-padding-active-bottom: $tabs-md-tab-padding-active-top !default;
/// @prop - Padding start of the active tab button
$tabs-md-tab-padding-active-start: $tabs-md-tab-padding-active-end !default;
/// @prop - Font size of the active tab button text
$tabs-md-tab-font-size-active: 1.4rem !default;
@ -71,12 +59,6 @@ $tabs-md-tab-text-margin-start: $tabs-md-tab-text-margin-end !defa
/// @prop - Capitalization of the tab button text
$tabs-md-tab-text-capitalization: none !default;
/// @prop - Transform origin x for the tab button text
$tabs-md-tab-text-transform-origin-x: 50% !default;
/// @prop - Transform origin y for the tab button text
$tabs-md-tab-text-transform-origin-y: 80% !default;
/// @prop - Transform for the active tab button text
$tabs-md-tab-text-transform-active: scale3d($tabs-md-tab-font-size-active / $tabs-md-tab-font-size, $tabs-md-tab-font-size-active / $tabs-md-tab-font-size, 1) !default;
@ -105,13 +87,13 @@ $tabs-md-tab-icon-right-transform-z-active: 0 !default;
$tabs-md-tab-icon-bottom-transform-x-active: 0 !default;
/// @prop - Transform y for the active tab button icon when the layout is icon-bottom
$tabs-md-tab-icon-bottom-transform-y-active: 2px !default;
$tabs-md-tab-icon-bottom-transform-y-active: 0.2rem !default;
/// @prop - Transform z for the active tab button icon when the layout is icon-bottom
$tabs-md-tab-icon-bottom-transform-z-active: 0 !default;
/// @prop - Transform x for the active tab button icon when the layout is icon-left
$tabs-md-tab-icon-left-transform-x-active: -2px !default;
$tabs-md-tab-icon-left-transform-x-active: -0.2rem !default;
/// @prop - Transform y for the active tab button icon when the layout is icon-left
$tabs-md-tab-icon-left-transform-y-active: 0 !default;
@ -119,50 +101,61 @@ $tabs-md-tab-icon-left-transform-y-active: 0 !default;
/// @prop - Transform z for the active tab button icon when the layout is icon-left
$tabs-md-tab-icon-left-transform-z-active: 0 !default;
/// @prop - Transform origin x for the tab button text
$tabs-md-tab-icon-transform-origin-x: 50% !default;
/// @prop - Transform origin y for the tab button text
$tabs-md-tab-icon-transform-origin-y: 150% !default;
/// @prop - Text transition for the tab button icon
$tabs-md-tab-icon-transition: transform .3s ease-in-out !default;
/// @prop - Size of the tab button icon
$tabs-md-tab-icon-size: 2.4rem !default;
.tabbar-md {
height: $tabs-md-tab-height;
.tabs-md .tabbar {
border-top: 1px solid $tabs-md-border-color;
background: $tabs-md-background;
contain: strict;
}
.tabbar-md > ion-tab-highlight {
background: $tabs-md-tab-color-active;
}
// Material Design Tab Button
// --------------------------------------------------
.tabs-md .tab-button {
.tabbar-md ion-tab-button {
@include padding($tabs-md-tab-padding-top, $tabs-md-tab-padding-end, $tabs-md-tab-padding-bottom, $tabs-md-tab-padding-start);
min-height: $tabs-md-tab-min-height;
// min-height: $tabs-md-tab-min-height;
display: flex;
max-width: 168px;
// min-width: 80px;
// Maximum: 168dp
// Minimum: 96dp
// max-width: 96px;
// min-width: 56px;
height: 100%;
font-weight: $tabs-md-tab-font-weight;
color: $tabs-md-tab-text-color;
fill: $tabs-md-tab-icon-color;
}
.tabs-md .tab-button[aria-selected=true] {
@include padding($tabs-md-tab-padding-active-top, $tabs-md-tab-padding-active-end, $tabs-md-tab-padding-active-bottom, $tabs-md-tab-padding-active-start);
.tabbar-md .tab-selected {
color: $tabs-md-tab-text-color-active;
fill: $tabs-md-tab-icon-color-active;
}
// Material Design Tab Button Text
// --------------------------------------------------
.tabs-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 transform-origin($tabs-md-tab-text-transform-origin-x, $tabs-md-tab-text-transform-origin-y);
@include transform-origin(center, bottom);
font-size: $tabs-md-tab-font-size;
@ -170,56 +163,53 @@ $tabs-md-tab-icon-size: 2.4rem !default;
transition: $tabs-md-tab-text-transition;
}
.tabs-md .tab-button[aria-selected=true] .tab-button-text {
.tabbar-md .tab-selected .tab-button-text {
transform: $tabs-md-tab-text-transform-active;
transition: $tabs-md-tab-text-transition;
}
.tabs-md[tabsLayout=icon-top] .has-icon .tab-button-text {
@include margin(4px, null, 0, null);
.tabbar-md.layout-icon-top .has-icon .tab-button-text {
@include margin(null, null, -.2rem, null);
}
.tabs-md[tabsLayout=icon-bottom] .tab-button .tab-button-text {
@include margin(0, null, null, null);
.tabbar-md.layout-icon-bottom .tab-button-text {
@include transform-origin(center, top);
@include margin(-.2rem, null, null, null);
}
// Material Design Tab Button Icon
// --------------------------------------------------
.tabs-md .tab-button-icon {
@include margin(1px, null, null, null);
.tabbar-md .tab-button-icon {
@include transform-origin(center, center);
@include transform-origin($tabs-md-tab-icon-transform-origin-x, $tabs-md-tab-icon-transform-origin-y);
min-width: $tabs-md-tab-icon-size;
width: $tabs-md-tab-icon-size;
height: $tabs-md-tab-icon-size;
font-size: $tabs-md-tab-icon-size;
color: $tabs-md-tab-icon-color;
transition: $tabs-md-tab-icon-transition;
}
// Tab layout: icon-top, icon-only, title-only
.tabs-md .tab-button[aria-selected=true] .tab-button-icon {
.tabbar-md .tab-selected .tab-button-icon {
@include transform(translate3d($tabs-md-tab-icon-transform-x-active, $tabs-md-tab-icon-transform-y-active, $tabs-md-tab-icon-transform-z-active));
color: $tabs-md-tab-icon-color-active;
}
// Tab layout: icon-end
.tabs-md[tabsLayout=icon-end] .tab-button[aria-selected=true] .tab-button-icon {
.tabbar-md.layout-icon-end .tab-selected .tab-button-icon {
@include transform(translate3d($tabs-md-tab-icon-right-transform-x-active, $tabs-md-tab-icon-right-transform-y-active, $tabs-md-tab-icon-right-transform-z-active));
}
// Tab layout: icon-bottom
.tabs-md[tabsLayout=icon-bottom] .tab-button[aria-selected=true] .tab-button-icon {
.tabbar-md.layout-icon-bottom .tab-selected .tab-button-icon {
@include transform(translate3d($tabs-md-tab-icon-bottom-transform-x-active, $tabs-md-tab-icon-bottom-transform-y-active, $tabs-md-tab-icon-bottom-transform-z-active));
}
// Tab layout: icon-start
.tabs-md[tabsLayout=icon-start] .tab-button[aria-selected=true] .tab-button-icon {
.tabbar-md.layout-icon-start .tab-selected .tab-button-icon {
@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));
}
@ -227,62 +217,33 @@ $tabs-md-tab-icon-size: 2.4rem !default;
// Material Design Tab with Icon or Title only
// --------------------------------------------------
.tabs-md[tabsLayout=icon-hide] .tab-button,
.tabs-md[tabsLayout=title-hide] .tab-button,
.tabs-md .tab-button.icon-only,
.tabs-md .tab-button.has-title-only {
@include padding(0, 10px);
.tabbar-md.layout-icon-hide ion-tab-button,
.tabbar-md.layout-title-hide ion-tab-button,
.tabbar-md ion-tab-button.icon-only,
.tabbar-md ion-tab-button.has-title-only {
justify-content: center;
}
// Material Design Tab Highlight
// --------------------------------------------------
.tabs-md[tabsHighlight=true] .tab-highlight {
@include position(null, null, 0, 0);
@include transform-origin(0, 0);
position: absolute;
display: block;
width: 1px;
height: 2px;
background: $tabs-md-tab-color-active;
transform: translateZ(0);
}
.tabs-md[tabsHighlight=true] .tab-highlight.animate {
transition-duration: 300ms;
}
.tabs-md[tabsHighlight=true][tabsPlacement=bottom] > .tabbar > .tab-highlight {
top: 0;
}
// Material Design Tabs Color Mixin
// --------------------------------------------------
@mixin tabbar-md($color-name, $color-base, $color-contrast) {
.tabs-md-#{$color-name} .tabbar {
background-color: $color-base;
}
.tabs-md-#{$color-name} .tab-button,
.tabs-md-#{$color-name} .tab-button-icon {
.tabbar-md-#{$color-name} {
color: rgba($color-contrast, $tabs-md-tab-opacity);
background-color: $color-base;
fill: rgba($color-contrast, $tabs-md-tab-opacity);
}
.tabs-md-#{$color-name} .tab-button:hover:not(.disable-hover),
.tabs-md-#{$color-name} .tab-button[aria-selected=true],
.tabs-md-#{$color-name} .tab-button[aria-selected=true] .tab-button-icon {
.tabbar-md-#{$color-name} ion-tab-button:hover:not(.disable-hover),
.tabbar-md-#{$color-name} ion-tab-button.tab-selected {
color: $color-contrast;
fill: $color-contrast;
}
.tabs-md-#{$color-name}[tabsHighlight=true] .tab-highlight {
.tabbar-md-#{$color-name} ion-tab-highlight {
background: $color-contrast;
}

View File

@ -1,36 +1,30 @@
@import "../../themes/ionic.globals";
// Tabs
// Tabbar
// --------------------------------------------------
.tabbar {
@include position(null, null, 0, 0);
position: absolute;
ion-tabbar {
position: relative;
z-index: $z-index-toolbar;
display: flex;
width: 100%;
justify-content: center;
// default to hidden until ready
//opacity: 0;
order: 1;
width: 100%;
}
.tabbar-hidden .tabbar {
ion-tabbar.tabbar-hidden {
display: none;
}
.tabbar.show-tabbar {
opacity: 1;
ion-tabbar.placement-top {
order: -1;
}
[tabsPlacement=top] > .tabbar {
top: 0;
bottom: auto;
}
.tab-button {
ion-tab-button {
@include margin(0);
@include text-align(center);
@include border-radius(0);
@ -43,8 +37,7 @@
flex: 1;
flex-direction: column;
align-items: center;
align-self: center;
justify-content: center;
justify-content: space-between;
border: 0;
@ -59,19 +52,13 @@
pointer-events: none;
}
.tab-disabled ion-badge,
.tab-disabled ion-icon,
.tab-disabled span {
.tab-disabled > * {
opacity: .4;
}
.tab-button-text {
@include margin(3px, null, 2px, null);
}
.tab-button-text,
.tab-button-icon {
//display: none;
display: none;
overflow: hidden;
align-self: center;
@ -92,31 +79,28 @@
white-space: normal;
}
[tabsLayout=icon-bottom] .tab-button .tab-button-icon {
order: 10;
}
[tabsLayout=icon-start] .tab-button,
[tabsLayout=icon-end] .tab-button {
.layout-icon-start ion-tab-button {
flex-direction: row;
}
[tabsLayout=icon-start] .tab-button .tab-button-icon {
@include padding-horizontal(null, 8px);
@include text-align(end);
.layout-icon-end ion-tab-button {
flex-direction: row-reverse;
}
[tabsLayout=icon-end] .tab-button .tab-button-icon {
@include padding-horizontal(8px, null);
@include text-align(start);
.layout-icon-bottom ion-tab-button {
flex-direction: column-reverse;
}
order: 10;
.layout-icon-start ion-tab-button,
.layout-icon-end ion-tab-button,
.layout-icon-hide ion-tab-button,
.layout-title-hide ion-tab-button {
justify-content: center;
}
.tab-hidden,
.tab-highlight,
[tabsLayout=icon-hide] .tab-button-icon,
[tabsLayout=title-hide] .tab-button-text {
.layout-icon-hide .tab-button-icon,
.layout-title-hide .tab-button-text {
display: none;
}
@ -140,8 +124,43 @@
@include position(null, calc(50% - 30px), null, null);
}
[tabsLayout=icon-bottom] .tab-badge,
[tabsLayout=icon-start] .tab-badge,
[tabsLayout=icon-end] .tab-badge {
.layout-icon-bottom .tab-badge,
.layout-icon-start .tab-badge,
.layout-icon-end .tab-badge {
@include position(null, calc(50% - 50px), null, null);
}
// Tab Highlight
// --------------------------------------------------
ion-tab-highlight {
@include position(null, null, 0, 0);
@include transform-origin(0, 0);
position: absolute;
display: block;
width: 1px;
height: 2px;
transform: translateZ(0);
&.animated {
transition-duration: 300ms;
transition-property: transform;
transition-timing-function: cubic-bezier(.4, 0, .2, 1);
will-change: transform;
}
}
.placement-top > ion-tab-highlight {
bottom: 0;
}
.placement-bottom > ion-tab-highlight {
top: 0;
}

View File

@ -1,97 +1,386 @@
import { Component, Listen, Prop, PropDidChange, State } from '@stencil/core';
import { Component, Element, Event, EventEmitter, Listen, Method, Prop, State } from '@stencil/core';
import { Config, HTMLIonTabElement } from '../../index';
import { TabEvent } from './tab';
export interface NavOptions { }
// import { isPresent } from '../../utils/helpers';
/**
* @name Tabs
* @description
* Tabs make it easy to navigate between different pages or functional
* aspects of an app. The Tabs component, written as `<ion-tabs>`, is
* a container of individual [Tab](../Tab/) components. Each individual `ion-tab`
* is a declarative component for a [NavController](../../../navigation/NavController/)
*
* For more information on using nav controllers like Tab or [Nav](../../nav/Nav/),
* take a look at the [NavController API Docs](../../../navigation/NavController/).
*
* ### Placement
*
* The position of the tabs relative to the content varies based on
* the mode. The tabs are placed at the bottom of the screen
* for iOS and Android, and at the top for Windows by default. The position can
* be configured using the `tabsPlacement` attribute on the `<ion-tabs>` component,
* or in an app's [config](../../config/Config/).
* See the [Input Properties](#input-properties) below for the available
* values of `tabsPlacement`.
*
* ### Layout
*
* The layout for all of the tabs can be defined using the `tabsLayout`
* property. If the individual tab has a title and icon, the icons will
* show on top of the title by default. All tabs can be changed by setting
* the value of `tabsLayout` on the `<ion-tabs>` element, or in your
* app's [config](../../config/Config/). For example, this is useful if
* you want to show tabs with a title only on Android, but show icons
* and a title for iOS. See the [Input Properties](#input-properties)
* below for the available values of `tabsLayout`.
*
* ### Selecting a Tab
*
* There are different ways you can select a specific tab from the tabs
* component. You can use the `selectedIndex` property to set the index
* on the `<ion-tabs>` element, or you can call `select()` from the `Tabs`
* instance after creation. See [usage](#usage) below for more information.
*
* @usage
*
* You can add a basic tabs template to a `@Component` using the following
* template:
*
* ```html
* <ion-tabs>
* <ion-tab [root]="tab1Root"></ion-tab>
* <ion-tab [root]="tab2Root"></ion-tab>
* <ion-tab [root]="tab3Root"></ion-tab>
* </ion-tabs>
* ```
*
* Where `tab1Root`, `tab2Root`, and `tab3Root` are each a page:
*
*```ts
* @Component({
* templateUrl: 'build/pages/tabs/tabs.html'
* })
* export class TabsPage {
* // this tells the tabs component which Pages
* // should be each tab's root Page
* tab1Root = Page1;
* tab2Root = Page2;
* tab3Root = Page3;
*
* constructor() {
*
* }
* }
*```
*
* By default, the first tab will be selected upon navigation to the
* Tabs page. We can change the selected tab by using `selectedIndex`
* on the `<ion-tabs>` element:
*
* ```html
* <ion-tabs selectedIndex="2">
* <ion-tab [root]="tab1Root"></ion-tab>
* <ion-tab [root]="tab2Root"></ion-tab>
* <ion-tab [root]="tab3Root"></ion-tab>
* </ion-tabs>
* ```
*
* Since the index starts at `0`, this will select the 3rd tab which has
* root set to `tab3Root`. If you wanted to change it dynamically from
* your class, you could use [property binding](https://angular.io/docs/ts/latest/guide/template-syntax.html#!#property-binding).
*
* Alternatively, you can grab the `Tabs` instance and call the `select()`
* method. This requires the `<ion-tabs>` element to have an `id`. For
* example, set the value of `id` to `myTabs`:
*
* ```html
* <ion-tabs #myTabs>
* <ion-tab [root]="tab1Root"></ion-tab>
* <ion-tab [root]="tab2Root"></ion-tab>
* <ion-tab [root]="tab3Root"></ion-tab>
* </ion-tabs>
* ```
*
* Then in your class you can grab the `Tabs` instance and call `select()`,
* passing the index of the tab as the argument. Here we're grabbing the tabs
* by using ViewChild.
*
*```ts
* export class TabsPage {
*
* @ViewChild('myTabs') tabRef: Tabs;
*
* ionViewDidEnter() {
* this.tabRef.select(2);
* }
*
* }
*```
*
* You can also switch tabs from a child component by calling `select()` on the
* parent view using the `NavController` instance. For example, assuming you have
* a `TabsPage` component, you could call the following from any of the child
* components to switch to `TabsRoot3`:
*
*```ts
* switchTabs() {
* this.navCtrl.parent.select(2);
* }
*```
* @demo /docs/demos/src/tabs/
*
* @see {@link /docs/components#tabs Tabs Component Docs}
* @see {@link ../Tab Tab API Docs}
* @see {@link ../../config/Config Config API Docs}
*
*/
@Component({
tag: 'ion-tabs',
styleUrls: {
ios: 'tabs.ios.scss',
md: 'tabs.md.scss',
wp: 'tabs.wp.scss'
},
host: {
theme: 'tabs'
}
})
export class Tabs {
// Current list of tabs
@State() tabs: any;
private ids: number = -1;
private tabsId: number = (++tabIds);
private selectHistory: string[] = [];
@Element() el: HTMLElement;
@State() tabs: HTMLIonTabElement[] = [];
@State() selectedTab: HTMLIonTabElement;
@Prop({ context: 'config' }) config: Config;
/**
* @state {number} The selected tab
* @input {string} A unique name for the tabs
*/
@State() selectedTab: any;
@Prop() name: string;
/**
* @state {number} The selected tab index
* @input {boolean} If true, the tabbar
*/
@State() selectedIndex: number = 0;
@Prop() tabbarHidden = false;
/**
* @input {string} Set the tabbar layout: `icon-top`, `icon-start`, `icon-end`, `icon-bottom`, `icon-hide`, `title-hide`.
*/
@Prop() tabsLayout: string = 'icon-top';
@Prop({ mutable: true }) tabbarLayout: string;
/**
* @input {string} Set position of the tabbar: `top`, `bottom`.
*/
@Prop() tabsPlacement: string = 'bottom';
@Prop({ mutable: true }) tabbarPlacement: string;
/**
* @input {boolean} If true, show the tab highlight bar under the selected tab.
*/
@Prop() tabsHighlight: boolean = false;
@Prop({ mutable: true }) tabbarHighlight: boolean;
/**
* @output {Event} Emitted when the tab changes.
* @output {any} Emitted when the tab changes.
*/
@Prop() ionChange: Function;
@Event() ionChange: EventEmitter;
/**
* If selectedIndex was changed, grab the reference to the tab it points to.
*/
@PropDidChange('selectedIndex')
protected selectedIndexChanged() {
this.selectedTab = this.tabs[this.selectedIndex];
protected ionViewDidLoad() {
this.loadConfig('tabsPlacement', 'bottom');
this.loadConfig('tabsLayout', 'icon-top');
this.loadConfig('tabsHighlight', true);
// TODO: handle navigation parent
// if (this.parent) {
// // this Tabs has a parent Nav
// this.parent.registerChildNav(this);
// } else if (viewCtrl && viewCtrl.getNav()) {
// // this Nav was opened from a modal
// this.parent = <any>viewCtrl.getNav();
// this.parent.registerChildNav(this);
// } else if (this._app) {
// // this is the root navcontroller for the entire app
// this._app.registerRootNav(this);
// }
// // Tabs may also be an actual ViewController which was navigated to
// // if Tabs is static and not navigated to within a NavController
// // then skip this and don't treat it as it's own ViewController
// if (viewCtrl) {
// viewCtrl._setContent(this);
// viewCtrl._setContentRef(elementRef);
// }
// }
this.initTabs();
}
protected ionViewDidUnload() {
this.tabs = this.selectedTab = null;
}
@Listen('ionTabbarClick')
@Listen('ionSelect')
tabChange(ev: CustomEvent) {
const selectedTab = ev.detail as HTMLIonTabElement;
this.select(selectedTab);
}
@Listen('ionTabDidLoad')
tabDidLoad(ev: TabEvent) {
const tab = ev.detail.tab;
// First tab? Select it
if (this.tabs.length === 0) {
this.handleOnTabSelected(tab, 0);
}
this.tabs = [ ...this.tabs, tab ];
protected addTab(ev: CustomEvent) {
const tab = ev.detail as HTMLIonTabElement;
const id = `t-${this.tabsId}-${++this.ids}`;
tab.btnId = 'tab-' + id;
tab.id = 'tabpanel-' + id;
this.tabs = [...this.tabs, tab];
ev.stopPropagation();
}
@Listen('ionTabDidUnload')
tabDidUnload(ev: any) {
this.tabs = this.tabs.filter((t: any) => t !== ev.detail.tab);
protected removeTab(ev: CustomEvent) {
const tab = ev.detail;
this.tabs.slice(this.tabs.indexOf(tab));
ev.stopPropagation();
}
handleOnTabSelected(tab: any, index: number) {
// Select just this tab
this.tabs.forEach((t: any) => t.isSelected = false);
tab.isSelected = true;
/**
* @param {number|Tab} tabOrIndex Index, or the Tab instance, of the tab to select.
*/
@Method()
select(tabOrIndex: number | HTMLIonTabElement) {
const selectedTab = (typeof tabOrIndex === 'number' ? this.getByIndex(tabOrIndex) : tabOrIndex);
if (!selectedTab) {
return;
}
// Store the selected tab and index
this.selectedTab = tab;
this.selectedIndex = index;
// Reset rest of tabs
for (let tab of this.tabs) {
if (selectedTab !== tab) {
tab.selected = false;
}
}
selectedTab.selected = true;
// Fire a change event
this.ionChange && this.ionChange(tab);
if (this.selectedTab === selectedTab) {
selectedTab.goToRoot();
} else {
const promise = selectedTab._setActive(true);
const leavingTab = this.selectedTab;
if (leavingTab) {
promise.then(() => {
Context.dom.raf(() => {
leavingTab._setActive(false);
});
});
}
this.selectedTab = selectedTab;
this.selectHistory.push(selectedTab.id);
this.ionChange.emit(selectedTab);
}
}
/**
* @param {number} index Index of the tab you want to get
* @returns {HTMLIonTabElement} Returns the tab who's index matches the one passed
*/
@Method()
getByIndex(index: number): HTMLIonTabElement {
return this.tabs[index];
}
/**
* @return {HTMLIonTabElement} Returns the currently selected tab
*/
@Method()
getSelected(): HTMLIonTabElement {
return this.tabs.find((tab) => tab.selected);
}
@Method()
getIndex(tab: HTMLIonTabElement): number {
return this.tabs.indexOf(tab);
}
@Method()
getTabs(): HTMLIonTabElement[] {
return this.tabs;
}
private initTabs() {
// find pre-selected tabs
let selectedTab = this.tabs.find(t => t.selected);
// reset all tabs none is selected
for (let tab of this.tabs) {
tab.selected = false;
}
// find a tab candidate in case, the selected in null
if (!selectedTab) {
selectedTab = this.tabs.find(t => t.show && t.enabled);
}
selectedTab._setActive(true);
this.selectedTab = selectedTab;
this.selectHistory.push(selectedTab.id);
}
private loadConfig(attrKey: string, fallback: any) {
const val = (this as any)[attrKey];
if (typeof val === 'undefined') {
(this as any)[attrKey] = this.config.get(attrKey, fallback);
}
}
/**
* Get the previously selected Tab which is currently not disabled or hidden.
* @param {boolean} trimHistory If the selection history should be trimmed up to the previous tab selection or not.
* @returns {HTMLIonTabElement}
*/
@Method()
previousTab(trimHistory: boolean = true): HTMLIonTabElement {
// walk backwards through the tab selection history
// and find the first previous tab that is enabled and shown
for (var i = this.selectHistory.length - 2; i >= 0; i--) {
var id = this.selectHistory[i];
var tab = this.tabs.find(t => t.id === id);
if (tab && tab.enabled && tab.show) {
if (trimHistory) {
this.selectHistory.splice(i + 1);
}
return tab;
}
}
return null;
}
@Method()
resize() {
const tab = this.getSelected();
tab && tab.resize();
}
protected render() {
return [
<ion-tab-bar
tabs={this.tabs}
onTabSelected={this.handleOnTabSelected.bind(this)}
selectedIndex={this.selectedIndex} />,
<slot></slot>
];
const dom = [
<div class='tabs-inner'>
<slot></slot>
</div>];
if (!this.tabbarHidden) {
dom.push(
<ion-tabbar
tabs={this.tabs}
selectedTab={this.selectedTab}
highlight={this.tabbarHighlight}
placement={this.tabbarPlacement}
layout={this.tabbarLayout}>
</ion-tabbar>
);
}
return dom;
}
}
let tabIds = -1;

View File

@ -54,11 +54,11 @@ $tabs-wp-tab-background-activated: rgba(0, 0, 0, .1) !default;
$tabs-wp-tab-icon-size: 2.4rem !default;
.tabs-wp .tabbar {
.tabbar-wp {
background: $tabs-wp-background;
}
.tabs-wp .tab-button {
.tabbar-wp ion-tab-button {
@include border-radius(0);
@include padding($tabs-wp-tab-padding-top, $tabs-wp-tab-padding-end, $tabs-wp-tab-padding-bottom, $tabs-wp-tab-padding-start);
@ -72,21 +72,25 @@ $tabs-wp-tab-icon-size: 2.4rem !default;
box-shadow: none;
}
.tabs-wp .tab-button[aria-selected=true] {
.tabbar-wp ion-tab-highlight {
background: $tabs-wp-tab-color-active;
}
.tabbar-wp ion-tab-button.tab-selected {
border-bottom-color: $tabs-wp-tab-border-color-active;
color: $tabs-wp-tab-color-active;
}
.tabs-wp .tab-button.activated {
.tabbar-wp ion-tab-button.activated {
background: $tabs-wp-tab-background-activated;
}
.tabs-wp[tabsPlacement=bottom] .tab-button {
.tabbar-wp.placement-bottom ion-tab-button {
border-top: $tabs-wp-tab-border;
border-bottom-width: 0;
}
.tabs-wp[tabsPlacement=bottom] .tab-button[aria-selected=true] {
.tabbar-wp.placement-bottom ion-tab-button.tab-selected {
border-top-color: $tabs-wp-tab-border-color-active;
}
@ -94,7 +98,7 @@ $tabs-wp-tab-icon-size: 2.4rem !default;
// Windows Tab Button Text
// --------------------------------------------------
.tabs-wp .tab-button-text {
.tabbar-wp .tab-button-text {
@include margin(5px, null);
}
@ -102,7 +106,7 @@ $tabs-wp-tab-icon-size: 2.4rem !default;
// Windows Tab Button Icon
// --------------------------------------------------
.tabs-wp .tab-button-icon {
.tabbar-wp .tab-button-icon {
min-width: $tabs-wp-tab-icon-size;
font-size: $tabs-wp-tab-icon-size;
@ -110,28 +114,28 @@ $tabs-wp-tab-icon-size: 2.4rem !default;
color: $tabs-wp-tab-icon-color;
}
.tabs-wp .tab-button[aria-selected=true] .tab-button-icon {
.tabbar-wp .tab-selected .tab-button-icon {
color: $tabs-wp-tab-icon-color-active;
}
.tabs-wp[tabsLayout=icon-bottom] .tab-button {
.tabbar-wp.layout-icon-bottom ion-tab-button {
@include padding(8px, null, 8px, null);
}
.tabs-wp[tabsLayout=icon-end] .tab-button,
.tabs-wp[tabsLayout=icon-start] .tab-button {
.tabbar-wp.layout-icon-end ion-tab-button,
.tabbar-wp.layout-icon-start ion-tab-button {
@include padding(null, null, 10px, null);
}
.tabs-wp[tabsLayout=icon-end] .tab-button ion-icon,
.tabs-wp[tabsLayout=icon-start] .tab-button ion-icon {
.tabbar-wp.layout-icon-end ion-icon,
.tabbar-wp.layout-icon-start ion-icon {
min-width: 24px;
}
.tabs-wp[tabsLayout=icon-hide] .tab-button,
.tabs-wp[tabsLayout=title-hide] .tab-button,
.tabs-wp .tab-button.icon-only,
.tabs-wp .tab-button.has-title-only {
.tabbar-wp.layout-icon-hide ion-tab-button,
.tabbar-wp.layout-title-hide ion-tab-button,
.tabbar-wp ion-tab-button.icon-only,
.tabbar-wp ion-tab-button.has-title-only {
@include padding(6px, 10px);
}
@ -141,19 +145,15 @@ $tabs-wp-tab-icon-size: 2.4rem !default;
@mixin tabbar-wp($color-name, $color-base, $color-contrast) {
.tabs-wp-#{$color-name} .tabbar {
background-color: $color-base;
}
.tabs-wp-#{$color-name} .tab-button,
.tabs-wp-#{$color-name} .tab-button-icon {
.tabbar-wp-#{$color-name} {
color: rgba($color-contrast, $tabs-wp-tab-opacity);
background-color: $color-base;
fill: rgba($color-contrast, $tabs-wp-tab-opacity);
}
.tabs-wp-#{$color-name} .tab-button:hover:not(.disable-hover),
.tabs-wp-#{$color-name} .tab-button:hover:not(.disable-hover) .tab-button-icon,
.tabs-wp-#{$color-name} .tab-button[aria-selected=true],
.tabs-wp-#{$color-name} .tab-button[aria-selected=true] .tab-button-icon {
.tabbar-wp-#{$color-name} ion-tab-button:hover:not(.disable-hover),
.tabbar-wp-#{$color-name} ion-tab-button.tab-selected {
border-color: $color-contrast;
color: $color-contrast;
}

View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Ionic Spinners</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<script src="/dist/ionic.js"></script>
</head>
<body>
<ion-app>
<ion-tabs>
<ion-tab root="page-tab" title="Plain List" icon="star"></ion-tab>
<ion-tab root="page-two" title="Schedule" icon="globe"></ion-tab>
<ion-tab root="page-three" title="Stopwatch" icon="logo-facebook"></ion-tab>
<ion-tab root="page-one" title="Messages" icon="chatboxes"></ion-tab>
</ion-tabs>
<ion-nav-controller></ion-nav-controller>
</ion-app>
<style>
f {
display: block;
margin: 15px auto;
max-width: 150px;
height: 150px;
background: blue;
}
f:last-of-type {
background: yellow;
}
</style>
</body>
</html>

View File

@ -89,10 +89,9 @@ export class Toggle implements BooleanInputComponent {
'onStart': this.onDragStart.bind(this),
'onMove': this.onDragMove.bind(this),
'onEnd': this.onDragEnd.bind(this),
'onPress': this.toggle.bind(this),
'gestureName': 'toggle',
'gesturePriority': 30,
'type': 'pan,press',
'type': 'pan',
'direction': 'x',
'threshold': 0,
'attachTo': 'parent'

View File

@ -22,6 +22,7 @@ export const PLATFORM_CONFIGS: PlatformConfig[] = [
name: IOS,
settings: {
mode: IOS,
tabsHighlight: false,
},
isMatch: (url, userAgent) => isPlatformMatch(url, userAgent, IOS, [IPHONE, IPAD, 'ipod'], WINDOWS_PHONE)
},

View File

@ -23,6 +23,9 @@ import { Segment } from './components/segment/segment';
import { SegmentButton, SegmentButtonEvent } from './components/segment-button/segment-button';
import { SplitPane, SplitPaneAlert } from './components/split-pane/split-pane';
import { Tab } from './components/tabs/tab';
import { Tabs } from './components/tabs/tabs';
import { Toast, ToastEvent, ToastOptions } from './components/toast/toast';
import { ToastController } from './components/toast-controller/toast-controller';

View File

@ -33,6 +33,7 @@ $toolbar-md-inactive-color: $toolbar-inactive-color !default;
// --------------------------------------------------
$tabs-md-background: $tabs-background !default;
$tabs-md-border-color: rgba(255, 255, 255, .1) !default;
$tabs-md-tab-color-inactive: $tabs-tab-color-inactive !default;
$tabs-md-tab-color-active: $tabs-tab-color-active !default;

View File

@ -33,6 +33,7 @@ $toolbar-md-inactive-color: $toolbar-inactive-color !default;
// --------------------------------------------------
$tabs-md-background: $tabs-background !default;
$tabs-md-border-color: rgba(0, 0, 0, .07) !default;
$tabs-md-tab-color-inactive: #3c3c3c !default;
$tabs-md-tab-color-active: $tabs-tab-color-active !default;

View File

@ -34,7 +34,7 @@ exports.config = {
{ components: ['ion-spinner'] },
{ components: ['ion-split-pane'] },
{ components: ['ion-range', 'ion-range-knob']},
{ components: ['ion-tabs', 'ion-tab', 'ion-tab-bar', 'ion-tab-button', 'ion-tab-highlight'] },
{ components: ['ion-tabs', 'ion-tab', 'ion-tabbar', 'ion-tab-button', 'ion-tab-highlight'] },
{ components: ['ion-toggle'] },
{ components: ['ion-nav', 'ion-nav-controller', 'stencil-ion-nav-delegate','page-one', 'page-two', 'page-three'] },
{ components: ['ion-toast', 'ion-toast-controller'] },