+
+
+ Add New Tab
+
+
+ Include extra content
+
+
+ Select after adding
+
+
+ Position:
+
+
+
+
+
+
+
+ Selected tab index:
+
+
+
+
+ {{tab.label}}
+ {{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: `
+ Tab Body Content
+
+ `
+})
+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.label}}
+ {{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 . */
- @ContentChild(MdTabLabel) templateLabel: MdTabLabel;
-
- /** Template inside the MdTab view that contains an . */
- @ViewChild(TemplateRef) _content: TemplateRef;
-
- /** The plain text label for the tab, used when there is no template label. */
- @Input('label') textLabel: string = '';
-
- private _contentPortal: TemplatePortal = null;
-
- constructor(private _viewContainerRef: ViewContainerRef) { }
-
- ngOnInit() {
- this._contentPortal = new TemplatePortal(this._content, this._viewContainerRef);
- }
-
- private _disabled = false;
- @Input() set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value); }
- get disabled(): boolean { return this._disabled; }
-
- get content(): TemplatePortal {
- return this._contentPortal;
- }
-}
-
/**
* Material design tab-group component. Supports basic tab pairs (label + content) and includes
* animated ink-bar, keyboard navigation, and screen reader.
@@ -94,20 +50,23 @@ 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;
@ViewChildren(MdTabLabelWrapper) _labelWrappers: QueryList;
- @ViewChildren(MdInkBar) _inkBar: QueryList;
-
+ @ViewChild(MdInkBar) _inkBar: MdInkBar;
@ViewChild('tabBodyWrapper') _tabBodyWrapper: ElementRef;
+ /** Whether this component has been initialized. */
private _isInitialized: boolean = false;
+ /** The tab index that should be selected after the content has been checked. */
+ private _indexToSelect = 0;
+
/** Snapshot of the height of the tab body wrapper before another tab is activated. */
- private _tabBodyWrapperHeight: number = 0;
+ private _tabBodyWrapperHeight: number = null;
/** Whether the tab group should grow to the size of the active tab */
private _dynamicHeight: boolean = false;
@@ -116,16 +75,9 @@ export class MdTabGroup {
}
/** The index of the active tab. */
- private _selectedIndex: number = 0;
+ private _selectedIndex: number = null;
@Input() set selectedIndex(value: number) {
- this._tabBodyWrapperHeight = this._tabBodyWrapper.nativeElement.clientHeight;
- if (value != this._selectedIndex && this.isValidIndex(value)) {
- this._selectedIndex = value;
-
- if (this._isInitialized) {
- this._onSelectChange.emit(this._createChangeEvent(value));
- }
- }
+ this._indexToSelect = value;
}
get selectedIndex(): number {
return this._selectedIndex;
@@ -141,7 +93,8 @@ export class MdTabGroup {
return this._onFocusChange.asObservable();
}
- private _onSelectChange: EventEmitter = new EventEmitter();
+ private _onSelectChange: EventEmitter =
+ new EventEmitter(true);
@Output() get selectChange(): Observable {
return this._onSelectChange.asObservable();
}
@@ -153,6 +106,37 @@ export class MdTabGroup {
this._groupId = nextId++;
}
+ /**
+ * After the content is checked, this component knows what tabs have been defined
+ * and what the selected index should be. This is where we can know exactly what position
+ * each tab should be in according to the new selected index, and additionally we know how
+ * a new selected tab should transition in (from the left or right).
+ */
+ ngAfterContentChecked(): void {
+ // Clamp the next selected index to the bounds of 0 and the tabs length.
+ this._indexToSelect =
+ Math.min(this._tabs.length - 1, Math.max(this._indexToSelect, 0));
+
+ // If there is a change in selected index, emit a change event. Should not trigger if
+ // the selected index has not yet been initialized.
+ if (this._selectedIndex != this._indexToSelect && this._selectedIndex != null) {
+ this._onSelectChange.emit(this._createChangeEvent(this._indexToSelect));
+ }
+
+ // Setup the position for each tab and optionally setup an origin on the next selected tab.
+ this._tabs.forEach((tab: MdTab, index: number) => {
+ tab.position = index - this._indexToSelect;
+
+ // If there is already a selected tab, then set up an origin for the next selected tab
+ // if it doesn't have one already.
+ if (this._selectedIndex != null && tab.position == 0 && !tab.origin) {
+ tab.origin = this._indexToSelect - this._selectedIndex;
+ }
+ });
+
+ this._selectedIndex = this._indexToSelect;
+ }
+
/**
* Waits one frame for the view to update, then updates the ink bar
* Note: This must be run outside of the zone or it will create an infinite change detection loop
@@ -182,7 +166,9 @@ export class MdTabGroup {
/** Tells the ink-bar to align itself to the current label wrapper */
private _updateInkBar(): void {
- this._inkBar.toArray()[0].alignToElement(this._currentLabelWrapper);
+ if (this._currentLabelWrapper) {
+ this._inkBar.alignToElement(this._currentLabelWrapper);
+ }
}
/**
@@ -190,7 +176,8 @@ export class MdTabGroup {
* ViewChildren references are ready.
*/
private get _currentLabelWrapper(): HTMLElement {
- return this._labelWrappers && this._labelWrappers.length
+ return this._labelWrappers && this._labelWrappers.length &&
+ this._labelWrappers.toArray()[this.selectedIndex]
? this._labelWrappers.toArray()[this.selectedIndex].elementRef.nativeElement
: null;
}
@@ -278,7 +265,7 @@ export class MdTabGroup {
* height property is true.
*/
_setTabBodyWrapperHeight(tabHeight: number): void {
- if (!this._dynamicHeight) { return; }
+ if (!this._dynamicHeight || !this._tabBodyWrapperHeight) { return; }
this._renderer.setElementStyle(this._tabBodyWrapper.nativeElement, 'height',
this._tabBodyWrapperHeight + 'px');
@@ -293,90 +280,14 @@ export class MdTabGroup {
/** Removes the height of the tab body wrapper. */
_removeTabBodyWrapperHeight(): void {
+ this._tabBodyWrapperHeight = this._tabBodyWrapper.nativeElement.clientHeight;
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, MdRippleModule],
- // Don't export MdInkBar or MdTabLabelWrapper, as they are internal implementation details.
+ // Don't export all components because some are only to be used internally.
exports: [MdTabGroup, MdTabLabel, MdTab, MdTabNavBar, MdTabLink, MdTabLinkRipple],
declarations: [MdTabGroup, MdTabLabel, MdTab, MdInkBar, MdTabLabelWrapper,
MdTabNavBar, MdTabLink, MdTabBody, MdTabLinkRipple],
diff --git a/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts b/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts
index 9cd316a89ab3..ce353cf992cc 100644
--- a/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts
+++ b/src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts
@@ -1,5 +1,5 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
-import {MdTabsModule} from '../tabs';
+import {MdTabsModule} from '../tab-group';
import {Component} from '@angular/core';
import {By} from '@angular/platform-browser';
diff --git a/src/lib/tabs/tab.ts b/src/lib/tabs/tab.ts
new file mode 100644
index 000000000000..b11bdbc3e547
--- /dev/null
+++ b/src/lib/tabs/tab.ts
@@ -0,0 +1,50 @@
+import {TemplatePortal} from '../core/portal/portal';
+import {
+ ViewContainerRef, Input, TemplateRef, ViewChild, OnInit, ContentChild,
+ Component
+} from '@angular/core';
+import {coerceBooleanProperty} from '../core/coersion/boolean-property';
+
+import {MdTabLabel} from './tab-label';
+
+@Component({
+ moduleId: module.id,
+ selector: 'md-tab',
+ templateUrl: 'tab.html',
+})
+export class MdTab implements OnInit {
+ /** Content for the tab label given by . */
+ @ContentChild(MdTabLabel) templateLabel: MdTabLabel;
+
+ /** Template inside the MdTab view that contains an . */
+ @ViewChild(TemplateRef) _content: TemplateRef;
+
+ /** The plain text label for the tab, used when there is no template label. */
+ @Input('label') textLabel: string = '';
+
+ /** The portal that will be the hosted content of the tab */
+ private _contentPortal: TemplatePortal = null;
+ get content(): TemplatePortal { return this._contentPortal; }
+
+ /**
+ * The relatively indexed position where 0 represents the center, negative is left, and positive
+ * represents the right.
+ */
+ position: number = null;
+
+ /**
+ * The initial relatively index origin of the tab if it was created and selected after there
+ * was already a selected tab. Provides context of what position the tab should originate from.
+ */
+ origin: number = null;
+
+ private _disabled = false;
+ @Input() set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value); }
+ get disabled(): boolean { return this._disabled; }
+
+ constructor(private _viewContainerRef: ViewContainerRef) { }
+
+ ngOnInit() {
+ this._contentPortal = new TemplatePortal(this._content, this._viewContainerRef);
+ }
+}