diff --git a/src/components/tabs/README.md b/src/components/tabs/README.md
index cf9865dfe76c..b5b5ca8d0136 100644
--- a/src/components/tabs/README.md
+++ b/src/components/tabs/README.md
@@ -28,3 +28,11 @@ A basic tab group would have the following markup.
| Name | Type | Description |
| --- | --- | --- |
| `selectedIndex` | `number` | The index of the currently active tab. |
+| `focusIndex` | `number` | The index of the currently active tab. |
+
+### Events
+
+| Name | Type | Description |
+| --- | --- | --- |
+| `focusChange` | `Event` | Fired when focus changes from one label to another |
+| `selectedChange` | `Event` | Fired when the selected tab changes |
diff --git a/src/components/tabs/tab-group.html b/src/components/tabs/tab-group.html
index d9acaef078e8..fdc851e23adb 100644
--- a/src/components/tabs/tab-group.html
+++ b/src/components/tabs/tab-group.html
@@ -3,23 +3,23 @@
(keydown.arrowLeft)="focusPreviousTab()"
(keydown.enter)="selectedIndex = focusIndex">
-
+
diff --git a/src/components/tabs/tab-group.spec.ts b/src/components/tabs/tab-group.spec.ts
index d36bd81833a3..d71d59da47b6 100644
--- a/src/components/tabs/tab-group.spec.ts
+++ b/src/components/tabs/tab-group.spec.ts
@@ -4,7 +4,9 @@ import {
beforeEach,
inject,
describe,
- async
+ async,
+ fakeAsync,
+ tick
} from '@angular/core/testing';
import {TestComponentBuilder, ComponentFixture} from '@angular/compiler/testing';
import {MD_TABS_DIRECTIVES, MdTabGroup} from './tabs';
@@ -47,44 +49,80 @@ describe('MdTabGroup', () => {
checkSelectedIndex(2);
});
- it('should cycle through tab focus with focusNextTab/focusPreviousTab functions', () => {
+ it('should cycle through tab focus with focusNextTab/focusPreviousTab functions',
+ fakeAsync(() => {
+ let testComponent = fixture.componentInstance;
let tabComponent = fixture.debugElement.query(By.css('md-tab-group')).componentInstance;
+
+ spyOn(testComponent, 'handleFocus').and.callThrough();
+ fixture.detectChanges();
+
tabComponent.focusIndex = 0;
fixture.detectChanges();
+ tick();
expect(tabComponent.focusIndex).toBe(0);
+ expect(testComponent.handleFocus).toHaveBeenCalledTimes(1);
+ expect(testComponent.focusEvent.index).toBe(0);
tabComponent.focusNextTab();
fixture.detectChanges();
+ tick();
expect(tabComponent.focusIndex).toBe(1);
+ expect(testComponent.handleFocus).toHaveBeenCalledTimes(2);
+ expect(testComponent.focusEvent.index).toBe(1);
tabComponent.focusNextTab();
fixture.detectChanges();
+ tick();
expect(tabComponent.focusIndex).toBe(2);
+ expect(testComponent.handleFocus).toHaveBeenCalledTimes(3);
+ expect(testComponent.focusEvent.index).toBe(2);
tabComponent.focusNextTab();
fixture.detectChanges();
+ tick();
expect(tabComponent.focusIndex).toBe(2); // should stop at 2
+ expect(testComponent.handleFocus).toHaveBeenCalledTimes(3);
+ expect(testComponent.focusEvent.index).toBe(2);
tabComponent.focusPreviousTab();
fixture.detectChanges();
+ tick();
expect(tabComponent.focusIndex).toBe(1);
+ expect(testComponent.handleFocus).toHaveBeenCalledTimes(4);
+ expect(testComponent.focusEvent.index).toBe(1);
tabComponent.focusPreviousTab();
fixture.detectChanges();
+ tick();
expect(tabComponent.focusIndex).toBe(0);
+ expect(testComponent.handleFocus).toHaveBeenCalledTimes(5);
+ expect(testComponent.focusEvent.index).toBe(0);
tabComponent.focusPreviousTab();
fixture.detectChanges();
+ tick();
expect(tabComponent.focusIndex).toBe(0); // should stop at 0
- });
+ expect(testComponent.handleFocus).toHaveBeenCalledTimes(5);
+ expect(testComponent.focusEvent.index).toBe(0);
+ }));
+
+ it('should change tabs based on selectedIndex', fakeAsync(() => {
+ let component = fixture.componentInstance;
+ let tabComponent = fixture.debugElement.query(By.css('md-tab-group')).componentInstance;
+
+ spyOn(component, 'handleSelection').and.callThrough();
- it('should change tabs based on selectedIndex', () => {
- let component = fixture.debugElement.componentInstance;
checkSelectedIndex(1);
- component.selectedIndex = 2;
+ tabComponent.selectedIndex = 2;
+
checkSelectedIndex(2);
- });
+ tick();
+
+ expect(component.handleSelection).toHaveBeenCalledTimes(1);
+ expect(component.selectEvent.index).toBe(2);
+ }));
});
describe('async tabs', () => {
@@ -131,7 +169,10 @@ describe('MdTabGroup', () => {
@Component({
selector: 'test-app',
template: `
-
+
Tab One
Tab one content
@@ -150,6 +191,14 @@ describe('MdTabGroup', () => {
})
class SimpleTabsTestApp {
selectedIndex: number = 1;
+ focusEvent: any;
+ selectEvent: any;
+ handleFocus(event: any) {
+ this.focusEvent = event;
+ }
+ handleSelection(event: any) {
+ this.selectEvent = event;
+ }
}
@Component({
diff --git a/src/components/tabs/tabs.ts b/src/components/tabs/tabs.ts
index e17adbf58af1..fba13f3911a5 100644
--- a/src/components/tabs/tabs.ts
+++ b/src/components/tabs/tabs.ts
@@ -1,4 +1,13 @@
-import {Component, Input, ViewChildren, NgZone} from '@angular/core';
+import {
+ ContentChild,
+ Directive,
+ Component,
+ Input,
+ Output,
+ ViewChildren,
+ NgZone,
+ EventEmitter
+} from '@angular/core';
import {QueryList} from '@angular/core';
import {ContentChildren} from '@angular/core';
import {PortalHostDirective} from '@angular2-material/core/portal/portal-directives';
@@ -6,10 +15,25 @@ import {MdTabLabel} from './tab-label';
import {MdTabContent} from './tab-content';
import {MdTabLabelWrapper} from './tab-label-wrapper';
import {MdInkBar} from './ink-bar';
+import {Observable} from 'rxjs/Observable';
/** 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;
+}
+
+@Directive({
+ selector: 'md-tab'
+})
+export class MdTab {
+ @ContentChild(MdTabLabel) label: MdTabLabel;
+ @ContentChild(MdTabContent) content: MdTabContent;
+}
+
/**
* Material design tab-group component. Supports basic tab pairs (label + content) and includes
* animated ink-bar, keyboard navigation, and screen reader.
@@ -24,15 +48,35 @@ let nextId = 0;
})
export class MdTabGroup {
/** @internal */
- @ContentChildren(MdTabLabel) labels: QueryList;
-
- /** @internal */
- @ContentChildren(MdTabContent) contents: QueryList;
+ @ContentChildren(MdTab) tabs: QueryList;
@ViewChildren(MdTabLabelWrapper) private _labelWrappers: QueryList;
@ViewChildren(MdInkBar) private _inkBar: QueryList;
- @Input() selectedIndex: number = 0;
+ private _isInitialized: boolean = false;
+
+ private _selectedIndex: number = 0;
+ @Input()
+ set selectedIndex(value: number) {
+ this._selectedIndex = value;
+
+ if (this._isInitialized) {
+ this._onSelectChange.emit(this._createChangeEvent(value));
+ }
+ }
+ get selectedIndex(): number {
+ return this._selectedIndex;
+ }
+
+ private _onFocusChange: EventEmitter = new EventEmitter();
+ @Output('focusChange') get focusChange(): Observable {
+ return this._onFocusChange.asObservable();
+ }
+
+ private _onSelectChange: EventEmitter = new EventEmitter();
+ @Output('selectChange') get selectChange(): Observable {
+ return this._onSelectChange.asObservable();
+ }
private _focusIndex: number = 0;
private _groupId: number;
@@ -52,6 +96,7 @@ export class MdTabGroup {
this._updateInkBar();
});
});
+ this._isInitialized = true;
}
/** Tells the ink-bar to align itself to the current label wrapper */
@@ -77,11 +122,25 @@ export class MdTabGroup {
/** When the focus index is set, we must manually send focus to the correct label */
set focusIndex(value: number) {
this._focusIndex = value;
+
+ if (this._isInitialized) {
+ this._onFocusChange.emit(this._createChangeEvent(value));
+ }
+
if (this._labelWrappers && this._labelWrappers.length) {
this._labelWrappers.toArray()[value].focus();
}
}
+ 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
* @internal
@@ -113,4 +172,4 @@ export class MdTabGroup {
}
}
-export const MD_TABS_DIRECTIVES = [MdTabGroup, MdTabLabel, MdTabContent];
+export const MD_TABS_DIRECTIVES = [MdTabGroup, MdTabLabel, MdTabContent, MdTab];