diff --git a/src/demo-app/tabs/tabs-demo.html b/src/demo-app/tabs/tabs-demo.html index fe5deabe9a41..93340efdd6d2 100644 --- a/src/demo-app/tabs/tabs-demo.html +++ b/src/demo-app/tabs/tabs-demo.html @@ -14,6 +14,79 @@

Tab Nav Bar

+

Tab Group Demo - Dynamic Tabs

+ + + Add New Tab + + + Include extra content + + + Select after adding + +
+ Position: + +
+ +
+
+ +
+ Selected tab index: + +
+ + + + {{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 - Dynamic Height

@@ -129,4 +202,4 @@

Tabs with simplified api

This tab is about combustion! -
+ \ No newline at end of file diff --git a/src/demo-app/tabs/tabs-demo.scss b/src/demo-app/tabs/tabs-demo.scss index 2dc75c934f18..552506ba1789 100644 --- a/src/demo-app/tabs/tabs-demo.scss +++ b/src/demo-app/tabs/tabs-demo.scss @@ -1,5 +1,6 @@ .demo-nav-bar { border: 1px solid #e0e0e0; + margin-bottom: 40px; [md-tab-nav-bar] { background: #f9f9f9; } @@ -13,10 +14,28 @@ .demo-tab-group { border: 1px solid #e0e0e0; + margin-bottom: 40px; .md-tab-header { background: #f9f9f9; } .md-tab-body-content { padding: 12px; } +} + +tabs-demo md-card { + width: 160px; + + md-checkbox { + display: block; + margin-top: 8px; + } + + md-input { + width: 100px; + } + + button { + width: 100%; + } } \ 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 488f28082077..aa3d9ce124a4 100644 --- a/src/demo-app/tabs/tabs-demo.ts +++ b/src/demo-app/tabs/tabs-demo.ts @@ -10,6 +10,7 @@ import {Observable} from 'rxjs/Observable'; encapsulation: ViewEncapsulation.None, }) export class TabsDemo { + // Nav bar demo tabLinks = [ {label: 'Sun', link: 'sunny-tab'}, {label: 'Rain', link: 'rainy-tab'}, @@ -17,20 +18,44 @@ export class TabsDemo { ]; activeLinkIndex = 0; + // Standard tabs demo tabs = [ { - label: 'Tab One', - content: 'This is the body of the first tab'}, - { - label: 'Tab Two', + label: 'Tab 1', + content: 'This is the body of the first tab' + }, { + label: 'Tab 2', disabled: true, - content: 'This is the body of the second tab'}, - { - label: 'Tab Three', + content: 'This is the body of the second tab' + }, { + label: 'Tab 3', extraContent: true, - content: 'This is the body of the third tab'}, + content: 'This is the body of the third tab' + }, { + label: 'Tab 4', + content: 'This is the body of the fourth tab' + }, + ]; + + // Dynamic tabs demo + activeTabIndex = 0; + addTabPosition = 0; + gotoNewTabAfterAdding = false; + createWithLongContent = false; + dynamicTabs = [ { - label: 'Tab Four', + label: 'Tab 1', + content: 'This is the body of the first tab' + }, { + label: 'Tab 2', + disabled: true, + content: 'This is the body of the second tab' + }, { + label: 'Tab 3', + extraContent: true, + content: 'This is the body of the third tab' + }, { + label: 'Tab 4', content: 'This is the body of the fourth tab' }, ]; @@ -50,6 +75,22 @@ export class TabsDemo { this.activeLinkIndex = this.tabLinks.indexOf(this.tabLinks.find(tab => router.url.indexOf(tab.link) != -1)); } + + addTab(includeExtraContent: boolean): void { + this.dynamicTabs.splice(this.addTabPosition, 0, { + label: 'New Tab ' + (this.dynamicTabs.length + 1), + content: 'New tab contents ' + (this.dynamicTabs.length + 1), + extraContent: includeExtraContent + }); + + if (this.gotoNewTabAfterAdding) { + this.activeTabIndex = this.addTabPosition; + } + } + + deleteTab(tab: any) { + this.dynamicTabs.splice(this.dynamicTabs.indexOf(tab), 1); + } } diff --git a/src/lib/tabs/index.ts b/src/lib/tabs/index.ts index c2d1b4e91b22..f40a858afd57 100644 --- a/src/lib/tabs/index.ts +++ b/src/lib/tabs/index.ts @@ -1 +1 @@ -export * from './tabs'; +export * from './tab-group'; diff --git a/src/lib/tabs/ink-bar.ts b/src/lib/tabs/ink-bar.ts index eeac3943f134..695116dbd0f5 100644 --- a/src/lib/tabs/ink-bar.ts +++ b/src/lib/tabs/ink-bar.ts @@ -10,15 +10,27 @@ export class MdInkBar { /** * Calculates the styles from the provided element in order to align the ink-bar to that element. + * Shows the ink bar if previously set as hidden. * @param element */ alignToElement(element: HTMLElement) { + this.show(); this._renderer.setElementStyle(this._elementRef.nativeElement, 'left', this._getLeftPosition(element)); this._renderer.setElementStyle(this._elementRef.nativeElement, 'width', this._getElementWidth(element)); } + /** Shows the ink bar. */ + show(): void { + this._renderer.setElementStyle(this._elementRef.nativeElement, 'visibility', 'visible'); + } + + /** Hides the ink bar. */ + hide(): void { + this._renderer.setElementStyle(this._elementRef.nativeElement, 'visibility', 'hidden'); + } + /** * Generates the pixel distance from the left based on the provided element in string format. * @param element diff --git a/src/lib/tabs/tab-body.html b/src/lib/tabs/tab-body.html index 2d89174484c7..f5da5de92cd9 100644 --- a/src/lib/tabs/tab-body.html +++ b/src/lib/tabs/tab-body.html @@ -1,4 +1,4 @@ -
diff --git a/src/lib/tabs/tab-body.spec.ts b/src/lib/tabs/tab-body.spec.ts new file mode 100644 index 000000000000..ed8a00e8ec55 --- /dev/null +++ b/src/lib/tabs/tab-body.spec.ts @@ -0,0 +1,193 @@ +import {async, ComponentFixture, TestBed, flushMicrotasks, fakeAsync} from '@angular/core/testing'; +import {Component, ViewChild, TemplateRef, ViewContainerRef} from '@angular/core'; +import {LayoutDirection, Dir} from '../core/rtl/dir'; +import {TemplatePortal} from '../core/portal/portal'; +import {MdTabBody} from './tab-body'; +import {MdRippleModule} from '../core/ripple/ripple'; +import {CommonModule} from '@angular/common'; +import {PortalModule} from '../core'; + + +describe('MdTabBody', () => { + let dir: LayoutDirection = 'ltr'; + + beforeEach(async(() => { + dir = 'ltr'; + TestBed.configureTestingModule({ + imports: [CommonModule, PortalModule, MdRippleModule], + declarations: [ + MdTabBody, + SimpleTabBodyApp, + ], + providers: [ + { provide: Dir, useFactory: () => { return {value: dir}; } + }] + }); + + TestBed.compileComponents(); + })); + + describe('when initialized as center', () => { + let fixture: ComponentFixture; + + describe('in LTR direction', () => { + beforeEach(() => { + dir = 'ltr'; + fixture = TestBed.createComponent(SimpleTabBodyApp); + }); + + it('should be center position without origin', () => { + fixture.componentInstance.position = 0; + fixture.detectChanges(); + + expect(fixture.componentInstance.mdTabBody._position).toBe('center'); + }); + + it('should be left-origin-center position with negative or zero origin', () => { + fixture.componentInstance.position = 0; + fixture.componentInstance.origin = 0; + fixture.detectChanges(); + + expect(fixture.componentInstance.mdTabBody._position).toBe('left-origin-center'); + }); + + it('should be right-origin-center position with positive nonzero origin', () => { + fixture.componentInstance.position = 0; + fixture.componentInstance.origin = 1; + fixture.detectChanges(); + + expect(fixture.componentInstance.mdTabBody._position).toBe('right-origin-center'); + }); + }); + + describe('in RTL direction', () => { + beforeEach(() => { + dir = 'rtl'; + fixture = TestBed.createComponent(SimpleTabBodyApp); + }); + + it('should be right-origin-center position with negative or zero origin', () => { + fixture.componentInstance.position = 0; + fixture.componentInstance.origin = 0; + fixture.detectChanges(); + + expect(fixture.componentInstance.mdTabBody._position).toBe('right-origin-center'); + }); + + it('should be left-origin-center position with positive nonzero origin', () => { + fixture.componentInstance.position = 0; + fixture.componentInstance.origin = 1; + fixture.detectChanges(); + + expect(fixture.componentInstance.mdTabBody._position).toBe('left-origin-center'); + }); + }); + }); + + describe('should properly set the position in LTR', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + dir = 'ltr'; + fixture = TestBed.createComponent(SimpleTabBodyApp); + fixture.detectChanges(); + }); + + it('to be left position with negative position', () => { + fixture.componentInstance.position = -1; + fixture.detectChanges(); + + expect(fixture.componentInstance.mdTabBody._position).toBe('left'); + }); + + it('to be center position with zero position', () => { + fixture.componentInstance.position = 0; + fixture.detectChanges(); + + expect(fixture.componentInstance.mdTabBody._position).toBe('center'); + }); + + it('to be left position with positive position', () => { + fixture.componentInstance.position = 1; + fixture.detectChanges(); + + expect(fixture.componentInstance.mdTabBody._position).toBe('right'); + }); + }); + + describe('should properly set the position in RTL', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + dir = 'rtl'; + fixture = TestBed.createComponent(SimpleTabBodyApp); + fixture.detectChanges(); + }); + + it('to be right position with negative position', () => { + fixture.componentInstance.position = -1; + fixture.detectChanges(); + + expect(fixture.componentInstance.mdTabBody._position).toBe('right'); + }); + + it('to be center position with zero position', () => { + fixture.componentInstance.position = 0; + fixture.detectChanges(); + + expect(fixture.componentInstance.mdTabBody._position).toBe('center'); + }); + + it('to be left position with positive position', () => { + fixture.componentInstance.position = 1; + fixture.detectChanges(); + + expect(fixture.componentInstance.mdTabBody._position).toBe('left'); + }); + }); + + describe('on centered', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + fixture = TestBed.createComponent(SimpleTabBodyApp); + }); + + it('should attach the content when centered and detach when not', fakeAsync(() => { + fixture.componentInstance.position = 1; + fixture.detectChanges(); + expect(fixture.componentInstance.mdTabBody._portalHost.hasAttached()).toBe(false); + + fixture.componentInstance.position = 0; + fixture.detectChanges(); + expect(fixture.componentInstance.mdTabBody._portalHost.hasAttached()).toBe(true); + + fixture.componentInstance.position = 1; + fixture.detectChanges(); + flushMicrotasks(); // Finish animation and let it detach in animation done handler + expect(fixture.componentInstance.mdTabBody._portalHost.hasAttached()).toBe(false); + })); + }); +}); + + +@Component({ + template: ` + + + ` +}) +class SimpleTabBodyApp { + content: TemplatePortal; + position: number; + origin: number; + + @ViewChild(MdTabBody) mdTabBody: MdTabBody; + @ViewChild(TemplateRef) template: TemplateRef; + + constructor(private _viewContainerRef: ViewContainerRef) { } + + ngAfterContentInit() { + this.content = new TemplatePortal(this.template, this._viewContainerRef); + } +} diff --git a/src/lib/tabs/tab-body.ts b/src/lib/tabs/tab-body.ts new file mode 100644 index 000000000000..e5960f31c737 --- /dev/null +++ b/src/lib/tabs/tab-body.ts @@ -0,0 +1,157 @@ +import { + ViewChild, + Component, + Input, + Output, + EventEmitter, + OnInit, + trigger, + state, + style, + animate, + transition, + AnimationTransitionEvent, + ElementRef, + Optional +} from '@angular/core'; +import {TemplatePortal, PortalHostDirective, Dir, LayoutDirection} from '../core'; +import 'rxjs/add/operator/map'; + +/** + * These position states are used internally as animation states for the tab body. Setting the + * position state to left, right, or center will transition the tab body from its current + * position to its respective state. If there is not current position (void, in the case of a new + * tab body), then there will be no transition animation to its state. + * + * In the case of a new tab body that should immediately be centered with an animating transition, + * then left-origin-center or right-origin-center can be used, which will use left or right as its + * psuedo-prior state. + */ +export type MdTabBodyPositionState = + 'left' | 'center' | 'right' | 'left-origin-center' | 'right-origin-center'; + +/** + * The origin state is an internally used state that is set on a new tab body indicating if it + * began to the left or right of the prior selected index. For example, if the selected index was + * set to 1, and a new tab is created and selected at index 2, then the tab body would have an + * origin of right because its index was greater than the prior selected index. + */ +export type MdTabBodyOriginState = 'left' | '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('left-origin-center', style({transform: 'translate3d(0, 0, 0)'})), + state('right-origin-center', style({transform: 'translate3d(0, 0, 0)'})), + state('center', style({transform: 'translate3d(0, 0, 0)'})), + state('right', style({transform: 'translate3d(100%, 0, 0)'})), + transition('* => left, * => right, left => center, right => center', + animate('500ms cubic-bezier(0.35, 0, 0.25, 1)')), + transition('void => left-origin-center', [ + style({transform: 'translate3d(-100%, 0, 0)'}), + animate('500ms cubic-bezier(0.35, 0, 0.25, 1)') + ]), + transition('void => right-origin-center', [ + style({transform: 'translate3d(100%, 0, 0)'}), + 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() + onCentering: EventEmitter = new EventEmitter(); + + /** Event emitted when the tab completes its animation towards the center. */ + @Output() + onCentered: EventEmitter = new EventEmitter(true); + + /** The tab body content to display. */ + @Input('content') _content: TemplatePortal; + + /** The shifted index position of the tab body, where zero represents the active center tab. */ + _position: MdTabBodyPositionState; + @Input('position') set position(position: number) { + if (position < 0) { + this._position = this._getLayoutDirection() == 'ltr' ? 'left' : 'right'; + } else if (position > 0) { + this._position = this._getLayoutDirection() == 'ltr' ? 'right' : 'left'; + } else { + this._position = 'center'; + } + } + + /** The origin position from which this tab should appear when it is centered into view. */ + _origin: MdTabBodyOriginState; + @Input('origin') set origin(origin: number) { + if (origin == null) { return; } + + const dir = this._getLayoutDirection(); + if ((dir == 'ltr' && origin <= 0) || (dir == 'rtl' && origin > 0)) { + this._origin = 'left'; + } else { + this._origin = 'right'; + } + } + + constructor(private _elementRef: ElementRef, @Optional() private _dir: Dir) {} + + /** + * After initialized, check if the content is centered and has an origin. If so, set the + * special position states that transition the tab from the left or right before centering. + */ + ngOnInit() { + if (this._position == 'center' && this._origin) { + this._position = this._origin == 'left' ? 'left-origin-center' : 'right-origin-center'; + } + } + + /** + * After the view has been set, check if the tab content is set to the center and attach the + * content if it is not already attached. + */ + ngAfterViewChecked() { + if (this._isCenterPosition(this._position) && !this._portalHost.hasAttached()) { + this._portalHost.attach(this._content); + } + } + + _onTranslateTabStarted(e: AnimationTransitionEvent) { + if (this._isCenterPosition(e.toState)) { + this.onCentering.emit(this._elementRef.nativeElement.clientHeight); + } + } + + _onTranslateTabComplete(e: AnimationTransitionEvent) { + // If the end state is that the tab is not centered, then detach the content. + if (!this._isCenterPosition(e.toState) && !this._isCenterPosition(this._position)) { + this._portalHost.detach(); + } + + // If the transition to the center is complete, emit an event. + if (this._isCenterPosition(e.toState) && this._isCenterPosition(this._position)) { + this.onCentered.emit(); + } + } + + /** The text direction of the containing app. */ + _getLayoutDirection(): LayoutDirection { + return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr'; + } + + + /** Whether the provided position state is considered center, regardless of origin. */ + private _isCenterPosition(position: MdTabBodyPositionState|string): boolean { + return position == 'center' || + position == 'left-origin-center' || + position == 'right-origin-center'; + } +} diff --git a/src/lib/tabs/tab-group.html b/src/lib/tabs/tab-group.html index 3a86a570b732..8094d740d26b 100644 --- a/src/lib/tabs/tab-group.html +++ b/src/lib/tabs/tab-group.html @@ -26,9 +26,10 @@ [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)"> + [content]="tab.content" + [position]="tab.position" + [origin]="tab.origin" + (onCentered)="_removeTabBodyWrapperHeight()" + (onCentering)="_setTabBodyWrapperHeight($event)">
diff --git a/src/lib/tabs/tab-group.spec.ts b/src/lib/tabs/tab-group.spec.ts index f4e8adfeca9f..a434b3e4e725 100644 --- a/src/lib/tabs/tab-group.spec.ts +++ b/src/lib/tabs/tab-group.spec.ts @@ -1,30 +1,24 @@ import { - async, fakeAsync, tick, ComponentFixture, TestBed, - flushMicrotasks + async, fakeAsync, tick, ComponentFixture, TestBed } from '@angular/core/testing'; -import {MdTabGroup, MdTabsModule} from './tabs'; +import {MdTabGroup, MdTabsModule} from './tab-group'; import {Component, ViewChild} from '@angular/core'; import {By} from '@angular/platform-browser'; import {Observable} from 'rxjs/Observable'; -import {LayoutDirection, Dir} from '../core/rtl/dir'; +import {MdTab} from './tab'; describe('MdTabGroup', () => { - let dir: LayoutDirection = 'ltr'; - beforeEach(async(() => { TestBed.configureTestingModule({ imports: [MdTabsModule.forRoot()], declarations: [ SimpleTabsTestApp, + SimpleDynamicTabsTestApp, + BindedTabsTestApp, AsyncTabsTestApp, DisabledTabsTestApp, TabGroupWithSimpleApi, - ], - providers: [ - {provide: Dir, useFactory: () => { - return {value: dir}; - }} ] }); @@ -147,86 +141,100 @@ describe('MdTabGroup', () => { expect(component.selectEvent.index).toBe(2); })); - it('should update tab positions and attach content when selected', fakeAsync(() => { + it('should update tab positions when selected index is changed', () => { 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 + const component: MdTabGroup = + fixture.debugElement.query(By.css('md-tab-group')).componentInstance; + const tabs: MdTab[] = component._tabs.toArray(); - expect(tabBodyList[0].componentInstance._position).toBe('left'); - expect(tabBodyList[0].componentInstance._content.isAttached).toBe(false); + expect(tabs[0].position).toBeLessThan(0); + expect(tabs[1].position).toBe(0); + expect(tabs[2].position).toBeGreaterThan(0); - expect(tabBodyList[1].componentInstance._position).toBe('center'); - expect(tabBodyList[1].componentInstance._content.isAttached).toBe(true); + // Move to third tab + component.selectedIndex = 2; + fixture.detectChanges(); + expect(tabs[0].position).toBeLessThan(0); + expect(tabs[1].position).toBeLessThan(0); + expect(tabs[2].position).toBe(0); - expect(tabBodyList[2].componentInstance._position).toBe('right'); - expect(tabBodyList[2].componentInstance._content.isAttached).toBe(false); + // Move to the first tab + component.selectedIndex = 0; + fixture.detectChanges(); + expect(tabs[0].position).toBe(0); + expect(tabs[1].position).toBeGreaterThan(0); + expect(tabs[2].position).toBeGreaterThan(0); + }); - // Move to third tab - tabComponent.selectedIndex = 2; + it('should clamp the selected index to the size of the number of tabs', () => { fixture.detectChanges(); - flushMicrotasks(); // finish animation + const component: MdTabGroup = + fixture.debugElement.query(By.css('md-tab-group')).componentInstance; - expect(tabBodyList[0].componentInstance._position).toBe('left'); - expect(tabBodyList[0].componentInstance._content.isAttached).toBe(false); + // Set the index to be negative, expect first tab selected + fixture.componentInstance.selectedIndex = -1; + fixture.detectChanges(); + expect(component.selectedIndex).toBe(0); - expect(tabBodyList[1].componentInstance._position).toBe('left'); - expect(tabBodyList[1].componentInstance._content.isAttached).toBe(false); + // Set the index beyond the size of the tabs, expect last tab selected + fixture.componentInstance.selectedIndex = 3; + fixture.detectChanges(); + expect(component.selectedIndex).toBe(2); + }); + }); - expect(tabBodyList[2].componentInstance._position).toBe('center'); - expect(tabBodyList[2].componentInstance._content.isAttached).toBe(true); + describe('dynamic binding tabs', () => { + let fixture: ComponentFixture; - // Move to the first tab - tabComponent.selectedIndex = 0; + beforeEach(async(() => { + fixture = TestBed.createComponent(SimpleDynamicTabsTestApp); 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); + it('should be able to add a new tab, select it, and have correct origin position', () => { + fixture.detectChanges(); + const component: MdTabGroup = + fixture.debugElement.query(By.css('md-tab-group')).componentInstance; - expect(tabBodyList[1].componentInstance._position).toBe('right'); - expect(tabBodyList[1].componentInstance._content.isAttached).toBe(false); + let tabs: MdTab[] = component._tabs.toArray(); + expect(tabs[0].origin).toBe(null); + expect(tabs[1].origin).toBe(0); + expect(tabs[2].origin).toBe(null); - expect(tabBodyList[2].componentInstance._position).toBe('right'); - expect(tabBodyList[2].componentInstance._content.isAttached).toBe(false); - })); + // Add a new tab on the right and select it, expect an origin >= than 0 (animate right) + fixture.componentInstance.tabs.push({label: 'New tab', content: 'to right of index'}); + fixture.componentInstance.selectedIndex = 4; + fixture.detectChanges(); + tabs = component._tabs.toArray(); + expect(tabs[3].origin).toBeGreaterThanOrEqual(0); - it('should support RTL for the tab positions', fakeAsync(() => { - dir = 'rtl'; + // Add a new tab in the beginning and select it, expect an origin < than 0 (animate left) + fixture.componentInstance.tabs.push({label: 'New tab', content: 'to left of index'}); + fixture.componentInstance.selectedIndex = 0; 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 + tabs = component._tabs.toArray(); + expect(tabs[0].origin).toBeLessThan(0); + }); - 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; + it('should update selected index if the last tab removed while selected', () => { fixture.detectChanges(); - flushMicrotasks(); // finish animation + const component: MdTabGroup = + fixture.debugElement.query(By.css('md-tab-group')).componentInstance; - expect(tabBodyList[0].componentInstance._position).toBe('right'); - expect(tabBodyList[1].componentInstance._position).toBe('right'); - expect(tabBodyList[2].componentInstance._position).toBe('center'); + const numberOfTabs = component._tabs.length; + fixture.componentInstance.selectedIndex = numberOfTabs - 1; + fixture.detectChanges(); - // Move to the first tab - tabComponent.selectedIndex = 0; + // Remove last tab while last tab is selected, expect next tab over to be selected + fixture.componentInstance.tabs.pop(); 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'); - })); + expect(component.selectedIndex).toBe(numberOfTabs - 2); + }); + }); describe('disabled tabs', () => { @@ -430,6 +438,61 @@ class SimpleTabsTestApp { } } +@Component({ + template: ` + + + + {{tab.content}} + + + ` +}) +class SimpleDynamicTabsTestApp { + tabs = [ + {label: 'Label 1', content: 'Content 1'}, + {label: 'Label 2', content: 'Content 2'}, + {label: 'Label 3', content: 'Content 3'}, + ]; + selectedIndex: number = 1; + focusEvent: any; + selectEvent: any; + handleFocus(event: any) { + this.focusEvent = event; + } + handleSelection(event: any) { + this.selectEvent = event; + } +} + +@Component({ + template: ` + + + {{tab.content}} + + + ` +}) +class BindedTabsTestApp { + tabs = [ + { label: 'one', content: 'one' }, + { label: 'two', content: 'two' } + ]; + selectedIndex = 0; + + addNewActiveTab(): void { + this.tabs.push({ + label: 'new tab', + content: 'new content' + }); + this.selectedIndex = this.tabs.length - 1; + } +} + @Component({ selector: 'test-app', template: ` diff --git a/src/lib/tabs/tabs.ts b/src/lib/tabs/tab-group.ts similarity index 58% rename from src/lib/tabs/tabs.ts rename to src/lib/tabs/tab-group.ts index 628141b44ad5..2b9241045ee8 100644 --- a/src/lib/tabs/tabs.ts +++ b/src/lib/tabs/tab-group.ts @@ -1,40 +1,25 @@ import { - NgModule, - ModuleWithProviders, - ContentChild, - ViewChild, - Component, - Input, - Output, - ViewChildren, - NgZone, - EventEmitter, - QueryList, - ContentChildren, - TemplateRef, - ViewContainerRef, - OnInit, - trigger, - state, - style, - animate, - transition, - AnimationTransitionEvent, - ElementRef, - Renderer, - Optional, + NgModule, + ModuleWithProviders, + ViewChild, + Component, + Input, + Output, + ViewChildren, + NgZone, + EventEmitter, + QueryList, + ContentChildren, + ElementRef, + Renderer } from '@angular/core'; import {CommonModule} from '@angular/common'; import { - PortalModule, - TemplatePortal, - RIGHT_ARROW, - LEFT_ARROW, - ENTER, - coerceBooleanProperty, - PortalHostDirective, - Dir, - LayoutDirection + PortalModule, + RIGHT_ARROW, + LEFT_ARROW, + ENTER, + coerceBooleanProperty } from '../core'; import {MdTabLabel} from './tab-label'; import {MdTabLabelWrapper} from './tab-label-wrapper'; @@ -43,48 +28,19 @@ import {MdInkBar} from './ink-bar'; import {Observable} from 'rxjs/Observable'; import 'rxjs/add/operator/map'; import {MdRippleModule} from '../core/ripple/ripple'; +import {MdTab} from './tab'; +import {MdTabBody} from './tab-body'; /** Used to generate unique ID's for each tab component */ let nextId = 0; + /** A simple change event emitted on focus or selection changes. */ export class MdTabChangeEvent { index: number; tab: MdTab; } -@Component({ - moduleId: module.id, - selector: 'md-tab', - templateUrl: 'tab.html', -}) -export class MdTab implements OnInit { - /** Content for the tab label given by