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).
This commit is contained in:
Kristiyan Kostadinov
2018-01-26 16:31:31 +01:00
committed by Jeremy Elbourn
parent 8271352e9b
commit 24f62eb63a
3 changed files with 93 additions and 3 deletions
+61 -2
View File
@@ -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', () => {
+29 -1
View File
@@ -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';
+3
View File
@@ -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 |