From a798855dd7826397f8ed4c60301b93cc0ed30522 Mon Sep 17 00:00:00 2001 From: Robert Messerle Date: Mon, 6 Jun 2016 14:30:26 -0700 Subject: [PATCH] feat(tabs): adds focus/select events closes #569 --- src/components/tabs/README.md | 8 +++ src/components/tabs/tab-group.html | 8 +-- src/components/tabs/tab-group.spec.ts | 65 +++++++++++++++++++++--- src/components/tabs/tabs.ts | 73 ++++++++++++++++++++++++--- 4 files changed, 135 insertions(+), 19 deletions(-) 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: ` - + @@ -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];