Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Jun 15, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 |
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',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like nothing is currently testing that the event object has the correct data.

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably include a reference to the MdTab instance itself

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];