mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(tabs): animate tab change, include optional dynamic height (#1788)
This commit is contained in:
committed by
Jeremy Elbourn
parent
716372bc85
commit
f6944e4f90
@@ -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 => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
.md-tab-header {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
.md-tab-body {
|
||||
.md-tab-body-content {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user