diff --git a/e2e/components/tabs/tabs.e2e.ts b/e2e/components/tabs/tabs.e2e.ts index daf4171824a7..61f8a9b9f95e 100644 --- a/e2e/components/tabs/tabs.e2e.ts +++ b/e2e/components/tabs/tabs.e2e.ts @@ -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[]>|webdriver.promise.Promise} + * 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[]>|webdriver.promise.Promise} - */ -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[]>|webdriver.promise.Promise} + * each element. */ function getClassStates(elements: ElementArrayFinder, className: string) { return elements.map(element => { diff --git a/src/demo-app/tabs/tabs-demo.html b/src/demo-app/tabs/tabs-demo.html index 99d73d9ec327..3d8943a29714 100644 --- a/src/demo-app/tabs/tabs-demo.html +++ b/src/demo-app/tabs/tabs-demo.html @@ -14,14 +14,84 @@

Tab Nav Bar

-

Tab Group Demo

+

Tab Group Demo - Dynamic Height

- - + + + + {{tab.content}} +
+
+
+ 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. +
+
+ 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. +
+
+ 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. +
+
+
+ +
+
+ + +

Tab Group Demo - Fixed Height

+ + + {{tab.content}}

+
+ 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. +
+
+ 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. +
+
+ 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. +
+

diff --git a/src/demo-app/tabs/tabs-demo.scss b/src/demo-app/tabs/tabs-demo.scss index 0a0337ee419c..2dc75c934f18 100644 --- a/src/demo-app/tabs/tabs-demo.scss +++ b/src/demo-app/tabs/tabs-demo.scss @@ -16,7 +16,7 @@ .md-tab-header { background: #f9f9f9; } - .md-tab-body { + .md-tab-body-content { padding: 12px; } } \ No newline at end of file diff --git a/src/demo-app/tabs/tabs-demo.ts b/src/demo-app/tabs/tabs-demo.ts index da9988ddd371..488f28082077 100644 --- a/src/demo-app/tabs/tabs-demo.ts +++ b/src/demo-app/tabs/tabs-demo.ts @@ -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; diff --git a/src/lib/core/portal/portal-directives.ts b/src/lib/core/portal/portal-directives.ts index dab58967322e..75feb45f10eb 100644 --- a/src/lib/core/portal/portal-directives.ts +++ b/src/lib/core/portal/portal-directives.ts @@ -57,7 +57,9 @@ export class PortalHostDirective extends BasePortalHost implements OnDestroy { } set portal(p: Portal) { - this._replaceAttachedPortal(p); + if (p) { + this._replaceAttachedPortal(p); + } } ngOnDestroy() { diff --git a/src/lib/core/style/_layout-common.scss b/src/lib/core/style/_layout-common.scss new file mode 100644 index 000000000000..4748f3b6fb80 --- /dev/null +++ b/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; +} diff --git a/src/lib/core/style/_sidenav-common.scss b/src/lib/core/style/_sidenav-common.scss deleted file mode 100644 index 1499cfc6bdde..000000000000 --- a/src/lib/core/style/_sidenav-common.scss +++ /dev/null @@ -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; -} diff --git a/src/lib/menu/menu.scss b/src/lib/menu/menu.scss index a9e4ba09670d..8dfc6096a723 100644 --- a/src/lib/menu/menu.scss +++ b/src/lib/menu/menu.scss @@ -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; diff --git a/src/lib/sidenav/sidenav.scss b/src/lib/sidenav/sidenav.scss index c38d47a4a00c..34b67918c9b8 100644 --- a/src/lib/sidenav/sidenav.scss +++ b/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. @@ -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; diff --git a/src/lib/tabs/_tabs-common.scss b/src/lib/tabs/_tabs-common.scss index 672cfa6495e2..fd6d640418d4 100644 --- a/src/lib/tabs/_tabs-common.scss +++ b/src/lib/tabs/_tabs-common.scss @@ -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; -} \ No newline at end of file + transition: $md-tab-animation-duration $ease-in-out-curve-function; +} diff --git a/src/lib/tabs/tab-body.html b/src/lib/tabs/tab-body.html new file mode 100644 index 000000000000..2d89174484c7 --- /dev/null +++ b/src/lib/tabs/tab-body.html @@ -0,0 +1,6 @@ +
+ +
diff --git a/src/lib/tabs/tab-group.html b/src/lib/tabs/tab-group.html index 6acbed0769f4..370373d22077 100644 --- a/src/lib/tabs/tab-group.html +++ b/src/lib/tabs/tab-group.html @@ -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 @@ -
-
- -
+
+ +
diff --git a/src/lib/tabs/tab-group.scss b/src/lib/tabs/tab-group.scss index 3e48ae569952..42a140d08751 100644 --- a/src/lib/tabs/tab-group.scss +++ b/src/lib/tabs/tab-group.scss @@ -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; } } diff --git a/src/lib/tabs/tab-group.spec.ts b/src/lib/tabs/tab-group.spec.ts index 1a635d9706bb..f4e8adfeca9f 100644 --- a/src/lib/tabs/tab-group.spec.ts +++ b/src/lib/tabs/tab-group.spec.ts @@ -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) { 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): HTMLElement { - return fixture.nativeElement.querySelector('.md-tab-label.md-tab-active'); + return fixture.nativeElement.querySelector('.md-tab-label-active'); } function getSelectedContent(fixture: ComponentFixture): HTMLElement { - return fixture.nativeElement.querySelector('.md-tab-body.md-tab-active'); + return fixture.nativeElement.querySelector('.md-tab-body-active'); } }); diff --git a/src/lib/tabs/tabs.ts b/src/lib/tabs/tabs.ts index 8c88f1cc1d78..fa287c929dec 100644 --- a/src/lib/tabs/tabs.ts +++ b/src/lib/tabs/tabs.ts @@ -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; @@ -90,11 +102,23 @@ export class MdTabGroup { @ViewChildren(MdTabLabelWrapper) _labelWrappers: QueryList; @ViewChildren(MdInkBar) _inkBar: QueryList; + @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 { 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 = new EventEmitter(); + + /** Event emitted when the tab completes its animation towards the center. */ + @Output() + onTabBodyCentered: EventEmitter = new EventEmitter(); + + /** 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 {