From 4aaac44fb76e9a5a03b591f10c5f79e75905307b Mon Sep 17 00:00:00 2001 From: Vanessa Schmitt Date: Wed, 1 Jul 2020 15:28:14 -0700 Subject: [PATCH] feat(a11y): Add optional home/end key support to ListKeyManager For some components like menus and listboxes, WAI a11y recommends that pressing home or end should focus the first or last element respectively. Add support for home/end to ListKeyManager. --- .../a11y/key-manager/list-key-manager.spec.ts | 30 ++++++++++++++++++- src/cdk/a11y/key-manager/list-key-manager.ts | 28 +++++++++++++++++ tools/public_api_guard/cdk/a11y.d.ts | 1 + 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/cdk/a11y/key-manager/list-key-manager.spec.ts b/src/cdk/a11y/key-manager/list-key-manager.spec.ts index df4bbe10379d..38760f42ded1 100644 --- a/src/cdk/a11y/key-manager/list-key-manager.spec.ts +++ b/src/cdk/a11y/key-manager/list-key-manager.spec.ts @@ -1,4 +1,4 @@ -import {DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, TAB, UP_ARROW} from '@angular/cdk/keycodes'; +import {DOWN_ARROW, END, HOME, LEFT_ARROW, RIGHT_ARROW, TAB, UP_ARROW} from '@angular/cdk/keycodes'; import {createKeyboardEvent} from '@angular/cdk/testing/private'; import {QueryList} from '@angular/core'; import {fakeAsync, tick} from '@angular/core/testing'; @@ -51,6 +51,8 @@ describe('Key managers', () => { leftArrow: KeyboardEvent, rightArrow: KeyboardEvent, tab: KeyboardEvent, + home: KeyboardEvent, + end: KeyboardEvent, unsupported: KeyboardEvent }; @@ -62,6 +64,8 @@ describe('Key managers', () => { leftArrow: createKeyboardEvent('keydown', LEFT_ARROW), rightArrow: createKeyboardEvent('keydown', RIGHT_ARROW), tab: createKeyboardEvent('keydown', TAB), + home: createKeyboardEvent('keydown', HOME), + end: createKeyboardEvent('keydown', END), unsupported: createKeyboardEvent('keydown', 192) // corresponds to the tilde character (~) }; }); @@ -195,6 +199,30 @@ describe('Key managers', () => { expect(fakeKeyEvents.downArrow.defaultPrevented).toBe(false); }); + describe('withHomeAndEnd', () => { + beforeEach(() => { + keyManager.withHomeAndEnd(); + }); + + it('should focus the first item when Home is pressed', () => { + keyManager.setActiveItem(1); + expect(keyManager.activeItemIndex).toBe(1); + + keyManager.onKeydown(fakeKeyEvents.home); + + expect(keyManager.activeItemIndex).toBe(0); + }); + + it('should focus the last item when End is pressed', () => { + keyManager.setActiveItem(0); + expect(keyManager.activeItemIndex).toBe(0); + + keyManager.onKeydown(fakeKeyEvents.end); + + expect(keyManager.activeItemIndex).toBe(itemList.items.length - 1); + }); + }); + describe('with `vertical` direction', function(this: KeyEventTestContext) { beforeEach(() => { keyManager.withVerticalOrientation(); diff --git a/src/cdk/a11y/key-manager/list-key-manager.ts b/src/cdk/a11y/key-manager/list-key-manager.ts index b7440157fb76..ec984a912bdf 100644 --- a/src/cdk/a11y/key-manager/list-key-manager.ts +++ b/src/cdk/a11y/key-manager/list-key-manager.ts @@ -19,6 +19,8 @@ import { ZERO, NINE, hasModifierKey, + HOME, + END, } from '@angular/cdk/keycodes'; import {debounceTime, filter, map, tap} from 'rxjs/operators'; @@ -47,6 +49,7 @@ export class ListKeyManager { private _vertical = true; private _horizontal: 'ltr' | 'rtl' | null; private _allowedModifierKeys: ListKeyManagerModifierKey[] = []; + private _homeAndEnd = false; /** * Predicate function that can be used to check whether an item should be skipped @@ -174,6 +177,15 @@ export class ListKeyManager { return this; } + /** + * Configures the key manager to focus the first and last items + * respectively when the Home key and End Key are pressed. + */ + withHomeAndEnd(): this { + this._homeAndEnd = true; + return this; + } + /** * Sets the active item to the item at the index specified. * @param index The index of the item to be set as active. @@ -244,6 +256,22 @@ export class ListKeyManager { return; } + case HOME: + if (this._homeAndEnd && isModifierAllowed) { + this.setFirstItemActive(); + break; + } else { + return; + } + + case END: + if (this._homeAndEnd && isModifierAllowed) { + this.setLastItemActive(); + break; + } else { + return; + } + default: if (isModifierAllowed || hasModifierKey(event, 'shiftKey')) { // Attempt to use the `event.key` which also maps it to the user's keyboard language, diff --git a/tools/public_api_guard/cdk/a11y.d.ts b/tools/public_api_guard/cdk/a11y.d.ts index 76d55766a2c7..f9e43a21ba80 100644 --- a/tools/public_api_guard/cdk/a11y.d.ts +++ b/tools/public_api_guard/cdk/a11y.d.ts @@ -208,6 +208,7 @@ export declare class ListKeyManager { updateActiveItem(index: number): void; updateActiveItem(item: T): void; withAllowedModifierKeys(keys: ListKeyManagerModifierKey[]): this; + withHomeAndEnd(): this; withHorizontalOrientation(direction: 'ltr' | 'rtl' | null): this; withTypeAhead(debounceInterval?: number): this; withVerticalOrientation(enabled?: boolean): this;