mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Jeremy Elbourn
parent
8271352e9b
commit
24f62eb63a
@@ -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', () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user