From 6bd90e8141fe286a70f7c6d981ec98946560d703 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Thu, 7 Jun 2018 07:49:28 +0200 Subject: [PATCH] refactor(tabs): use common logic for handling keyboard focus (#9819) Switches the tab header component to use the `FocusKeyManager` to manage the focused item, rather than implementing the logic itself. --- src/lib/tabs/BUILD.bazel | 1 + src/lib/tabs/tab-header.spec.ts | 17 +++---- src/lib/tabs/tab-header.ts | 90 ++++++++++----------------------- 3 files changed, 34 insertions(+), 74 deletions(-) diff --git a/src/lib/tabs/BUILD.bazel b/src/lib/tabs/BUILD.bazel index 126494324b8..cbff2f3c796 100644 --- a/src/lib/tabs/BUILD.bazel +++ b/src/lib/tabs/BUILD.bazel @@ -15,6 +15,7 @@ ng_module( ] + glob(["**/*.html"]), deps = [ "//src/lib/core", + "//src/cdk/a11y", "//src/cdk/bidi", "//src/cdk/coercion", "//src/cdk/observers", diff --git a/src/lib/tabs/tab-header.spec.ts b/src/lib/tabs/tab-header.spec.ts index 959c6a545ea..efd3dfb19af 100644 --- a/src/lib/tabs/tab-header.spec.ts +++ b/src/lib/tabs/tab-header.spec.ts @@ -47,11 +47,14 @@ describe('MatTabHeader', () => { })); describe('focusing', () => { + let tabListContainer: HTMLElement; + beforeEach(() => { fixture = TestBed.createComponent(SimpleTabHeaderApp); fixture.detectChanges(); appComponent = fixture.componentInstance; + tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement; }); it('should initialize to the selected index', () => { @@ -83,12 +86,12 @@ describe('MatTabHeader', () => { // Move focus right, verify that the disabled tab is 1 and should be skipped expect(appComponent.disabledTabIndex).toBe(1); - appComponent.tabHeader._focusNextTab(); + dispatchKeyboardEvent(tabListContainer, 'keydown', RIGHT_ARROW); fixture.detectChanges(); expect(appComponent.tabHeader.focusIndex).toBe(2); // Move focus right to index 3 - appComponent.tabHeader._focusNextTab(); + dispatchKeyboardEvent(tabListContainer, 'keydown', RIGHT_ARROW); fixture.detectChanges(); expect(appComponent.tabHeader.focusIndex).toBe(3); }); @@ -99,13 +102,13 @@ describe('MatTabHeader', () => { expect(appComponent.tabHeader.focusIndex).toBe(3); // Move focus left to index 3 - appComponent.tabHeader._focusPreviousTab(); + dispatchKeyboardEvent(tabListContainer, 'keydown', LEFT_ARROW); fixture.detectChanges(); expect(appComponent.tabHeader.focusIndex).toBe(2); // Move focus left, verify that the disabled tab is 1 and should be skipped expect(appComponent.disabledTabIndex).toBe(1); - appComponent.tabHeader._focusPreviousTab(); + dispatchKeyboardEvent(tabListContainer, 'keydown', LEFT_ARROW); fixture.detectChanges(); expect(appComponent.tabHeader.focusIndex).toBe(0); }); @@ -115,8 +118,6 @@ describe('MatTabHeader', () => { fixture.detectChanges(); expect(appComponent.tabHeader.focusIndex).toBe(0); - let tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement; - // Move focus right to 2 dispatchKeyboardEvent(tabListContainer, 'keydown', RIGHT_ARROW); fixture.detectChanges(); @@ -147,7 +148,6 @@ describe('MatTabHeader', () => { fixture.detectChanges(); expect(appComponent.tabHeader.focusIndex).toBe(3); - const tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement; const event = dispatchKeyboardEvent(tabListContainer, 'keydown', HOME); fixture.detectChanges(); @@ -161,7 +161,6 @@ describe('MatTabHeader', () => { fixture.detectChanges(); expect(appComponent.tabHeader.focusIndex).toBe(3); - const tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement; dispatchKeyboardEvent(tabListContainer, 'keydown', HOME); fixture.detectChanges(); @@ -174,7 +173,6 @@ describe('MatTabHeader', () => { fixture.detectChanges(); expect(appComponent.tabHeader.focusIndex).toBe(0); - const tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement; const event = dispatchKeyboardEvent(tabListContainer, 'keydown', END); fixture.detectChanges(); @@ -188,7 +186,6 @@ describe('MatTabHeader', () => { fixture.detectChanges(); expect(appComponent.tabHeader.focusIndex).toBe(0); - const tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement; dispatchKeyboardEvent(tabListContainer, 'keydown', END); fixture.detectChanges(); diff --git a/src/lib/tabs/tab-header.ts b/src/lib/tabs/tab-header.ts index 517d560ae1e..6b1bf808e5e 100644 --- a/src/lib/tabs/tab-header.ts +++ b/src/lib/tabs/tab-header.ts @@ -8,7 +8,7 @@ import {Direction, Directionality} from '@angular/cdk/bidi'; import {coerceNumberProperty} from '@angular/cdk/coercion'; -import {END, ENTER, HOME, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes'; +import {END, ENTER, HOME, SPACE} from '@angular/cdk/keycodes'; import {ViewportRuler} from '@angular/cdk/scrolling'; import { AfterContentChecked, @@ -31,6 +31,7 @@ import {CanDisableRipple, mixinDisableRipple} from '@angular/material/core'; import {merge, of as observableOf, Subscription} from 'rxjs'; import {MatInkBar} from './ink-bar'; import {MatTabLabelWrapper} from './tab-label-wrapper'; +import {FocusKeyManager} from '@angular/cdk/a11y'; /** @@ -80,9 +81,6 @@ export class MatTabHeader extends _MatTabHeaderMixinBase @ViewChild('tabListContainer') _tabListContainer: ElementRef; @ViewChild('tabList') _tabList: ElementRef; - /** The tab index that is focused. */ - private _focusIndex: number = 0; - /** The distance in pixels that the tab labels should be translated to the left. */ private _scrollDistance = 0; @@ -110,6 +108,9 @@ export class MatTabHeader extends _MatTabHeaderMixinBase /** Whether the scroll distance has changed and should be applied after the view is checked. */ private _scrollDistanceChanged: boolean; + /** Used to manage focus between the tabs. */ + private _keyManager: FocusKeyManager; + private _selectedIndex: number = 0; /** The index of the active tab. */ @@ -119,7 +120,10 @@ export class MatTabHeader extends _MatTabHeaderMixinBase value = coerceNumberProperty(value); this._selectedIndexChanged = this._selectedIndex != value; this._selectedIndex = value; - this._focusIndex = value; + + if (this._keyManager) { + this._keyManager.updateActiveItemIndex(value); + } } /** Event emitted when the option is selected. */ @@ -164,18 +168,12 @@ export class MatTabHeader extends _MatTabHeaderMixinBase _handleKeydown(event: KeyboardEvent) { switch (event.keyCode) { - case RIGHT_ARROW: - this._focusNextTab(); - break; - case LEFT_ARROW: - this._focusPreviousTab(); - break; case HOME: - this._focusFirstTab(); + this._keyManager.setFirstItemActive(); event.preventDefault(); break; case END: - this._focusLastTab(); + this._keyManager.setLastItemActive(); event.preventDefault(); break; case ENTER: @@ -183,6 +181,8 @@ export class MatTabHeader extends _MatTabHeaderMixinBase this.selectFocusedIndex.emit(this.focusIndex); event.preventDefault(); break; + default: + this._keyManager.onKeydown(event); } } @@ -197,10 +197,19 @@ export class MatTabHeader extends _MatTabHeaderMixinBase this._alignInkBarToSelectedTab(); }; + this._keyManager = new FocusKeyManager(this._labelWrappers) + .withHorizontalOrientation(this._getLayoutDirection()); + + this._keyManager.updateActiveItemIndex(0); + // Defer the first call in order to allow for slower browsers to lay out the elements. // This helps in cases where the user lands directly on a page with paginated tabs. typeof requestAnimationFrame !== 'undefined' ? requestAnimationFrame(realign) : realign(); - this._realignInkBar = merge(dirChange, resize).subscribe(realign); + + this._realignInkBar = merge(dirChange, resize).subscribe(() => { + realign(); + this._keyManager.withHorizontalOrientation(this._getLayoutDirection()); + }); } ngOnDestroy() { @@ -227,14 +236,14 @@ export class MatTabHeader extends _MatTabHeaderMixinBase /** Tracks which element has focus; used for keyboard navigation */ get focusIndex(): number { - return this._focusIndex; + return this._keyManager ? this._keyManager.activeItemIndex! : 0; } /** When the focus index is set, we must manually send focus to the correct label */ set focusIndex(value: number) { - if (!this._isValidIndex(value) || this._focusIndex == value) { return; } + if (!this._isValidIndex(value) || this.focusIndex == value || !this._keyManager) { return; } - this._focusIndex = value; + this._keyManager.setActiveItem(value); this.indexFocused.emit(value); this._setTabFocus(value); } @@ -276,53 +285,6 @@ export class MatTabHeader extends _MatTabHeaderMixinBase } } - /** - * Moves the focus towards the beginning or the end of the list depending on the offset provided. - * Valid offsets are 1 and -1. - */ - _moveFocus(offset: number) { - if (this._labelWrappers) { - const tabs: MatTabLabelWrapper[] = this._labelWrappers.toArray(); - - for (let i = this.focusIndex + offset; i < tabs.length && i >= 0; i += offset) { - if (this._isValidIndex(i)) { - this.focusIndex = i; - return; - } - } - } - } - - /** Increment the focus index by 1 until a valid tab is found. */ - _focusNextTab(): void { - this._moveFocus(this._getLayoutDirection() == 'ltr' ? 1 : -1); - } - - /** Decrement the focus index by 1 until a valid tab is found. */ - _focusPreviousTab(): void { - this._moveFocus(this._getLayoutDirection() == 'ltr' ? -1 : 1); - } - - /** Focuses the first tab. */ - private _focusFirstTab(): void { - for (let i = 0; i < this._labelWrappers.length; i++) { - if (this._isValidIndex(i)) { - this.focusIndex = i; - break; - } - } - } - - /** Focuses the last tab. */ - private _focusLastTab(): void { - for (let i = this._labelWrappers.length - 1; i > -1; i--) { - if (this._isValidIndex(i)) { - this.focusIndex = i; - break; - } - } - } - /** The layout direction of the containing app. */ _getLayoutDirection(): Direction { return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr';