Skip to content

Commit

Permalink
feat(tabs): animate tab change, include optional dynamic height (#1788)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewseguin authored and jelbourn committed Nov 11, 2016
1 parent 716372b commit f6944e4
Show file tree
Hide file tree
Showing 15 changed files with 416 additions and 111 deletions.
39 changes: 16 additions & 23 deletions e2e/components/tabs/tabs.e2e.ts
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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 => {
Expand All @@ -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 => {
Expand Down
76 changes: 73 additions & 3 deletions src/demo-app/tabs/tabs-demo.html
Expand Up @@ -14,14 +14,84 @@ <h1>Tab Nav Bar</h1>
</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>
Expand Down
2 changes: 1 addition & 1 deletion src/demo-app/tabs/tabs-demo.scss
Expand Up @@ -16,7 +16,7 @@
.md-tab-header {
background: #f9f9f9;
}
.md-tab-body {
.md-tab-body-content {
padding: 12px;
}
}
18 changes: 15 additions & 3 deletions src/demo-app/tabs/tabs-demo.ts
Expand Up @@ -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>;
Expand Down
4 changes: 3 additions & 1 deletion src/lib/core/portal/portal-directives.ts
Expand Up @@ -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() {
Expand Down
8 changes: 8 additions & 0 deletions src/lib/core/style/_layout-common.scss
@@ -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 changes: 0 additions & 8 deletions src/lib/core/style/_sidenav-common.scss

This file was deleted.

2 changes: 1 addition & 1 deletion src/lib/menu/menu.scss
Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions src/lib/sidenav/sidenav.scss
@@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -66,7 +66,7 @@ md-sidenav-layout {
}

.md-sidenav-backdrop {
@include md-fullscreen();
@include md-fill();

display: block;

Expand Down
6 changes: 4 additions & 2 deletions src/lib/tabs/_tabs-common.scss
Expand Up @@ -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;
Expand Down Expand Up @@ -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 changes: 6 additions & 0 deletions src/lib/tabs/tab-body.html
@@ -0,0 +1,6 @@
<div class="md-tab-body-content"
[@translateTab]="_position"
(@translateTab.start)="_onTranslateTabStarted($event)"
(@translateTab.done)="_onTranslateTabComplete($event)">
<template portalHost></template>
</div>
24 changes: 12 additions & 12 deletions src/lib/tabs/tab-group.html
Expand Up @@ -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">

Expand All @@ -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>
24 changes: 15 additions & 9 deletions src/lib/tabs/tab-group.scss
@@ -1,4 +1,5 @@
@import '../core/style/variables';
@import '../core/style/layout-common';
@import 'tabs-common';

:host {
Expand Down Expand Up @@ -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;
}
}

Expand Down

0 comments on commit f6944e4

Please sign in to comment.