Skip to content

Commit 497a3c1

Browse files
robertmesserlejelbourn
authored andcommitted
feat(tabs): adds focus/select events (#649)
closes #569
1 parent afed818 commit 497a3c1

File tree

4 files changed

+135
-19
lines changed

4 files changed

+135
-19
lines changed

src/components/tabs/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,11 @@ A basic tab group would have the following markup.
2828
| Name | Type | Description |
2929
| --- | --- | --- |
3030
| `selectedIndex` | `number` | The index of the currently active tab. |
31+
| `focusIndex` | `number` | The index of the currently active tab. |
32+
33+
### Events
34+
35+
| Name | Type | Description |
36+
| --- | --- | --- |
37+
| `focusChange` | `Event` | Fired when focus changes from one label to another |
38+
| `selectedChange` | `Event` | Fired when the selected tab changes |

src/components/tabs/tab-group.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,23 @@
33
(keydown.arrowLeft)="focusPreviousTab()"
44
(keydown.enter)="selectedIndex = focusIndex">
55
<div class="md-tab-label" role="tab" md-tab-label-wrapper
6-
*ngFor="let label of labels; let i = index"
6+
*ngFor="let tab of tabs; let i = index"
77
[id]="getTabLabelId(i)"
88
[tabIndex]="selectedIndex == i ? 0 : -1"
99
[attr.aria-controls]="getTabContentId(i)"
1010
[attr.aria-selected]="selectedIndex == i"
1111
[class.md-active]="selectedIndex == i"
1212
(click)="focusIndex = selectedIndex = i">
13-
<template [portalHost]="label"></template>
13+
<template [portalHost]="tab.label"></template>
1414
</div>
1515
<md-ink-bar></md-ink-bar>
1616
</div>
1717
<div class="md-tab-body-wrapper">
1818
<div class="md-tab-body"
19-
*ngFor="let content of contents; let i = index"
19+
*ngFor="let tab of tabs; let i = index"
2020
[id]="getTabContentId(i)"
2121
[class.md-active]="selectedIndex == i"
2222
[attr.aria-labelledby]="getTabLabelId(i)">
23-
<template role="tabpanel" [portalHost]="content" *ngIf="selectedIndex == i"></template>
23+
<template role="tabpanel" [portalHost]="tab.content" *ngIf="selectedIndex == i"></template>
2424
</div>
2525
</div>

src/components/tabs/tab-group.spec.ts

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {
44
beforeEach,
55
inject,
66
describe,
7-
async
7+
async,
8+
fakeAsync,
9+
tick
810
} from '@angular/core/testing';
911
import {TestComponentBuilder, ComponentFixture} from '@angular/compiler/testing';
1012
import {MD_TABS_DIRECTIVES, MdTabGroup} from './tabs';
@@ -47,44 +49,80 @@ describe('MdTabGroup', () => {
4749
checkSelectedIndex(2);
4850
});
4951

50-
it('should cycle through tab focus with focusNextTab/focusPreviousTab functions', () => {
52+
it('should cycle through tab focus with focusNextTab/focusPreviousTab functions',
53+
fakeAsync(() => {
54+
let testComponent = fixture.componentInstance;
5155
let tabComponent = fixture.debugElement.query(By.css('md-tab-group')).componentInstance;
56+
57+
spyOn(testComponent, 'handleFocus').and.callThrough();
58+
fixture.detectChanges();
59+
5260
tabComponent.focusIndex = 0;
5361
fixture.detectChanges();
62+
tick();
5463
expect(tabComponent.focusIndex).toBe(0);
64+
expect(testComponent.handleFocus).toHaveBeenCalledTimes(1);
65+
expect(testComponent.focusEvent.index).toBe(0);
5566

5667
tabComponent.focusNextTab();
5768
fixture.detectChanges();
69+
tick();
5870
expect(tabComponent.focusIndex).toBe(1);
71+
expect(testComponent.handleFocus).toHaveBeenCalledTimes(2);
72+
expect(testComponent.focusEvent.index).toBe(1);
5973

6074
tabComponent.focusNextTab();
6175
fixture.detectChanges();
76+
tick();
6277
expect(tabComponent.focusIndex).toBe(2);
78+
expect(testComponent.handleFocus).toHaveBeenCalledTimes(3);
79+
expect(testComponent.focusEvent.index).toBe(2);
6380

6481
tabComponent.focusNextTab();
6582
fixture.detectChanges();
83+
tick();
6684
expect(tabComponent.focusIndex).toBe(2); // should stop at 2
85+
expect(testComponent.handleFocus).toHaveBeenCalledTimes(3);
86+
expect(testComponent.focusEvent.index).toBe(2);
6787

6888
tabComponent.focusPreviousTab();
6989
fixture.detectChanges();
90+
tick();
7091
expect(tabComponent.focusIndex).toBe(1);
92+
expect(testComponent.handleFocus).toHaveBeenCalledTimes(4);
93+
expect(testComponent.focusEvent.index).toBe(1);
7194

7295
tabComponent.focusPreviousTab();
7396
fixture.detectChanges();
97+
tick();
7498
expect(tabComponent.focusIndex).toBe(0);
99+
expect(testComponent.handleFocus).toHaveBeenCalledTimes(5);
100+
expect(testComponent.focusEvent.index).toBe(0);
75101

76102
tabComponent.focusPreviousTab();
77103
fixture.detectChanges();
104+
tick();
78105
expect(tabComponent.focusIndex).toBe(0); // should stop at 0
79-
});
106+
expect(testComponent.handleFocus).toHaveBeenCalledTimes(5);
107+
expect(testComponent.focusEvent.index).toBe(0);
108+
}));
109+
110+
it('should change tabs based on selectedIndex', fakeAsync(() => {
111+
let component = fixture.componentInstance;
112+
let tabComponent = fixture.debugElement.query(By.css('md-tab-group')).componentInstance;
113+
114+
spyOn(component, 'handleSelection').and.callThrough();
80115

81-
it('should change tabs based on selectedIndex', () => {
82-
let component = fixture.debugElement.componentInstance;
83116
checkSelectedIndex(1);
84117

85-
component.selectedIndex = 2;
118+
tabComponent.selectedIndex = 2;
119+
86120
checkSelectedIndex(2);
87-
});
121+
tick();
122+
123+
expect(component.handleSelection).toHaveBeenCalledTimes(1);
124+
expect(component.selectEvent.index).toBe(2);
125+
}));
88126
});
89127

