Skip to content

Commit f6944e4

Browse files
andrewseguinjelbourn
authored andcommitted
feat(tabs): animate tab change, include optional dynamic height (#1788)
1 parent 716372b commit f6944e4

15 files changed

+416
-111
lines changed

Diff for: e2e/components/tabs/tabs.e2e.ts

+16-23
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ describe('tabs', () => {
1111
browser.get('/tabs');
1212
tabGroup = element(by.css('md-tab-group'));
1313
tabLabels = element.all(by.css('.md-tab-label'));
14-
tabBodies = element.all(by.css('.md-tab-body'));
14+
tabBodies = element.all(by.css('md-tab-body'));
1515
});
1616

1717
it('should change tabs when the label is clicked', () => {
1818
tabLabels.get(1).click();
19-
expect(getActiveStates(tabLabels)).toEqual([false, true, false]);
20-
expect(getActiveStates(tabBodies)).toEqual([false, true, false]);
19+
expect(getLabelActiveStates(tabLabels)).toEqual([false, true, false]);
20+
expect(getBodyActiveStates(tabBodies)).toEqual([false, true, false]);
2121

2222
tabLabels.get(0).click();
23-
expect(getActiveStates(tabLabels)).toEqual([true, false, false]);
24-
expect(getActiveStates(tabBodies)).toEqual([true, false, false]);
23+
expect(getLabelActiveStates(tabLabels)).toEqual([true, false, false]);
24+
expect(getBodyActiveStates(tabBodies)).toEqual([true, false, false]);
2525
});
2626

2727
it('should change focus with keyboard interaction', () => {
@@ -49,18 +49,13 @@ describe('tabs', () => {
4949
});
5050
});
5151

52-
/**
53-
* A helper function to perform the sendKey action
54-
* @param key
55-
*/
52+
/** A helper function to perform the sendKey action. */
5653
function pressKey(key: string) {
5754
browser.actions().sendKeys(key).perform();
5855
}
5956

6057
/**
61-
* Returns an array of true/false that represents the focus states of the provided elements
62-
* @param elements
63-
* @returns {webdriver.promise.Promise<Promise<boolean>[]>|webdriver.promise.Promise<T[]>}
58+
* Returns an array of true/false that represents the focus states of the provided elements.
6459
*/
6560
function getFocusStates(elements: ElementArrayFinder) {
6661
return elements.map(element => {
@@ -72,21 +67,19 @@ function getFocusStates(elements: ElementArrayFinder) {
7267
});
7368
}
7469

75-
/**
76-
* Returns an array of true/false that represents the active states for the provided elements
77-
* @param elements
78-
* @returns {webdriver.promise.Promise<Promise<boolean>[]>|webdriver.promise.Promise<T[]>}
79-
*/
80-
function getActiveStates(elements: ElementArrayFinder) {
81-
return getClassStates(elements, 'md-tab-active');
70+
/** Returns an array of true/false that represents the active states for the provided elements. */
71+
function getLabelActiveStates(elements: ElementArrayFinder) {
72+
return getClassStates(elements, 'md-tab-label-active');
73+
}
74+
75+
/** Returns an array of true/false that represents the active states for the provided elements */
76+
function getBodyActiveStates(elements: ElementArrayFinder) {
77+
return getClassStates(elements, 'md-tab-body-active');
8278
}
8379

8480
/**
8581
* Returns an array of true/false values that represents whether the provided className is on
86-
* each element
87-
* @param elements
88-
* @param className
89-
* @returns {webdriver.promise.Promise<Promise<boolean>[]>|webdriver.promise.Promise<T[]>}
82+
* each element.
9083
*/
9184
function getClassStates(elements: ElementArrayFinder, className: string) {
9285
return elements.map(element => {

Diff for: src/demo-app/tabs/tabs-demo.html

+73-3
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,84 @@ <h1>Tab Nav Bar</h1>
1414
</div>
1515

1616

17-
<h1>Tab Group Demo</h1>
17+
<h1>Tab Group Demo - Dynamic Height</h1>
1818

19-
<md-tab-group class="demo-tab-group">
20-
<md-tab *ngFor="let tab of tabs; let i = index" [disabled]="i == 1">
19+
<md-tab-group class="demo-tab-group" md-dynamic-height>
20+
<md-tab *ngFor="let tab of tabs" [disabled]="tab.disabled">
21+
<template md-tab-label>{{tab.label}}</template>
22+
{{tab.content}}
23+
<br>
24+
<br>
25+
<div *ngIf="tab.extraContent">
26+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla venenatis ante augue.
27+
Phasellus volutpat neque ac dui mattis vulputate. Etiam consequat aliquam cursus.
28+
In sodales pretium ultrices. Maecenas lectus est, sollicitudin consectetur felis nec,
29+
feugiat ultricies mi. Aliquam erat volutpat. Nam placerat, tortor in ultrices porttitor,
30+
orci enim rutrum enim, vel tempor sapien arcu a tellus. Vivamus convallis sodales ante varius
31+
gravida. Curabitur a purus vel augue ultrices ultricies id a nisl. Nullam malesuada consequat
32+
diam, a facilisis tortor volutpat et. Sed urna dolor, aliquet vitae posuere vulputate, euismod
33+
ac lorem. Sed felis risus, pulvinar at interdum quis, vehicula sed odio. Phasellus in enim
34+
venenatis, iaculis tortor eu, bibendum ante. Donec ac tellus dictum neque volutpat blandit.
35+
Praesent efficitur faucibus risus, ac auctor purus porttitor vitae. Phasellus ornare dui nec
36+
orci posuere, nec luctus mauris semper.
37+
<br>
38+
<br>
39+
Morbi viverra, ante vel aliquet tincidunt, leo dolor pharetra quam, at semper massa orci nec
40+
magna. Donec posuere nec sapien sed laoreet. Etiam cursus nunc in condimentum facilisis.
41+
Etiam in tempor tortor. Vivamus faucibus egestas enim, at convallis diam pulvinar vel.
42+
Cras ac orci eget nisi maximus cursus. Nunc urna libero, viverra sit amet nisl at, hendrerit
43+
tempor turpis. Maecenas facilisis convallis mi vel tempor. Nullam vitae nunc leo. Cras sed
44+
nisl consectetur, rhoncus sapien sit amet, tempus sapien.
45+
<br>
46+
<br>
47+
Integer turpis erat, porttitor vitae mi faucibus, laoreet interdum tellus. Curabitur posuere
48+
molestie dictum. Morbi eget congue risus, quis rhoncus quam. Suspendisse vitae hendrerit erat,
49+
at posuere mi. Cras eu fermentum nunc. Sed id ante eu orci commodo volutpat non ac est.
50+
Praesent ligula diam, congue eu enim scelerisque, finibus commodo lectus.
51+
</div>
52+
<br>
53+
<br>
54+
<md-input placeholder="Tab Label" [(ngModel)]="tab.label"></md-input>
55+
</md-tab>
56+
</md-tab-group>
57+
58+
59+
<h1>Tab Group Demo - Fixed Height</h1>
60+
61+
<md-tab-group class="demo-tab-group" style="height: 200px">
62+
<md-tab *ngFor="let tab of tabs" [disabled]="tab.disabled">
2163
<template md-tab-label>{{tab.label}}</template>
2264
{{tab.content}}
2365
<br>
2466
<br>
67+
<div *ngIf="tab.extraContent">
68+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla venenatis ante augue.
69+
Phasellus volutpat neque ac dui mattis vulputate. Etiam consequat aliquam cursus.
70+
In sodales pretium ultrices. Maecenas lectus est, sollicitudin consectetur felis nec,
71+
feugiat ultricies mi. Aliquam erat volutpat. Nam placerat, tortor in ultrices porttitor,
72+
orci enim rutrum enim, vel tempor sapien arcu a tellus. Vivamus convallis sodales ante varius
73+
gravida. Curabitur a purus vel augue ultrices ultricies id a nisl. Nullam malesuada consequat
74+
diam, a facilisis tortor volutpat et. Sed urna dolor, aliquet vitae posuere vulputate, euismod
75+
ac lorem. Sed felis risus, pulvinar at interdum quis, vehicula sed odio. Phasellus in enim
76+
venenatis, iaculis tortor eu, bibendum ante. Donec ac tellus dictum neque volutpat blandit.
77+
Praesent efficitur faucibus risus, ac auctor purus porttitor vitae. Phasellus ornare dui nec
78+
orci posuere, nec luctus mauris semper.
79+
<br>
80+
<br>
81+
Morbi viverra, ante vel aliquet tincidunt, leo dolor pharetra quam, at semper massa orci nec
82+
magna. Donec posuere nec sapien sed laoreet. Etiam cursus nunc in condimentum facilisis.
83+
Etiam in tempor tortor. Vivamus faucibus egestas enim, at convallis diam pulvinar vel.
84+
Cras ac orci eget nisi maximus cursus. Nunc urna libero, viverra sit amet nisl at, hendrerit
85+
tempor turpis. Maecenas facilisis convallis mi vel tempor. Nullam vitae nunc leo. Cras sed
86+
nisl consectetur, rhoncus sapien sit amet, tempus sapien.
87+
<br>
88+
<br>
89+
Integer turpis erat, porttitor vitae mi faucibus, laoreet interdum tellus. Curabitur posuere
90+
molestie dictum. Morbi eget congue risus, quis rhoncus quam. Suspendisse vitae hendrerit erat,
91+
at posuere mi. Cras eu fermentum nunc. Sed id ante eu orci commodo volutpat non ac est.
92+
Praesent ligula diam, congue eu enim scelerisque, finibus commodo lectus.
93+
</div>
94+
<br>
2595
<br>
2696
<md-input placeholder="Tab Label" [(ngModel)]="tab.label"></md-input>
2797
</md-tab>

Diff for: src/demo-app/tabs/tabs-demo.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
.md-tab-header {
1717
background: #f9f9f9;
1818
}
19-
.md-tab-body {
19+
.md-tab-body-content {
2020
padding: 12px;
2121
}
2222
}

Diff for: src/demo-app/tabs/tabs-demo.ts

+15-3
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,21 @@ export class TabsDemo {
1818
activeLinkIndex = 0;
1919

2020
tabs = [
21-
{label: 'Tab One', content: 'This is the body of the first tab'},
22-
{label: 'Tab Two', content: 'This is the body of the second tab'},
23-
{label: 'Tab Three', content: 'This is the body of the third tab'},
21+
{
22+
label: 'Tab One',
23+
content: 'This is the body of the first tab'},
24+
{
25+
label: 'Tab Two',
26+
disabled: true,
27+
content: 'This is the body of the second tab'},
28+
{
29+
label: 'Tab Three',
30+
extraContent: true,
31+
content: 'This is the body of the third tab'},
32+
{
33+
label: 'Tab Four',
34+
content: 'This is the body of the fourth tab'
35+
},
2436
];
2537

2638
asyncTabs: Observable<any>;

Diff for: src/lib/core/portal/portal-directives.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ export class PortalHostDirective extends BasePortalHost implements OnDestroy {
5757
}
5858

5959
set portal(p: Portal<any>) {
60-
this._replaceAttachedPortal(p);
60+
if (p) {
61+
this._replaceAttachedPortal(p);
62+
}
6163
}
6264

6365
ngOnDestroy() {

Diff for: src/lib/core/style/_layout-common.scss

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// This mixin ensures an element spans to fill the nearest ancestor with defined positioning.
2+
@mixin md-fill {
3+
position: absolute;
4+
top: 0;
5+
left: 0;
6+
right: 0;
7+
bottom: 0;
8+
}

Diff for: src/lib/core/style/_sidenav-common.scss

-8
This file was deleted.

Diff for: src/lib/menu/menu.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// TODO(kara): animation for menu opening
33

44
@import '../core/style/button-common';
5-
@import '../core/style/sidenav-common';
5+
@import '../core/style/layout-common';
66
@import '../core/style/menu-common';
77

88
$md-menu-vertical-padding: 8px !default;

Diff for: src/lib/sidenav/sidenav.scss

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@import '../core/style/variables';
22
@import '../core/style/elevation';
3-
@import '../core/style/sidenav-common';
3+
@import '../core/style/layout-common';
44

55

66
// Mixin to help with defining LTR/RTL 'transform: translate3d()' values.
@@ -57,7 +57,7 @@ md-sidenav-layout {
5757

5858
// TODO(hansl): Update this with a more robust solution.
5959
&[fullscreen] {
60-
@include md-fullscreen();
60+
@include md-fill();
6161

6262
&.md-sidenav-opened {
6363
overflow: hidden;
@@ -66,7 +66,7 @@ md-sidenav-layout {
6666
}
6767

6868
.md-sidenav-backdrop {
69-
@include md-fullscreen();
69+
@include md-fill();
7070

7171
display: block;
7272

Diff for: src/lib/tabs/_tabs-common.scss

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
$md-tab-bar-height: 48px !default;
44

5+
$md-tab-animation-duration: 500ms !default;
6+
57
// Mixin styles for labels that are contained within the tab header.
68
@mixin tab-label {
79
line-height: $md-tab-bar-height;
@@ -36,5 +38,5 @@ $md-tab-bar-height: 48px !default;
3638
position: absolute;
3739
bottom: 0;
3840
height: 2px;
39-
transition: 350ms ease-out;
40-
}
41+
transition: $md-tab-animation-duration $ease-in-out-curve-function;
42+
}

Diff for: src/lib/tabs/tab-body.html

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<div class="md-tab-body-content"
2+
[@translateTab]="_position"
3+
(@translateTab.start)="_onTranslateTabStarted($event)"
4+
(@translateTab.done)="_onTranslateTabComplete($event)">
5+
<template portalHost></template>
6+
</div>

Diff for: src/lib/tabs/tab-group.html

+12-12
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[tabIndex]="selectedIndex == i ? 0 : -1"
77
[attr.aria-controls]="_getTabContentId(i)"
88
[attr.aria-selected]="selectedIndex == i"
9-
[class.md-tab-active]="selectedIndex == i"
9+
[class.md-tab-label-active]="selectedIndex == i"
1010
[class.md-tab-disabled]="tab.disabled"
1111
(click)="focusIndex = selectedIndex = i">
1212

@@ -20,15 +20,15 @@
2020
</div>
2121
<md-ink-bar></md-ink-bar>
2222
</div>
23-
<div class="md-tab-body-wrapper">
24-
<div class="md-tab-body"
25-
role="tabpanel"
26-
*ngFor="let tab of _tabs; let i = index"
27-
[id]="_getTabContentId(i)"
28-
[class.md-tab-active]="selectedIndex == i"
29-
[attr.aria-labelledby]="_getTabLabelId(i)">
30-
<template [ngIf]="selectedIndex == i">
31-
<template [portalHost]="tab.content"></template>
32-
</template>
33-
</div>
23+
<div class="md-tab-body-wrapper" #tabBodyWrapper>
24+
<md-tab-body role="tabpanel"
25+
*ngFor="let tab of _tabs; let i = index"
26+
[id]="_getTabContentId(i)"
27+
[attr.aria-labelledby]="_getTabLabelId(i)"
28+
[class.md-tab-body-active]="selectedIndex == i"
29+
[md-tab-body-position]="i - selectedIndex"
30+
[md-tab-body-content]="tab.content"
31+
(onTabBodyCentered)="_removeTabBodyWrapperHeight()"
32+
(onTabBodyCentering)="_setTabBodyWrapperHeight($event)">
33+
</md-tab-body>
3434
</div>

Diff for: src/lib/tabs/tab-group.scss

+15-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@import '../core/style/variables';
2+
@import '../core/style/layout-common';
23
@import 'tabs-common';
34

45
:host {
@@ -32,19 +33,24 @@ md-ink-bar {
3233
.md-tab-body-wrapper {
3334
position: relative;
3435
overflow: hidden;
35-
flex-grow: 1;
3636
display: flex;
37+
transition: height $md-tab-animation-duration $ease-in-out-curve-function;
3738
}
3839

3940
// Wraps each tab body
40-
.md-tab-body {
41-
display: none;
42-
overflow: auto;
43-
box-sizing: border-box;
44-
flex-grow: 1;
45-
flex-shrink: 1;
46-
&.md-tab-active {
47-
display: block;
41+
md-tab-body {
42+
@include md-fill;
43+
display: block;
44+
overflow: hidden;
45+
&.md-tab-body-active {
46+
position: relative;
47+
overflow-x: hidden;
48+
overflow-y: auto;
49+
z-index: 1;
50+
flex-grow: 1;
51+
}
52+
:host[md-dynamic-height] &.md-tab-body-active {
53+
overflow-y: hidden;
4854
}
4955
}
5056

0 commit comments

Comments
 (0)