Skip to content

Commit

Permalink
feat(tabs): adds focus/select events (#649)
Browse files Browse the repository at this point in the history
closes #569
  • Loading branch information
robertmesserle authored and jelbourn committed Jun 15, 2016
1 parent afed818 commit 497a3c1
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 19 deletions.
8 changes: 8 additions & 0 deletions src/components/tabs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

This comment has been minimized.

Copy link
@dmacfarlane

dmacfarlane Jun 20, 2016

This event name should be 'selectChange' instead of 'selectedChange'?

This comment has been minimized.

Copy link
@dmacfarlane
8 changes: 4 additions & 4 deletions src/components/tabs/tab-group.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
65 changes: 57 additions & 8 deletions src/components/tabs/tab-group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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>
Expand All @@ -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({
Expand Down
73 changes: 66 additions & 7 deletions src/components/tabs/tabs.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
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';
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.
Expand All @@ -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;
Expand All @@ -52,6 +96,7 @@ export class MdTabGroup {
this._updateInkBar();
});
});
this._isInitialized = true;
}

/** Tells the ink-bar to align itself to the current label wrapper */
Expand All @@ -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
Expand Down Expand Up @@ -113,4 +172,4 @@ export class MdTabGroup {
}
}

export const MD_TABS_DIRECTIVES = [MdTabGroup, MdTabLabel, MdTabContent];
export const MD_TABS_DIRECTIVES = [MdTabGroup, MdTabLabel, MdTabContent, MdTab];

0 comments on commit 497a3c1

Please sign in to comment.