Skip to content

Commit d96de48

Browse files
authored
refactor: handle home/end presses through key manager (#19955)
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.
1 parent 3345a9a commit d96de48

File tree

14 files changed

+62
-120
lines changed

14 files changed

+62
-120
lines changed

src/cdk-experimental/listbox/listbox.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,7 @@ import {
1616
QueryList
1717
} from '@angular/core';
1818
import {ActiveDescendantKeyManager, Highlightable, ListKeyManagerOption} from '@angular/cdk/a11y';
19-
import {
20-
DOWN_ARROW,
21-
END,
22-
ENTER,
23-
HOME,
24-
LEFT_ARROW,
25-
RIGHT_ARROW,
26-
SPACE,
27-
UP_ARROW
28-
} from '@angular/cdk/keycodes';
19+
import {DOWN_ARROW, ENTER, SPACE, UP_ARROW, LEFT_ARROW, RIGHT_ARROW} from '@angular/cdk/keycodes';
2920
import {BooleanInput, coerceBooleanProperty, coerceArray} from '@angular/cdk/coercion';
3021
import {SelectionChange, SelectionModel} from '@angular/cdk/collections';
3122
import {defer, merge, Observable, Subject} from 'rxjs';
@@ -340,6 +331,7 @@ export class CdkListbox<T> implements AfterContentInit, OnDestroy, OnInit, Contr
340331
this._listKeyManager = new ActiveDescendantKeyManager(this._options)
341332
.withWrap()
342333
.withTypeAhead()
334+
.withHomeAndEnd()
343335
.withAllowedModifierKeys(['shiftKey']);
344336

345337
if (this.orientation === 'vertical') {
@@ -376,11 +368,7 @@ export class CdkListbox<T> implements AfterContentInit, OnDestroy, OnInit, Contr
376368
const {keyCode} = event;
377369
const previousActiveIndex = manager.activeItemIndex;
378370

379-
if (keyCode === HOME || keyCode === END) {
380-
event.preventDefault();
381-
keyCode === HOME ? manager.setFirstItemActive() : manager.setLastItemActive();
382-
383-
} else if (keyCode === SPACE || keyCode === ENTER) {
371+
if (keyCode === SPACE || keyCode === ENTER) {
384372
if (manager.activeItem && !manager.isTyping()) {
385373
this._toggleActiveOption();
386374
}

src/cdk/a11y/key-manager/list-key-manager.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -179,11 +179,12 @@ export class ListKeyManager<T extends ListKeyManagerOption> {
179179
}
180180

181181
/**
182-
* Configures the key manager to focus the first and last items
183-
* respectively when the Home key and End Key are pressed.
182+
* Configures the key manager to activate the first and last items
183+
* respectively when the Home or End key is pressed.
184+
* @param enabled Whether pressing the Home or End key activates the first/last item.
184185
*/
185-
withHomeAndEnd(): this {
186-
this._homeAndEnd = true;
186+
withHomeAndEnd(enabled: boolean = true): this {
187+
this._homeAndEnd = enabled;
187188
return this;
188189
}
189190

src/cdk/stepper/stepper.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
coerceNumberProperty,
1515
NumberInput
1616
} from '@angular/cdk/coercion';
17-
import {END, ENTER, hasModifierKey, HOME, SPACE} from '@angular/cdk/keycodes';
17+
import {ENTER, hasModifierKey, SPACE} from '@angular/cdk/keycodes';
1818
import {DOCUMENT} from '@angular/common';
1919
import {
2020
AfterViewInit,
@@ -350,6 +350,7 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
350350
// AfterViewInit so we're guaranteed for both view and content children to be defined.
351351
this._keyManager = new FocusKeyManager<FocusableOption>(this._stepHeader)
352352
.withWrap()
353+
.withHomeAndEnd()
353354
.withVerticalOrientation(this._orientation === 'vertical');
354355

355356
(this._dir ? (this._dir.change as Observable<Direction>) : observableOf<Direction>())
@@ -487,12 +488,6 @@ export class CdkStepper implements AfterContentInit, AfterViewInit, OnDestroy {
487488
(keyCode === SPACE || keyCode === ENTER)) {
488489
this.selectedIndex = manager.activeItemIndex;
489490
event.preventDefault();
490-
} else if (keyCode === HOME) {
491-
manager.setFirstItemActive();
492-
event.preventDefault();
493-
} else if (keyCode === END) {
494-
manager.setLastItemActive();
495-
event.preventDefault();
496491
} else {
497492
manager.onKeydown(event);
498493
}

src/material-experimental/mdc-chips/chip-grid.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {Directionality} from '@angular/cdk/bidi';
1010
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
11-
import {BACKSPACE, TAB, HOME, END} from '@angular/cdk/keycodes';
11+
import {BACKSPACE, TAB} from '@angular/cdk/keycodes';
1212
import {
1313
AfterContentInit,
1414
AfterViewInit,
@@ -419,15 +419,7 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
419419
}
420420
event.preventDefault();
421421
} else if (this._originatesFromChip(event)) {
422-
if (keyCode === HOME) {
423-
manager.setFirstCellActive();
424-
event.preventDefault();
425-
} else if (keyCode === END) {
426-
manager.setLastCellActive();
427-
event.preventDefault();
428-
} else {
429-
manager.onKeydown(event);
430-
}
422+
manager.onKeydown(event);
431423
}
432424
this.stateChanges.next();
433425
}
@@ -456,6 +448,7 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
456448
/** Initializes the key manager to manage focus. */
457449
private _initKeyManager() {
458450
this._keyManager = new GridFocusKeyManager(this._chips)
451+
.withHomeAndEnd()
459452
.withDirectionality(this._dir ? this._dir.value : 'ltr');
460453

461454
if (this._dir) {

src/material-experimental/mdc-chips/chip-listbox.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import {FocusKeyManager} from '@angular/cdk/a11y';
1010
import {Directionality} from '@angular/cdk/bidi';
1111
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
12-
import {END, HOME} from '@angular/cdk/keycodes';
1312
import {
1413
AfterContentInit,
1514
ChangeDetectionStrategy,
@@ -354,15 +353,7 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont
354353
*/
355354
_keydown(event: KeyboardEvent) {
356355
if (this._originatesFromChip(event)) {
357-
if (event.keyCode === HOME) {
358-
this._keyManager.setFirstItemActive();
359-
event.preventDefault();
360-
} else if (event.keyCode === END) {
361-
this._keyManager.setLastItemActive();
362-
event.preventDefault();
363-
} else {
364-
this._keyManager.onKeydown(event);
365-
}
356+
this._keyManager.onKeydown(event);
366357
}
367358
}
368359

@@ -457,6 +448,7 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont
457448
this._keyManager = new FocusKeyManager<MatChip>(this._chips)
458449
.withWrap()
459450
.withVerticalOrientation()
451+
.withHomeAndEnd()
460452
.withHorizontalOrientation(this._dir ? this._dir.value : 'ltr');
461453

462454
if (this._dir) {

src/material-experimental/mdc-chips/grid-key-manager.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
DOWN_ARROW,
1414
LEFT_ARROW,
1515
RIGHT_ARROW,
16+
HOME,
17+
END,
1618
} from '@angular/cdk/keycodes';
1719

1820

@@ -37,6 +39,7 @@ export class GridKeyManager<T> {
3739
private _activeRow: GridKeyManagerRow<T> | null = null;
3840
private _activeCell: T | null = null;
3941
private _dir: 'ltr' | 'rtl' = 'ltr';
42+
private _homeAndEnd = false;
4043

4144
constructor(private _rows: QueryList<GridKeyManagerRow<T>> | GridKeyManagerRow<T>[]) {
4245
// We allow for the rows to be an array because, in some cases, the consumer may
@@ -93,6 +96,16 @@ export class GridKeyManager<T> {
9396
}
9497
}
9598

99+
/**
100+
* Configures the key manager to activate the first and last items
101+
* respectively when the Home or End key is pressed.
102+
* @param enabled Whether pressing the Home or End key activates the first/last item.
103+
*/
104+
withHomeAndEnd(enabled: boolean = true): this {
105+
this._homeAndEnd = enabled;
106+
return this;
107+
}
108+
96109
/**
97110
* Sets the active cell depending on the key event passed in.
98111
* @param event Keyboard event to be used for determining which element should be active.
@@ -117,6 +130,22 @@ export class GridKeyManager<T> {
117130
this._dir === 'rtl' ? this.setNextColumnActive() : this.setPreviousColumnActive();
118131
break;
119132

133+
case HOME:
134+
if (this._homeAndEnd) {
135+
this.setFirstCellActive();
136+
break;
137+
} else {
138+
return;
139+
}
140+
141+
case END:
142+
if (this._homeAndEnd) {
143+
this.setLastCellActive();
144+
break;
145+
} else {
146+
return;
147+
}
148+
120149
default:
121150
// Note that we return here, in order to avoid preventing
122151
// the default action of non-navigational keys.

src/material/chips/chip-list.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {FocusKeyManager} from '@angular/cdk/a11y';
1010
import {Directionality} from '@angular/cdk/bidi';
1111
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
1212
import {SelectionModel} from '@angular/cdk/collections';
13-
import {BACKSPACE, END, HOME} from '@angular/cdk/keycodes';
13+
import {BACKSPACE} from '@angular/cdk/keycodes';
1414
import {
1515
AfterContentInit,
1616
ChangeDetectionStrategy,
@@ -351,6 +351,7 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo
351351
this._keyManager = new FocusKeyManager<MatChip>(this.chips)
352352
.withWrap()
353353
.withVerticalOrientation()
354+
.withHomeAndEnd()
354355
.withHorizontalOrientation(this._dir ? this._dir.value : 'ltr');
355356

356357
if (this._dir) {
@@ -503,16 +504,7 @@ export class MatChipList extends _MatChipListMixinBase implements MatFormFieldCo
503504
this._keyManager.setLastItemActive();
504505
event.preventDefault();
505506
} else if (target && target.classList.contains('mat-chip')) {
506-
if (event.keyCode === HOME) {
507-
this._keyManager.setFirstItemActive();
508-
event.preventDefault();
509-
} else if (event.keyCode === END) {
510-
this._keyManager.setLastItemActive();
511-
event.preventDefault();
512-
} else {
513-
this._keyManager.onKeydown(event);
514-
}
515-
507+
this._keyManager.onKeydown(event);
516508
this.stateChanges.next();
517509
}
518510
}

src/material/expansion/accordion.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {Directive, Input, ContentChildren, QueryList, AfterContentInit} from '@a
1010
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
1111
import {CdkAccordion} from '@angular/cdk/accordion';
1212
import {FocusKeyManager} from '@angular/cdk/a11y';
13-
import {HOME, END, hasModifierKey} from '@angular/cdk/keycodes';
1413
import {startWith} from 'rxjs/operators';
1514
import {
1615
MAT_ACCORDION,
@@ -75,27 +74,12 @@ export class MatAccordion extends CdkAccordion implements MatAccordionBase, Afte
7574
this._ownHeaders.notifyOnChanges();
7675
});
7776

78-
this._keyManager = new FocusKeyManager(this._ownHeaders).withWrap();
77+
this._keyManager = new FocusKeyManager(this._ownHeaders).withWrap().withHomeAndEnd();
7978
}
8079

8180
/** Handles keyboard events coming in from the panel headers. */
8281
_handleHeaderKeydown(event: KeyboardEvent) {
83-
const {keyCode} = event;
84-
const manager = this._keyManager;
85-
86-
if (keyCode === HOME) {
87-
if (!hasModifierKey(event)) {
88-
manager.setFirstItemActive();
89-
event.preventDefault();
90-
}
91-
} else if (keyCode === END) {
92-
if (!hasModifierKey(event)) {
93-
manager.setLastItemActive();
94-
event.preventDefault();
95-
}
96-
} else {
97-
this._keyManager.onKeydown(event);
98-
}
82+
this._keyManager.onKeydown(event);
9983
}
10084

10185
_handleHeaderFocus(header: MatExpansionPanelHeader) {

src/material/list/selection-list.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ describe('MatSelectionList without forms', () => {
477477
const manager = selectionList.componentInstance._keyManager;
478478
expect(manager.activeItemIndex).toBe(-1);
479479

480-
const event = createKeyboardEvent('keydown', HOME, undefined, {shift: true});
480+
const event = createKeyboardEvent('keydown', HOME, undefined, {alt: true});
481481

482482
dispatchEvent(selectionList.nativeElement, event);
483483
fixture.detectChanges();
@@ -501,7 +501,7 @@ describe('MatSelectionList without forms', () => {
501501
const manager = selectionList.componentInstance._keyManager;
502502
expect(manager.activeItemIndex).toBe(-1);
503503

504-
const event = createKeyboardEvent('keydown', END, undefined, {shift: true});
504+
const event = createKeyboardEvent('keydown', END, undefined, {alt: true});
505505

506506
dispatchEvent(selectionList.nativeElement, event);
507507
fixture.detectChanges();

src/material/list/selection-list.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@ import {SelectionModel} from '@angular/cdk/collections';
1212
import {
1313
A,
1414
DOWN_ARROW,
15-
END,
1615
ENTER,
1716
hasModifierKey,
18-
HOME,
1917
SPACE,
2018
UP_ARROW,
2119
} from '@angular/cdk/keycodes';
@@ -431,6 +429,7 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD
431429
this._keyManager = new FocusKeyManager<MatListOption>(this.options)
432430
.withWrap()
433431
.withTypeAhead()
432+
.withHomeAndEnd()
434433
// Allow disabled items to be focusable. For accessibility reasons, there must be a way for
435434
// screenreader users, that allows reading the different options of the list.
436435
.skipPredicate(() => false)
@@ -556,13 +555,6 @@ export class MatSelectionList extends _MatSelectionListMixinBase implements CanD
556555
event.preventDefault();
557556
}
558557
break;
559-
case HOME:
560-
case END:
561-
if (!hasModifier) {
562-
keyCode === HOME ? manager.setFirstItemActive() : manager.setLastItemActive();
563-
event.preventDefault();
564-
}
565-
break;
566558
default:
567559
// The "A" key gets special treatment, because it's used for the "select all" functionality.
568560
if (keyCode === A && this.multiple && hasModifierKey(event, 'ctrlKey') &&

src/material/menu/menu.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ import {
1515
RIGHT_ARROW,
1616
DOWN_ARROW,
1717
UP_ARROW,
18-
HOME,
19-
END,
2018
hasModifierKey,
2119
} from '@angular/cdk/keycodes';
2220
import {
@@ -265,7 +263,10 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>
265263

266264
ngAfterContentInit() {
267265
this._updateDirectDescendants();
268-
this._keyManager = new FocusKeyManager(this._directDescendantItems).withWrap().withTypeAhead();
266+
this._keyManager = new FocusKeyManager(this._directDescendantItems)
267+
.withWrap()
268+
.withTypeAhead()
269+
.withHomeAndEnd();
269270
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.closed.emit('tab'));
270271

271272
// 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<MatMenuItem>
331332
this.closed.emit('keydown');
332333
}
333334
break;
334-
case HOME:
335-
case END:
336-
if (!hasModifierKey(event)) {
337-
keyCode === HOME ? manager.setFirstItemActive() : manager.setLastItemActive();
338-
event.preventDefault();
339-
}
340-
break;
341335
default:
342336
if (keyCode === UP_ARROW || keyCode === DOWN_ARROW) {
343337
manager.setFocusOrigin('keyboard');

0 commit comments

Comments
 (0)