From 23432bd12c0e637b09c0a420e652657708440ec7 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Mon, 13 Jul 2020 06:23:45 +0200 Subject: [PATCH] refactor: handle home/end presses through key manager Based on top of #19834, changes the places where we were handling home/end to go through the key manager. Also allows `withHomeAndEnd` to be turned off, similarly to what we're doing with the other methods. --- src/cdk-experimental/listbox/listbox.ts | 18 ++---------- src/cdk/a11y/key-manager/list-key-manager.ts | 9 +++--- src/cdk/stepper/stepper.ts | 9 ++---- .../mdc-chips/chip-grid.ts | 13 ++------- .../mdc-chips/chip-listbox.ts | 12 ++------ .../mdc-chips/grid-key-manager.ts | 29 +++++++++++++++++++ src/material/chips/chip-list.ts | 14 ++------- src/material/expansion/accordion.ts | 20 ++----------- src/material/list/selection-list.spec.ts | 4 +-- src/material/list/selection-list.ts | 10 +------ src/material/menu/menu.ts | 14 +++------ src/material/select/select.ts | 17 ++--------- src/material/tabs/paginated-tab-header.ts | 11 ++----- tools/public_api_guard/cdk/a11y.d.ts | 2 +- 14 files changed, 62 insertions(+), 120 deletions(-) diff --git a/src/cdk-experimental/listbox/listbox.ts b/src/cdk-experimental/listbox/listbox.ts index 05c19bd3080f..9228d5b6c9d3 100644 --- a/src/cdk-experimental/listbox/listbox.ts +++ b/src/cdk-experimental/listbox/listbox.ts @@ -16,16 +16,7 @@ import { QueryList } from '@angular/core'; import {ActiveDescendantKeyManager, Highlightable, ListKeyManagerOption} from '@angular/cdk/a11y'; -import { - DOWN_ARROW, - END, - ENTER, - HOME, - LEFT_ARROW, - RIGHT_ARROW, - SPACE, - UP_ARROW -} from '@angular/cdk/keycodes'; +import {DOWN_ARROW, ENTER, SPACE, UP_ARROW, LEFT_ARROW, RIGHT_ARROW} from '@angular/cdk/keycodes'; import {BooleanInput, coerceBooleanProperty, coerceArray} from '@angular/cdk/coercion'; import {SelectionChange, SelectionModel} from '@angular/cdk/collections'; import {defer, merge, Observable, Subject} from 'rxjs'; @@ -340,6 +331,7 @@ export class CdkListbox implements AfterContentInit, OnDestroy, OnInit, Contr this._listKeyManager = new ActiveDescendantKeyManager(this._options) .withWrap() .withTypeAhead() + .withHomeAndEnd() .withAllowedModifierKeys(['shiftKey']); if (this.orientation === 'vertical') { @@ -376,11 +368,7 @@ export class CdkListbox implements AfterContentInit, OnDestroy, OnInit, Contr const {keyCode} = event; const previousActiveIndex = manager.activeItemIndex; - if (keyCode === HOME || keyCode === END) { - event.preventDefault(); - keyCode === HOME ? manager.setFirstItemActive() : manager.setLastItemActive(); - - } else if (keyCode === SPACE || keyCode === ENTER) { + if (keyCode === SPACE || keyCode === ENTER) { if (manager.activeItem && !manager.isTyping()) { this._toggleActiveOption(); } diff --git a/src/cdk/a11y/key-manager/list-key-manager.ts b/src/cdk/a11y/key-manager/list-key-manager.ts index c3e4a135e56a..f1f530f6b887 100644 --- a/src/cdk/a11y/key-manager/list-key-manager.ts +++ b/src/cdk/a11y/key-manager/list-key-manager.ts @@ -178,11 +178,12 @@ export class ListKeyManager { } /** - * Configures the key manager to focus the first and last items - * respectively when the Home key and End Key are pressed. + * Configures the key manager to activate the first and last items + * respectively when the Home or End key is pressed. + * @param enabled Whether pressing the Home or End key activates the first/last item. */ - withHomeAndEnd(): this { - this._homeAndEnd = true; + withHomeAndEnd(enabled: boolean = true): this { + this._homeAndEnd = enabled; return this; } diff --git a/src/cdk/stepper/stepper.ts b/src/cdk/stepper/stepper.ts index f21d1da7ce38..011ee07824cd 100644 --- a/src/cdk/stepper/stepper.ts +++ b/src/cdk/stepper/stepper.ts @@ -14,7 +14,7 @@ import { coerceNumberProperty, NumberInput } from '@angular/cdk/coercion'; -import {END, ENTER, hasModifierKey, HOME, SPACE} from '@angular/cdk/keycodes'; +import {ENTER, hasModifierKey, SPACE} from '@angular/cdk/keycodes'; import {DOCUMENT} from '@angular/common'; import { AfterViewInit, @@ -349,6 +349,7 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { // AfterViewInit so we're guaranteed for both view and content children to be defined. this._keyManager = new FocusKeyManager(this._stepHeader) .withWrap() + .withHomeAndEnd() .withVerticalOrientation(this._orientation === 'vertical'); (this._dir ? (this._dir.change as Observable) : observableOf()) @@ -486,12 +487,6 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy { (keyCode === SPACE || keyCode === ENTER)) { this.selectedIndex = manager.activeItemIndex; event.preventDefault(); - } else if (keyCode === HOME) { - manager.setFirstItemActive(); - event.preventDefault(); - } else if (keyCode === END) { - manager.setLastItemActive(); - event.preventDefault(); } else { manager.onKeydown(event); } diff --git a/src/material-experimental/mdc-chips/chip-grid.ts b/src/material-experimental/mdc-chips/chip-grid.ts index 1c73dfa7dae7..f460a49bad58 100644 --- a/src/material-experimental/mdc-chips/chip-grid.ts +++ b/src/material-experimental/mdc-chips/chip-grid.ts @@ -8,7 +8,7 @@ import {Directionality} from '@angular/cdk/bidi'; import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; -import {BACKSPACE, TAB, HOME, END} from '@angular/cdk/keycodes'; +import {BACKSPACE, TAB} from '@angular/cdk/keycodes'; import { AfterContentInit, AfterViewInit, @@ -419,15 +419,7 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn } event.preventDefault(); } else if (this._originatesFromChip(event)) { - if (keyCode === HOME) { - manager.setFirstCellActive(); - event.preventDefault(); - } else if (keyCode === END) { - manager.setLastCellActive(); - event.preventDefault(); - } else { - manager.onKeydown(event); - } + manager.onKeydown(event); } this.stateChanges.next(); } @@ -456,6 +448,7 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn /** Initializes the key manager to manage focus. */ private _initKeyManager() { this._keyManager = new GridFocusKeyManager(this._chips) + .withHomeAndEnd() .withDirectionality(this._dir ? this._dir.value : 'ltr'); if (this._dir) { diff --git a/src/material-experimental/mdc-chips/chip-listbox.ts b/src/material-experimental/mdc-chips/chip-listbox.ts index 27cc149dc107..ddb567b9c218 100644 --- a/src/material-experimental/mdc-chips/chip-listbox.ts +++ b/src/material-experimental/mdc-chips/chip-listbox.ts @@ -9,7 +9,6 @@ import {FocusKeyManager} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; -import {END, HOME} from '@angular/cdk/keycodes'; import { AfterContentInit, ChangeDetectionStrategy, @@ -354,15 +353,7 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont */ _keydown(event: KeyboardEvent) { if (this._originatesFromChip(event)) { - if (event.keyCode === HOME) { - this._keyManager.setFirstItemActive(); - event.preventDefault(); - } else if (event.keyCode === END) { - this._keyManager.setLastItemActive(); - event.preventDefault(); - } else { - this._keyManager.onKeydown(event); - } + this._keyManager.onKeydown(event); } } @@ -457,6 +448,7 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont this._keyManager = new FocusKeyManager(this._chips) .withWrap() .withVerticalOrientation() + .withHomeAndEnd() .withHorizontalOrientation(this._dir ? this._dir.value : 'ltr'); if (this._dir) { diff --git a/src/material-experimental/mdc-chips/grid-key-manager.ts b/src/material-experimental/mdc-chips/grid-key-manager.ts index 1fe9fd5ede28..693598cc3140 100644 --- a/src/material-experimental/mdc-chips/grid-key-manager.ts +++ b/src/material-experimental/mdc-chips/grid-key-manager.ts @@ -13,6 +13,8 @@ import { DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, + HOME, + END, } from '@angular/cdk/keycodes'; @@ -37,6 +39,7 @@ export class GridKeyManager { private _activeRow: GridKeyManagerRow | null = null; private _activeCell: T | null = null; private _dir: 'ltr' | 'rtl' = 'ltr'; + private _homeAndEnd = false; constructor(private _rows: QueryList> | GridKeyManagerRow[]) { // We allow for the rows to be an array because, in some cases, the consumer may @@ -93,6 +96,16 @@ export class GridKeyManager { } } + /** + * Configures the key manager to activate the first and last items + * respectively when the Home or End key is pressed. + * @param enabled Whether pressing the Home or End key activates the first/last item. + */ + withHomeAndEnd(enabled: boolean = true): this { + this._homeAndEnd = enabled; + return this; + } + /** * Sets the active cell depending on the key event passed in. * @param event Keyboard event to be used for determining which element should be active. @@ -117,6 +130,22 @@ export class GridKeyManager { this._dir === 'rtl' ? this.setNextColumnActive() : this.setPreviousColumnActive(); break; + case HOME: + if (this._homeAndEnd) { + this.setFirstCellActive(); + break; + } else { + return; + } + + case END: + if (this._homeAndEnd) { + this.setLastCellActive(); + break; + } else { + return; + } + default: // Note that we return here, in order to avoid preventing // the default action of non-navigational keys. diff --git a/src/material/chips/chip-list.ts b/src/material/chips/chip-list.ts index ff4f47bc613b..d2caf2f0afd5 100644 --- a/src/material/chips/chip-list.ts +++ b/src/material/chips/chip-list.ts @@ -10,7 +10,7 @@ import {FocusKeyManager} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; import {SelectionModel} from '@angular/cdk/collections'; -import {BACKSPACE, END, HOME} from '@angular/cdk/keycodes'; +import {BACKSPACE} from '@angular/cdk/keycodes'; import { AfterContentInit, ChangeDetectionStrategy, @@ -351,6 +351,7 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo this._keyManager = new FocusKeyManager(this.chips) .withWrap() .withVerticalOrientation() + .withHomeAndEnd() .withHorizontalOrientation(this._dir ? this._dir.value : 'ltr'); if (this._dir) { @@ -503,16 +504,7 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo this._keyManager.setLastItemActive(); event.preventDefault(); } else if (target && target.classList.contains('mat-chip')) { - if (event.keyCode === HOME) { - this._keyManager.setFirstItemActive(); - event.preventDefault(); - } else if (event.keyCode === END) { - this._keyManager.setLastItemActive(); - event.preventDefault(); - } else { - this._keyManager.onKeydown(event); - } - + this._keyManager.onKeydown(event); this.stateChanges.next(); } } diff --git a/src/material/expansion/accordion.ts b/src/material/expansion/accordion.ts index 6714f68c3bea..eea3a4bf5e7b 100644 --- a/src/material/expansion/accordion.ts +++ b/src/material/expansion/accordion.ts @@ -10,7 +10,6 @@ import {Directive, Input, ContentChildren, QueryList, AfterContentInit} from '@a import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; import {CdkAccordion} from '@angular/cdk/accordion'; import {FocusKeyManager} from '@angular/cdk/a11y'; -import {HOME, END, hasModifierKey} from '@angular/cdk/keycodes'; import {startWith} from 'rxjs/operators'; import { MAT_ACCORDION, @@ -75,27 +74,12 @@ export class MatAccordion extends CdkAccordion implements MatAccordionBase, Afte this._ownHeaders.notifyOnChanges(); }); - this._keyManager = new FocusKeyManager(this._ownHeaders).withWrap(); + this._keyManager = new FocusKeyManager(this._ownHeaders).withWrap().withHomeAndEnd(); } /** Handles keyboard events coming in from the panel headers. */ _handleHeaderKeydown(event: KeyboardEvent) { - const {keyCode} = event; - const manager = this._keyManager; - - if (keyCode === HOME) { - if (!hasModifierKey(event)) { - manager.setFirstItemActive(); - event.preventDefault(); - } - } else if (keyCode === END) { - if (!hasModifierKey(event)) { - manager.setLastItemActive(); - event.preventDefault(); - } - } else { - this._keyManager.onKeydown(event); - } + this._keyManager.onKeydown(event); } _handleHeaderFocus(header: MatExpansionPanelHeader) { diff --git a/src/material/list/selection-list.spec.ts b/src/material/list/selection-list.spec.ts index 59c25bbc2151..5b781a65f9ea 100644 --- a/src/material/list/selection-list.spec.ts +++ b/src/material/list/selection-list.spec.ts @@ -453,7 +453,7 @@ describe('MatSelectionList without forms', () => { const manager = selectionList.componentInstance._keyManager; expect(manager.activeItemIndex).toBe(-1); - const event = createKeyboardEvent('keydown', HOME, undefined, {shift: true}); + const event = createKeyboardEvent('keydown', HOME, undefined, {alt: true}); dispatchEvent(selectionList.nativeElement, event); fixture.detectChanges(); @@ -477,7 +477,7 @@ describe('MatSelectionList without forms', () => { const manager = selectionList.componentInstance._keyManager; expect(manager.activeItemIndex).toBe(-1); - const event = createKeyboardEvent('keydown', END, undefined, {shift: true}); + const event = createKeyboardEvent('keydown', END, undefined, {alt: true}); dispatchEvent(selectionList.nativeElement, event); fixture.detectChanges(); diff --git a/src/material/list/selection-list.ts b/src/material/list/selection-list.ts index 35179047f201..6bf1bf305a98 100644 --- a/src/material/list/selection-list.ts +++ b/src/material/list/selection-list.ts @@ -12,10 +12,8 @@ import {SelectionModel} from '@angular/cdk/collections'; import { A, DOWN_ARROW, - END, ENTER, hasModifierKey, - HOME, SPACE, UP_ARROW, } from '@angular/cdk/keycodes'; @@ -427,6 +425,7 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD this._keyManager = new FocusKeyManager(this.options) .withWrap() .withTypeAhead() + .withHomeAndEnd() // Allow disabled items to be focusable. For accessibility reasons, there must be a way for // screenreader users, that allows reading the different options of the list. .skipPredicate(() => false) @@ -533,13 +532,6 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD event.preventDefault(); } break; - case HOME: - case END: - if (!hasModifier) { - keyCode === HOME ? manager.setFirstItemActive() : manager.setLastItemActive(); - event.preventDefault(); - } - break; default: // The "A" key gets special treatment, because it's used for the "select all" functionality. if (keyCode === A && this.multiple && hasModifierKey(event, 'ctrlKey') && diff --git a/src/material/menu/menu.ts b/src/material/menu/menu.ts index 43e4e2026ef7..6c28165db83e 100644 --- a/src/material/menu/menu.ts +++ b/src/material/menu/menu.ts @@ -15,8 +15,6 @@ import { RIGHT_ARROW, DOWN_ARROW, UP_ARROW, - HOME, - END, hasModifierKey, } from '@angular/cdk/keycodes'; import { @@ -265,7 +263,10 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel ngAfterContentInit() { this._updateDirectDescendants(); - this._keyManager = new FocusKeyManager(this._directDescendantItems).withWrap().withTypeAhead(); + this._keyManager = new FocusKeyManager(this._directDescendantItems) + .withWrap() + .withTypeAhead() + .withHomeAndEnd(); this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.closed.emit('tab')); // If a user manually (programatically) focuses a menu item, we need to reflect that focus @@ -331,13 +332,6 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel this.closed.emit('keydown'); } break; - case HOME: - case END: - if (!hasModifierKey(event)) { - keyCode === HOME ? manager.setFirstItemActive() : manager.setLastItemActive(); - event.preventDefault(); - } - break; default: if (keyCode === UP_ARROW || keyCode === DOWN_ARROW) { manager.setFocusOrigin('keyboard'); diff --git a/src/material/select/select.ts b/src/material/select/select.ts index 9aed26c3529d..161dc674ebaf 100644 --- a/src/material/select/select.ts +++ b/src/material/select/select.ts @@ -18,10 +18,8 @@ import {SelectionModel} from '@angular/cdk/collections'; import { A, DOWN_ARROW, - END, ENTER, hasModifierKey, - HOME, LEFT_ARROW, RIGHT_ARROW, SPACE, @@ -772,14 +770,7 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit, this.open(); } else if (!this.multiple) { const previouslySelectedOption = this.selected; - - if (keyCode === HOME || keyCode === END) { - keyCode === HOME ? manager.setFirstItemActive() : manager.setLastItemActive(); - event.preventDefault(); - } else { - manager.onKeydown(event); - } - + manager.onKeydown(event); const selectedOption = this.selected; // Since the value has changed, we need to announce it ourselves. @@ -798,10 +789,7 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit, const isArrowKey = keyCode === DOWN_ARROW || keyCode === UP_ARROW; const isTyping = manager.isTyping(); - if (keyCode === HOME || keyCode === END) { - event.preventDefault(); - keyCode === HOME ? manager.setFirstItemActive() : manager.setLastItemActive(); - } else if (isArrowKey && event.altKey) { + if (isArrowKey && event.altKey) { // Close the select on ALT + arrow key to match the native