feat(tabs): adds focus/select events (#649)

closes #569
This commit is contained in:
Robert Messerle
2016-06-15 11:47:23 -07:00
committed by Jeremy Elbourn
parent afed81884f
commit 497a3c147f
4 changed files with 135 additions and 19 deletions
+8
View File
@@ -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 |
+4 -4
View File
@@ -3,23 +3,23 @@
(keydown.arrowLeft)="focusPreviousTab()"
(keydown.enter)="selectedIndex = focusIndex">
<div class="md-tab-label" role="tab" md-tab-label-wrapper
*ngFor="let label of labels; let i = index"
*ngFor="let tab of tabs; let i = index"
[id]="getTabLabelId(i)"
[tabIndex]="selectedIndex == i ? 0 : -1"
[attr.aria-controls]="getTabContentId(i)"
[attr.aria-selected]="selectedIndex == i"
[class.md-active]="selectedIndex == i"
(click)="focusIndex = selectedIndex = i">
<template [portalHost]="label"></template>
<template [portalHost]="tab.label"></template>
</div>
<md-ink-bar></md-ink-bar>
</div>
<div class="md-tab-body-wrapper">
<div class="md-tab-body"
*ngFor="let content of contents; let i = index"
*ngFor="let tab of tabs; let i = index"
[id]="getTabContentId(i)"
[class.md-active]="selectedIndex == i"
[attr.aria-labelledby]="getTabLabelId(i)">
<template role="tabpanel" [portalHost]="content" *ngIf="selectedIndex == i"></template>
<template role="tabpanel" [portalHost]="tab.content" *ngIf="selectedIndex == i"></template>
</div>
</div>
+57 -8
View File
@@ -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: `
<md-tab-group class="tab-group" [selectedIndex]="selectedIndex">
<md-tab-group class="tab-group"
[selectedIndex]="selectedIndex"
(focusChange)="handleFocus($event)"
(selectChange)="handleSelection($event)">
<md-tab>
<template md-tab-label>Tab One</template>
<template md-tab-content>Tab one content</template>
@@ -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({
+66 -7
View File
@@ -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<MdTabLabel>;
/** @internal */
@ContentChildren(MdTabContent) contents: QueryList<MdTabContent>;
@ContentChildren(MdTab) tabs: QueryList<MdTab>;
@ViewChildren(MdTabLabelWrapper) private _labelWrappers: QueryList<MdTabLabelWrapper>;
@ViewChildren(MdInkBar) private _inkBar: QueryList<MdInkBar>;
@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<MdTabChangeEvent> = new EventEmitter<MdTabChangeEvent>();
@Output('focusChange') get focusChange(): Observable<MdTabChangeEvent> {
return this._onFocusChange.asObservable();
}
private _onSelectChange: EventEmitter<MdTabChangeEvent> = new EventEmitter<MdTabChangeEvent>();
@Output('selectChange') get selectChange(): Observable<MdTabChangeEvent> {
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];