feat(tabs): animate tab change, include optional dynamic height (#1788)

This commit is contained in:
Andrew Seguin
2016-11-11 14:27:01 -08:00
committed by Jeremy Elbourn
parent 716372bc85
commit f6944e4f90
15 changed files with 416 additions and 111 deletions
+16 -23
View File
@@ -11,17 +11,17 @@ describe('tabs', () => {
browser.get('/tabs');
tabGroup = element(by.css('md-tab-group'));
tabLabels = element.all(by.css('.md-tab-label'));
tabBodies = element.all(by.css('.md-tab-body'));
tabBodies = element.all(by.css('md-tab-body'));
});
it('should change tabs when the label is clicked', () => {
tabLabels.get(1).click();
expect(getActiveStates(tabLabels)).toEqual([false, true, false]);
expect(getActiveStates(tabBodies)).toEqual([false, true, false]);
expect(getLabelActiveStates(tabLabels)).toEqual([false, true, false]);
expect(getBodyActiveStates(tabBodies)).toEqual([false, true, false]);
tabLabels.get(0).click();
expect(getActiveStates(tabLabels)).toEqual([true, false, false]);
expect(getActiveStates(tabBodies)).toEqual([true, false, false]);
expect(getLabelActiveStates(tabLabels)).toEqual([true, false, false]);
expect(getBodyActiveStates(tabBodies)).toEqual([true, false, false]);
});
it('should change focus with keyboard interaction', () => {
@@ -49,18 +49,13 @@ describe('tabs', () => {
});
});
/**
* A helper function to perform the sendKey action
* @param key
*/
/** A helper function to perform the sendKey action. */
function pressKey(key: string) {
browser.actions().sendKeys(key).perform();
}
/**
* Returns an array of true/false that represents the focus states of the provided elements
* @param elements
* @returns {webdriver.promise.Promise<Promise<boolean>[]>|webdriver.promise.Promise<T[]>}
* Returns an array of true/false that represents the focus states of the provided elements.
*/
function getFocusStates(elements: ElementArrayFinder) {
return elements.map(element => {
@@ -72,21 +67,19 @@ function getFocusStates(elements: ElementArrayFinder) {
});
}
/**
* Returns an array of true/false that represents the active states for the provided elements
* @param elements
* @returns {webdriver.promise.Promise<Promise<boolean>[]>|webdriver.promise.Promise<T[]>}
*/
function getActiveStates(elements: ElementArrayFinder) {
return getClassStates(elements, 'md-tab-active');
/** Returns an array of true/false that represents the active states for the provided elements. */
function getLabelActiveStates(elements: ElementArrayFinder) {
return getClassStates(elements, 'md-tab-label-active');
}
/** Returns an array of true/false that represents the active states for the provided elements */
function getBodyActiveStates(elements: ElementArrayFinder) {
return getClassStates(elements, 'md-tab-body-active');
}
/**
* Returns an array of true/false values that represents whether the provided className is on
* each element
* @param elements
* @param className
* @returns {webdriver.promise.Promise<Promise<boolean>[]>|webdriver.promise.Promise<T[]>}
* each element.
*/
function getClassStates(elements: ElementArrayFinder, className: string) {
return elements.map(element => {
+73 -3
View File
@@ -14,14 +14,84 @@
</div>
<h1>Tab Group Demo</h1>
<h1>Tab Group Demo - Dynamic Height</h1>
<md-tab-group class="demo-tab-group">
<md-tab *ngFor="let tab of tabs; let i = index" [disabled]="i == 1">
<md-tab-group class="demo-tab-group" md-dynamic-height>
<md-tab *ngFor="let tab of tabs" [disabled]="tab.disabled">
<template md-tab-label>{{tab.label}}</template>
{{tab.content}}
<br>
<br>
<div *ngIf="tab.extraContent">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla venenatis ante augue.
Phasellus volutpat neque ac dui mattis vulputate. Etiam consequat aliquam cursus.
In sodales pretium ultrices. Maecenas lectus est, sollicitudin consectetur felis nec,
feugiat ultricies mi. Aliquam erat volutpat. Nam placerat, tortor in ultrices porttitor,
orci enim rutrum enim, vel tempor sapien arcu a tellus. Vivamus convallis sodales ante varius
gravida. Curabitur a purus vel augue ultrices ultricies id a nisl. Nullam malesuada consequat
diam, a facilisis tortor volutpat et. Sed urna dolor, aliquet vitae posuere vulputate, euismod
ac lorem. Sed felis risus, pulvinar at interdum quis, vehicula sed odio. Phasellus in enim
venenatis, iaculis tortor eu, bibendum ante. Donec ac tellus dictum neque volutpat blandit.
Praesent efficitur faucibus risus, ac auctor purus porttitor vitae. Phasellus ornare dui nec
orci posuere, nec luctus mauris semper.
<br>
<br>
Morbi viverra, ante vel aliquet tincidunt, leo dolor pharetra quam, at semper massa orci nec
magna. Donec posuere nec sapien sed laoreet. Etiam cursus nunc in condimentum facilisis.
Etiam in tempor tortor. Vivamus faucibus egestas enim, at convallis diam pulvinar vel.
Cras ac orci eget nisi maximus cursus. Nunc urna libero, viverra sit amet nisl at, hendrerit
tempor turpis. Maecenas facilisis convallis mi vel tempor. Nullam vitae nunc leo. Cras sed
nisl consectetur, rhoncus sapien sit amet, tempus sapien.
<br>
<br>
Integer turpis erat, porttitor vitae mi faucibus, laoreet interdum tellus. Curabitur posuere
molestie dictum. Morbi eget congue risus, quis rhoncus quam. Suspendisse vitae hendrerit erat,
at posuere mi. Cras eu fermentum nunc. Sed id ante eu orci commodo volutpat non ac est.
Praesent ligula diam, congue eu enim scelerisque, finibus commodo lectus.
</div>
<br>
<br>
<md-input placeholder="Tab Label" [(ngModel)]="tab.label"></md-input>
</md-tab>
</md-tab-group>
<h1>Tab Group Demo - Fixed Height</h1>
<md-tab-group class="demo-tab-group" style="height: 200px">
<md-tab *ngFor="let tab of tabs" [disabled]="tab.disabled">
<template md-tab-label>{{tab.label}}</template>
{{tab.content}}
<br>
<br>
<div *ngIf="tab.extraContent">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla venenatis ante augue.
Phasellus volutpat neque ac dui mattis vulputate. Etiam consequat aliquam cursus.
In sodales pretium ultrices. Maecenas lectus est, sollicitudin consectetur felis nec,
feugiat ultricies mi. Aliquam erat volutpat. Nam placerat, tortor in ultrices porttitor,
orci enim rutrum enim, vel tempor sapien arcu a tellus. Vivamus convallis sodales ante varius
gravida. Curabitur a purus vel augue ultrices ultricies id a nisl. Nullam malesuada consequat
diam, a facilisis tortor volutpat et. Sed urna dolor, aliquet vitae posuere vulputate, euismod
ac lorem. Sed felis risus, pulvinar at interdum quis, vehicula sed odio. Phasellus in enim
venenatis, iaculis tortor eu, bibendum ante. Donec ac tellus dictum neque volutpat blandit.
Praesent efficitur faucibus risus, ac auctor purus porttitor vitae. Phasellus ornare dui nec
orci posuere, nec luctus mauris semper.
<br>
<br>
Morbi viverra, ante vel aliquet tincidunt, leo dolor pharetra quam, at semper massa orci nec
magna. Donec posuere nec sapien sed laoreet. Etiam cursus nunc in condimentum facilisis.
Etiam in tempor tortor. Vivamus faucibus egestas enim, at convallis diam pulvinar vel.
Cras ac orci eget nisi maximus cursus. Nunc urna libero, viverra sit amet nisl at, hendrerit
tempor turpis. Maecenas facilisis convallis mi vel tempor. Nullam vitae nunc leo. Cras sed
nisl consectetur, rhoncus sapien sit amet, tempus sapien.
<br>
<br>
Integer turpis erat, porttitor vitae mi faucibus, laoreet interdum tellus. Curabitur posuere
molestie dictum. Morbi eget congue risus, quis rhoncus quam. Suspendisse vitae hendrerit erat,
at posuere mi. Cras eu fermentum nunc. Sed id ante eu orci commodo volutpat non ac est.
Praesent ligula diam, congue eu enim scelerisque, finibus commodo lectus.
</div>
<br>
<br>
<md-input placeholder="Tab Label" [(ngModel)]="tab.label"></md-input>
</md-tab>
+1 -1
View File
@@ -16,7 +16,7 @@
.md-tab-header {
background: #f9f9f9;
}
.md-tab-body {
.md-tab-body-content {
padding: 12px;
}
}
+15 -3
View File
@@ -18,9 +18,21 @@ export class TabsDemo {
activeLinkIndex = 0;
tabs = [
{label: 'Tab One', content: 'This is the body of the first tab'},
{label: 'Tab Two', content: 'This is the body of the second tab'},
{label: 'Tab Three', content: 'This is the body of the third tab'},
{
label: 'Tab One',
content: 'This is the body of the first tab'},
{
label: 'Tab Two',
disabled: true,
content: 'This is the body of the second tab'},
{
label: 'Tab Three',
extraContent: true,
content: 'This is the body of the third tab'},
{
label: 'Tab Four',
content: 'This is the body of the fourth tab'
},
];
asyncTabs: Observable<any>;
+3 -1
View File
@@ -57,7 +57,9 @@ export class PortalHostDirective extends BasePortalHost implements OnDestroy {
}
set portal(p: Portal<any>) {
this._replaceAttachedPortal(p);
if (p) {
this._replaceAttachedPortal(p);
}
}
ngOnDestroy() {
+8
View File
@@ -0,0 +1,8 @@
// This mixin ensures an element spans to fill the nearest ancestor with defined positioning.
@mixin md-fill {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
-8
View File
@@ -1,8 +0,0 @@
// This mixin ensures an element spans the whole viewport.
@mixin md-fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
+1 -1
View File
@@ -2,7 +2,7 @@
// TODO(kara): animation for menu opening
@import '../core/style/button-common';
@import '../core/style/sidenav-common';
@import '../core/style/layout-common';
@import '../core/style/menu-common';
$md-menu-vertical-padding: 8px !default;
+3 -3
View File
@@ -1,6 +1,6 @@
@import '../core/style/variables';
@import '../core/style/elevation';
@import '../core/style/sidenav-common';
@import '../core/style/layout-common';
// Mixin to help with defining LTR/RTL 'transform: translate3d()' values.
@@ -57,7 +57,7 @@ md-sidenav-layout {
// TODO(hansl): Update this with a more robust solution.
&[fullscreen] {
@include md-fullscreen();
@include md-fill();
&.md-sidenav-opened {
overflow: hidden;
@@ -66,7 +66,7 @@ md-sidenav-layout {
}
.md-sidenav-backdrop {
@include md-fullscreen();
@include md-fill();
display: block;
+4 -2
View File
@@ -2,6 +2,8 @@
$md-tab-bar-height: 48px !default;
$md-tab-animation-duration: 500ms !default;
// Mixin styles for labels that are contained within the tab header.
@mixin tab-label {
line-height: $md-tab-bar-height;
@@ -36,5 +38,5 @@ $md-tab-bar-height: 48px !default;
position: absolute;
bottom: 0;
height: 2px;
transition: 350ms ease-out;
}
transition: $md-tab-animation-duration $ease-in-out-curve-function;
}
+6
View File
@@ -0,0 +1,6 @@
<div class="md-tab-body-content"
[@translateTab]="_position"
(@translateTab.start)="_onTranslateTabStarted($event)"
(@translateTab.done)="_onTranslateTabComplete($event)">
<template portalHost></template>
</div>
+12 -12
View File
@@ -6,7 +6,7 @@
[tabIndex]="selectedIndex == i ? 0 : -1"
[attr.aria-controls]="_getTabContentId(i)"
[attr.aria-selected]="selectedIndex == i"
[class.md-tab-active]="selectedIndex == i"
[class.md-tab-label-active]="selectedIndex == i"
[class.md-tab-disabled]="tab.disabled"
(click)="focusIndex = selectedIndex = i">
@@ -20,15 +20,15 @@
</div>
<md-ink-bar></md-ink-bar>
</div>
<div class="md-tab-body-wrapper">
<div class="md-tab-body"
role="tabpanel"
*ngFor="let tab of _tabs; let i = index"
[id]="_getTabContentId(i)"
[class.md-tab-active]="selectedIndex == i"
[attr.aria-labelledby]="_getTabLabelId(i)">
<template [ngIf]="selectedIndex == i">
<template [portalHost]="tab.content"></template>
</template>
</div>
<div class="md-tab-body-wrapper" #tabBodyWrapper>
<md-tab-body role="tabpanel"
*ngFor="let tab of _tabs; let i = index"
[id]="_getTabContentId(i)"
[attr.aria-labelledby]="_getTabLabelId(i)"
[class.md-tab-body-active]="selectedIndex == i"
[md-tab-body-position]="i - selectedIndex"
[md-tab-body-content]="tab.content"
(onTabBodyCentered)="_removeTabBodyWrapperHeight()"
(onTabBodyCentering)="_setTabBodyWrapperHeight($event)">
</md-tab-body>
</div>
+15 -9
View File
@@ -1,4 +1,5 @@
@import '../core/style/variables';
@import '../core/style/layout-common';
@import 'tabs-common';
:host {
@@ -32,19 +33,24 @@ md-ink-bar {
.md-tab-body-wrapper {
position: relative;
overflow: hidden;
flex-grow: 1;
display: flex;
transition: height $md-tab-animation-duration $ease-in-out-curve-function;
}
// Wraps each tab body
.md-tab-body {
display: none;
overflow: auto;
box-sizing: border-box;
flex-grow: 1;
flex-shrink: 1;
&.md-tab-active {
display: block;
md-tab-body {
@include md-fill;
display: block;
overflow: hidden;
&.md-tab-body-active {
position: relative;
overflow-x: hidden;
overflow-y: auto;
z-index: 1;
flex-grow: 1;
}
:host[md-dynamic-height] &.md-tab-body-active {
overflow-y: hidden;
}
}
+99 -8
View File
@@ -1,11 +1,16 @@
import {async, fakeAsync, tick, ComponentFixture, TestBed} from '@angular/core/testing';
import {
async, fakeAsync, tick, ComponentFixture, TestBed,
flushMicrotasks
} from '@angular/core/testing';
import {MdTabGroup, MdTabsModule} from './tabs';
import {Component, ViewChild} from '@angular/core';
import {By} from '@angular/platform-browser';
import {Observable} from 'rxjs/Observable';
import {LayoutDirection, Dir} from '../core/rtl/dir';
describe('MdTabGroup', () => {
let dir: LayoutDirection = 'ltr';
beforeEach(async(() => {
TestBed.configureTestingModule({
@@ -16,6 +21,11 @@ describe('MdTabGroup', () => {
DisabledTabsTestApp,
TabGroupWithSimpleApi,
],
providers: [
{provide: Dir, useFactory: () => {
return {value: dir};
}}
]
});
TestBed.compileComponents();
@@ -136,6 +146,87 @@ describe('MdTabGroup', () => {
expect(component.handleSelection).toHaveBeenCalledTimes(1);
expect(component.selectEvent.index).toBe(2);
}));
it('should update tab positions and attach content when selected', fakeAsync(() => {
fixture.detectChanges();
const tabComponent = fixture.debugElement.query(By.css('md-tab-group')).componentInstance;
const tabBodyList = fixture.debugElement.queryAll(By.css('md-tab-body'));
// Begin on the second tab
flushMicrotasks(); // finish animation
expect(tabBodyList[0].componentInstance._position).toBe('left');
expect(tabBodyList[0].componentInstance._content.isAttached).toBe(false);
expect(tabBodyList[1].componentInstance._position).toBe('center');
expect(tabBodyList[1].componentInstance._content.isAttached).toBe(true);
expect(tabBodyList[2].componentInstance._position).toBe('right');
expect(tabBodyList[2].componentInstance._content.isAttached).toBe(false);
// Move to third tab
tabComponent.selectedIndex = 2;
fixture.detectChanges();
flushMicrotasks(); // finish animation
expect(tabBodyList[0].componentInstance._position).toBe('left');
expect(tabBodyList[0].componentInstance._content.isAttached).toBe(false);
expect(tabBodyList[1].componentInstance._position).toBe('left');
expect(tabBodyList[1].componentInstance._content.isAttached).toBe(false);
expect(tabBodyList[2].componentInstance._position).toBe('center');
expect(tabBodyList[2].componentInstance._content.isAttached).toBe(true);
// Move to the first tab
tabComponent.selectedIndex = 0;
fixture.detectChanges();
flushMicrotasks(); // finish animation
// Check that the tab bodies have correctly positions themselves
expect(tabBodyList[0].componentInstance._position).toBe('center');
expect(tabBodyList[0].componentInstance._content.isAttached).toBe(true);
expect(tabBodyList[1].componentInstance._position).toBe('right');
expect(tabBodyList[1].componentInstance._content.isAttached).toBe(false);
expect(tabBodyList[2].componentInstance._position).toBe('right');
expect(tabBodyList[2].componentInstance._content.isAttached).toBe(false);
}));
it('should support RTL for the tab positions', fakeAsync(() => {
dir = 'rtl';
fixture.detectChanges();
const tabComponent = fixture.debugElement.query(By.css('md-tab-group')).componentInstance;
const tabBodyList = fixture.debugElement.queryAll(By.css('md-tab-body'));
// Begin on the second tab
flushMicrotasks(); // finish animation
expect(tabBodyList[0].componentInstance._position).toBe('right');
expect(tabBodyList[1].componentInstance._position).toBe('center');
expect(tabBodyList[2].componentInstance._position).toBe('left');
// Move to third tab
tabComponent.selectedIndex = 2;
fixture.detectChanges();
flushMicrotasks(); // finish animation
expect(tabBodyList[0].componentInstance._position).toBe('right');
expect(tabBodyList[1].componentInstance._position).toBe('right');
expect(tabBodyList[2].componentInstance._position).toBe('center');
// Move to the first tab
tabComponent.selectedIndex = 0;
fixture.detectChanges();
flushMicrotasks(); // finish animation
// Check that the tab bodies have correctly positions themselves
expect(tabBodyList[0].componentInstance._position).toBe('center');
expect(tabBodyList[1].componentInstance._position).toBe('left');
expect(tabBodyList[2].componentInstance._position).toBe('left');
}));
});
describe('disabled tabs', () => {
@@ -278,8 +369,8 @@ describe('MdTabGroup', () => {
});
/**
* Checks that the `selectedIndex` has been updated; checks that the label and body have the
* `md-tab-active` class
* Checks that the `selectedIndex` has been updated; checks that the label and body have their
* respective `active` classes
*/
function checkSelectedIndex(index: number, fixture: ComponentFixture<any>) {
fixture.detectChanges();
@@ -290,19 +381,19 @@ describe('MdTabGroup', () => {
let tabLabelElement = fixture.debugElement
.query(By.css(`.md-tab-label:nth-of-type(${index + 1})`)).nativeElement;
expect(tabLabelElement.classList.contains('md-tab-active')).toBe(true);
expect(tabLabelElement.classList.contains('md-tab-label-active')).toBe(true);
let tabContentElement = fixture.debugElement
.query(By.css(`#${tabLabelElement.id}`)).nativeElement;
expect(tabContentElement.classList.contains('md-tab-active')).toBe(true);
.query(By.css(`md-tab-body:nth-of-type(${index + 1})`)).nativeElement;
expect(tabContentElement.classList.contains('md-tab-body-active')).toBe(true);
}
function getSelectedLabel(fixture: ComponentFixture<any>): HTMLElement {
return fixture.nativeElement.querySelector('.md-tab-label.md-tab-active');
return fixture.nativeElement.querySelector('.md-tab-label-active');
}
function getSelectedContent(fixture: ComponentFixture<any>): HTMLElement {
return fixture.nativeElement.querySelector('.md-tab-body.md-tab-active');
return fixture.nativeElement.querySelector('.md-tab-body-active');
}
});
+160 -37
View File
@@ -14,6 +14,15 @@ import {
TemplateRef,
ViewContainerRef,
OnInit,
trigger,
state,
style,
animate,
transition,
AnimationTransitionEvent,
ElementRef,
Renderer,
Optional,
} from '@angular/core';
import {CommonModule} from '@angular/common';
import {
@@ -23,6 +32,9 @@ import {
LEFT_ARROW,
ENTER,
coerceBooleanProperty,
PortalHostDirective,
Dir,
LayoutDirection
} from '../core';
import {MdTabLabel} from './tab-label';
import {MdTabLabelWrapper} from './tab-label-wrapper';
@@ -82,7 +94,7 @@ export class MdTab implements OnInit {
moduleId: module.id,
selector: 'md-tab-group',
templateUrl: 'tab-group.html',
styleUrls: ['tab-group.css'],
styleUrls: ['tab-group.css']
})
export class MdTabGroup {
@ContentChildren(MdTab) _tabs: QueryList<MdTab>;
@@ -90,11 +102,23 @@ export class MdTabGroup {
@ViewChildren(MdTabLabelWrapper) _labelWrappers: QueryList<MdTabLabelWrapper>;
@ViewChildren(MdInkBar) _inkBar: QueryList<MdInkBar>;
@ViewChild('tabBodyWrapper') _tabBodyWrapper: ElementRef;
private _isInitialized: boolean = false;
/** Snapshot of the height of the tab body wrapper before another tab is activated. */
private _tabBodyWrapperHeight: number = 0;
/** Whether the tab group should grow to the size of the active tab */
private _dynamicHeight: boolean = false;
@Input('md-dynamic-height') set dynamicHeight(value: boolean) {
this._dynamicHeight = coerceBooleanProperty(value);
}
/** The index of the active tab. */
private _selectedIndex: number = 0;
@Input()
set selectedIndex(value: number) {
@Input() set selectedIndex(value: number) {
this._tabBodyWrapperHeight = this._tabBodyWrapper.nativeElement.clientHeight;
if (value != this._selectedIndex && this.isValidIndex(value)) {
this._selectedIndex = value;
@@ -107,19 +131,6 @@ export class MdTabGroup {
return this._selectedIndex;
}
/**
* Determines if an index is valid. If the tabs are not ready yet, we assume that the user is
* providing a valid index and return true.
*/
isValidIndex(index: number): boolean {
if (this._tabs) {
const tab = this._tabs.toArray()[index];
return tab && !tab.disabled;
} else {
return true;
}
}
/** Output to enable support for two-way binding on `selectedIndex`. */
@Output() get selectedIndexChange(): Observable<number> {
return this.selectChange.map(event => event.index);
@@ -138,7 +149,7 @@ export class MdTabGroup {
private _focusIndex: number = 0;
private _groupId: number;
constructor(private _zone: NgZone) {
constructor(private _zone: NgZone, private _renderer: Renderer) {
this._groupId = nextId++;
}
@@ -156,6 +167,19 @@ export class MdTabGroup {
this._isInitialized = true;
}
/**
* Determines if an index is valid. If the tabs are not ready yet, we assume that the user is
* providing a valid index and return true.
*/
isValidIndex(index: number): boolean {
if (this._tabs) {
const tab = this._tabs.toArray()[index];
return tab && !tab.disabled;
} else {
return true;
}
}
/** Tells the ink-bar to align itself to the current label wrapper */
private _updateInkBar(): void {
this._inkBar.toArray()[0].alignToElement(this._currentLabelWrapper);
@@ -191,25 +215,6 @@ export class MdTabGroup {
}
}
private _createChangeEvent(index: number): MdTabChangeEvent {
const event = new MdTabChangeEvent;
event.index = index;
if (this._tabs && this._tabs.length) {
event.tab = this._tabs.toArray()[index];
}
return event;
}
/** Returns a unique id for each tab label element */
_getTabLabelId(i: number): string {
return `md-tab-label-${this._groupId}-${i}`;
}
/** Returns a unique id for each tab content element */
_getTabContentId(i: number): string {
return `md-tab-content-${this._groupId}-${i}`;
}
handleKeydown(event: KeyboardEvent) {
switch (event.keyCode) {
case RIGHT_ARROW:
@@ -248,15 +253,133 @@ export class MdTabGroup {
focusPreviousTab(): void {
this.moveFocus(-1);
}
private _createChangeEvent(index: number): MdTabChangeEvent {
const event = new MdTabChangeEvent;
event.index = index;
if (this._tabs && this._tabs.length) {
event.tab = this._tabs.toArray()[index];
}
return event;
}
/** Returns a unique id for each tab label element */
_getTabLabelId(i: number): string {
return `md-tab-label-${this._groupId}-${i}`;
}
/** Returns a unique id for each tab content element */
_getTabContentId(i: number): string {
return `md-tab-content-${this._groupId}-${i}`;
}
/**
* Sets the height of the body wrapper to the height of the activating tab if dynamic
* height property is true.
*/
_setTabBodyWrapperHeight(tabHeight: number): void {
if (!this._dynamicHeight) { return; }
this._renderer.setElementStyle(this._tabBodyWrapper.nativeElement, 'height',
this._tabBodyWrapperHeight + 'px');
// This conditional forces the browser to paint the height so that
// the animation to the new height can have an origin.
if (this._tabBodyWrapper.nativeElement.offsetHeight) {
this._renderer.setElementStyle(this._tabBodyWrapper.nativeElement, 'height',
tabHeight + 'px');
}
}
/** Removes the height of the tab body wrapper. */
_removeTabBodyWrapperHeight(): void {
this._renderer.setElementStyle(this._tabBodyWrapper.nativeElement, 'height', '');
}
}
export type MdTabBodyActiveState = 'left' | 'center' | 'right';
@Component({
moduleId: module.id,
selector: 'md-tab-body',
templateUrl: 'tab-body.html',
animations: [
trigger('translateTab', [
state('left', style({transform: 'translate3d(-100%, 0, 0)'})),
state('center', style({transform: 'translate3d(0, 0, 0)'})),
state('right', style({transform: 'translate3d(100%, 0, 0)'})),
transition('* => *', animate('500ms cubic-bezier(0.35, 0, 0.25, 1)')),
])
]
})
export class MdTabBody implements OnInit {
/** The portal host inside of this container into which the tab body content will be loaded. */
@ViewChild(PortalHostDirective) _portalHost: PortalHostDirective;
/** Event emitted when the tab begins to animate towards the center as the active tab. */
@Output()
onTabBodyCentering: EventEmitter<number> = new EventEmitter<number>();
/** Event emitted when the tab completes its animation towards the center. */
@Output()
onTabBodyCentered: EventEmitter<void> = new EventEmitter<void>();
/** The tab body content to display. */
@Input('md-tab-body-content') _content: TemplatePortal;
/** The shifted index position of the tab body, where zero represents the active center tab. */
_position: MdTabBodyActiveState;
@Input('md-tab-body-position') set position(v: number) {
if (v < 0) {
this._position = this.getLayoutDirection() == 'ltr' ? 'left' : 'right';
} else if (v > 0) {
this._position = this.getLayoutDirection() == 'ltr' ? 'right' : 'left';
} else {
this._position = 'center';
}
if (this._position === 'center' && !this._portalHost.hasAttached() && this._content) {
this._portalHost.attach(this._content);
}
}
constructor(private _elementRef: ElementRef, @Optional() private _dir: Dir) {}
ngOnInit() {
if (this._position == 'center' && !this._portalHost.hasAttached()) {
this._portalHost.attach(this._content);
}
}
_onTranslateTabStarted(e: AnimationTransitionEvent) {
if (e.fromState != 'void' && e.toState == 'center') {
this.onTabBodyCentering.emit(this._elementRef.nativeElement.clientHeight);
}
}
_onTranslateTabComplete(e: AnimationTransitionEvent) {
if ((e.toState == 'left' || e.toState == 'right') && this._position !== 'center') {
// If the end state is that the tab is not centered, then detach the content.
this._portalHost.detach();
}
if ((e.toState == 'center') && this._position == 'center') {
this.onTabBodyCentered.emit();
}
}
/** The text direction of the containing app. */
getLayoutDirection(): LayoutDirection {
return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr';
}
}
@NgModule({
imports: [CommonModule, PortalModule],
// Don't export MdInkBar or MdTabLabelWrapper, as they are internal implementation details.
exports: [MdTabGroup, MdTabLabel, MdTab, MdTabNavBar, MdTabLink],
declarations: [MdTabGroup, MdTabLabel, MdTab, MdInkBar, MdTabLabelWrapper,
MdTabNavBar, MdTabLink],
MdTabNavBar, MdTabLink, MdTabBody],
})
export class MdTabsModule {
static forRoot(): ModuleWithProviders {