From 24f62eb63abf4cb29a404fc750802a79c851954e Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 26 Jan 2018 16:31:31 +0100 Subject: [PATCH] fix(tabs): move focus to first/last tabs using home/end (#9171) Implements the ability to focus the first and last tabs using the home and end keys, based on the [accessibility guidelines](https://www.w3.org/TR/wai-aria-practices-1.1/#tabpanel). --- src/lib/tabs/tab-header.spec.ts | 63 +++++++++++++++++++++++++++++++-- src/lib/tabs/tab-header.ts | 30 +++++++++++++++- src/lib/tabs/tabs.md | 3 ++ 3 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src/lib/tabs/tab-header.spec.ts b/src/lib/tabs/tab-header.spec.ts index 8ccd1ef952d..1177d7d2a76 100644 --- a/src/lib/tabs/tab-header.spec.ts +++ b/src/lib/tabs/tab-header.spec.ts @@ -1,10 +1,15 @@ import { - async, ComponentFixture, TestBed, fakeAsync, tick, discardPeriodicTasks + async, + ComponentFixture, + TestBed, + fakeAsync, + tick, + discardPeriodicTasks, } from '@angular/core/testing'; import {Component, ViewChild} from '@angular/core'; import {CommonModule} from '@angular/common'; import {By} from '@angular/platform-browser'; -import {ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes'; +import {ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE, HOME, END} from '@angular/cdk/keycodes'; import {PortalModule} from '@angular/cdk/portal'; import {Direction, Directionality} from '@angular/cdk/bidi'; import {dispatchFakeEvent, dispatchKeyboardEvent} from '@angular/cdk/testing'; @@ -136,6 +141,60 @@ describe('MatTabHeader', () => { expect(appComponent.selectedIndex).toBe(0); expect(spaceEvent.defaultPrevented).toBe(true); }); + + it('should move focus to the first tab when pressing HOME', () => { + appComponent.tabHeader.focusIndex = 3; + fixture.detectChanges(); + expect(appComponent.tabHeader.focusIndex).toBe(3); + + const tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement; + const event = dispatchKeyboardEvent(tabListContainer, 'keydown', HOME); + fixture.detectChanges(); + + expect(appComponent.tabHeader.focusIndex).toBe(0); + expect(event.defaultPrevented).toBe(true); + }); + + it('should skip disabled items when moving focus using HOME', () => { + appComponent.tabHeader.focusIndex = 3; + appComponent.tabs[0].disabled = true; + fixture.detectChanges(); + expect(appComponent.tabHeader.focusIndex).toBe(3); + + const tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement; + dispatchKeyboardEvent(tabListContainer, 'keydown', HOME); + fixture.detectChanges(); + + // Note that the second tab is disabled by default already. + expect(appComponent.tabHeader.focusIndex).toBe(2); + }); + + it('should move focus to the last tab when pressing END', () => { + appComponent.tabHeader.focusIndex = 0; + fixture.detectChanges(); + expect(appComponent.tabHeader.focusIndex).toBe(0); + + const tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement; + const event = dispatchKeyboardEvent(tabListContainer, 'keydown', END); + fixture.detectChanges(); + + expect(appComponent.tabHeader.focusIndex).toBe(3); + expect(event.defaultPrevented).toBe(true); + }); + + it('should skip disabled items when moving focus using END', () => { + appComponent.tabHeader.focusIndex = 0; + appComponent.tabs[3].disabled = true; + fixture.detectChanges(); + expect(appComponent.tabHeader.focusIndex).toBe(0); + + const tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement; + dispatchKeyboardEvent(tabListContainer, 'keydown', END); + fixture.detectChanges(); + + expect(appComponent.tabHeader.focusIndex).toBe(2); + }); + }); describe('pagination', () => { diff --git a/src/lib/tabs/tab-header.ts b/src/lib/tabs/tab-header.ts index db4067839ea..32c3ed0f794 100644 --- a/src/lib/tabs/tab-header.ts +++ b/src/lib/tabs/tab-header.ts @@ -7,7 +7,7 @@ */ import {Direction, Directionality} from '@angular/cdk/bidi'; -import {ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes'; +import {ENTER, LEFT_ARROW, RIGHT_ARROW, SPACE, HOME, END} from '@angular/cdk/keycodes'; import { AfterContentChecked, AfterContentInit, @@ -173,6 +173,14 @@ export class MatTabHeader extends _MatTabHeaderMixinBase case LEFT_ARROW: this._focusPreviousTab(); break; + case HOME: + this._focusFirstTab(); + event.preventDefault(); + break; + case END: + this._focusLastTab(); + event.preventDefault(); + break; case ENTER: case SPACE: this.selectFocusedIndex.emit(this.focusIndex); @@ -296,6 +304,26 @@ export class MatTabHeader extends _MatTabHeaderMixinBase 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'; diff --git a/src/lib/tabs/tabs.md b/src/lib/tabs/tabs.md index 4ff541e86bf..e6b044af585 100644 --- a/src/lib/tabs/tabs.md +++ b/src/lib/tabs/tabs.md @@ -91,4 +91,7 @@ Tabs without text or labels should be given a meaningful label via `aria-label` |----------------------|----------------------------| | `LEFT_ARROW` | Move focus to previous tab | | `RIGHT_ARROW` | Move focus to next tab | +| `HOME` | Move focus to first tab | +| `END` | Move focus to last tab | +| `RIGHT_ARROW` | Move focus to next tab | | `SPACE` or `ENTER` | Switch to focused tab |