90128
describe('async tabs', () => {
@@ -131,7 +169,10 @@ describe('MdTabGroup', () => {
131169
@Component({
132170
selector: 'test-app',
133171
template: `
134-
<md-tab-group class="tab-group" [selectedIndex]="selectedIndex">
172+
<md-tab-group class="tab-group"
173+
[selectedIndex]="selectedIndex"
174+
(focusChange)="handleFocus($event)"
175+
(selectChange)="handleSelection($event)">
135176
<md-tab>
136177
<template md-tab-label>Tab One</template>
137178
<template md-tab-content>Tab one content</template>
@@ -150,6 +191,14 @@ describe('MdTabGroup', () => {
150191
})
151192
class SimpleTabsTestApp {
152193
selectedIndex: number = 1;
194+
focusEvent: any;
195+
selectEvent: any;
196+
handleFocus(event: any) {
197+
this.focusEvent = event;
198+
}
199+
handleSelection(event: any) {
200+
this.selectEvent = event;
201+
}
153202
}
154203

155204
@Component({

src/components/tabs/tabs.ts

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,39 @@
1-
import {Component, Input, ViewChildren, NgZone} from '@angular/core';
1+
import {
2+
ContentChild,
3+
Directive,
4+
Component,
5+
Input,
6+
Output,
7+
ViewChildren,
8+
NgZone,
9+
EventEmitter
10+
} from '@angular/core';
211
import {QueryList} from '@angular/core';
312
import {ContentChildren} from '@angular/core';
413
import {PortalHostDirective} from '@angular2-material/core/portal/portal-directives';
514
import {MdTabLabel} from './tab-label';
615
import {MdTabContent} from './tab-content';
716
import {MdTabLabelWrapper} from './tab-label-wrapper';
817
import {MdInkBar} from './ink-bar';
18+
import {Observable} from 'rxjs/Observable';
919

1020
/** Used to generate unique ID's for each tab component */
1121
let nextId = 0;
1222

23+
/** A simple change event emitted on focus or selection changes. */
24+
export class MdTabChangeEvent {
25+
index: number;
26+
tab: MdTab;
27+
}
28+
29+
@Directive({
30+
selector: 'md-tab'
31+
})
32+
export class MdTab {
33+
@ContentChild(MdTabLabel) label: MdTabLabel;
34+
@ContentChild(MdTabContent) content: MdTabContent;
35+
}
36+
1337
/**
1438
* Material design tab-group component. Supports basic tab pairs (label + content) and includes
1539
* animated ink-bar, keyboard navigation, and screen reader.
@@ -24,15 +48,35 @@ let nextId = 0;
2448
})
2549
export class MdTabGroup {
2650
/** @internal */
27-
@ContentChildren(MdTabLabel) labels: QueryList<MdTabLabel>;
28-
29-
/** @internal */
30-
@ContentChildren(MdTabContent) contents: QueryList<MdTabContent>;
51+
@ContentChildren(MdTab) tabs: QueryList<MdTab>;
3152

3253
@ViewChildren(MdTabLabelWrapper) private _labelWrappers: QueryList<MdTabLabelWrapper>;
3354
@ViewChildren(MdInkBar) private _inkBar: QueryList<MdInkBar>;
3455

35-
@Input() selectedIndex: number = 0;
56+
private _isInitialized: boolean = false;
57+
58+
private _selectedIndex: number = 0;
59+
@Input()
60+
set selectedIndex(value: number) {
61+
this._selectedIndex = value;
62+
63+
if (this._isInitialized) {
64+
this._onSelectChange.emit(this._createChangeEvent(value));
65+
}
66+
}
67+
get selectedIndex(): number {
68+
return this._selectedIndex;
69+
}
70+
71+
private _onFocusChange: EventEmitter<MdTabChangeEvent> = new EventEmitter<MdTabChangeEvent>();
72+
@Output('focusChange') get focusChange(): Observable<MdTabChangeEvent> {
73+
return this._onFocusChange.asObservable();
74+
}
75+
76+
private _onSelectChange: EventEmitter<MdTabChangeEvent> = new EventEmitter<MdTabChangeEvent>();
77+
@Output('selectChange') get selectChange(): Observable<MdTabChangeEvent> {
78+
return this._onSelectChange.asObservable();
79+
}
3680

3781
private _focusIndex: number = 0;
3882
private _groupId: number;
@@ -52,6 +96,7 @@ export class MdTabGroup {
5296
this._updateInkBar();
5397
});
5498
});
99+
this._isInitialized = true;
55100
}
56101

57102
/** Tells the ink-bar to align itself to the current label wrapper */
@@ -77,11 +122,25 @@ export class MdTabGroup {
77122
/** When the focus index is set, we must manually send focus to the correct label */
78123
set focusIndex(value: number) {
79124
this._focusIndex = value;
125+
126+
if (this._isInitialized) {
127+
this._onFocusChange.emit(this._createChangeEvent(value));
128+
}
129+
80130
if (this._labelWrappers && this._labelWrappers.length) {
81131
this._labelWrappers.toArray()[value].focus();
82132
}
83133
}
84134

135+
private _createChangeEvent(index: number): MdTabChangeEvent {
136+
const event = new MdTabChangeEvent;
137+
event.index = index;
138+
if (this.tabs && this.tabs.length) {
139+
event.tab = this.tabs.toArray()[index];
140+
}
141+
return event;
142+
}
143+
85144
/**
86145
* Returns a unique id for each tab label element
87146
* @internal
@@ -113,4 +172,4 @@ export class MdTabGroup {
113172
}
114173
}
115174

116-
export const MD_TABS_DIRECTIVES = [MdTabGroup, MdTabLabel, MdTabContent];
175+
export const MD_TABS_DIRECTIVES = [MdTabGroup, MdTabLabel, MdTabContent, MdTab];

0 commit comments

Comments
 (0)