Skip to content

Commit 0185dd1

Browse files
crisbetojelbourn
authored andcommitted
fix(menu): support focus first/last item via home/end keys (#14896)
Adds support for jumping to the first/last item using the home/end keys.
1 parent 677db8c commit 0185dd1

File tree

2 files changed

+119
-4
lines changed

2 files changed

+119
-4
lines changed

src/lib/menu/menu-directive.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,16 @@
99
import {FocusKeyManager, FocusOrigin} from '@angular/cdk/a11y';
1010
import {Direction} from '@angular/cdk/bidi';
1111
import {coerceBooleanProperty} from '@angular/cdk/coercion';
12-
import {ESCAPE, LEFT_ARROW, RIGHT_ARROW, DOWN_ARROW, UP_ARROW} from '@angular/cdk/keycodes';
12+
import {
13+
ESCAPE,
14+
LEFT_ARROW,
15+
RIGHT_ARROW,
16+
DOWN_ARROW,
17+
UP_ARROW,
18+
HOME,
19+
END,
20+
hasModifierKey,
21+
} from '@angular/cdk/keycodes';
1322
import {
1423
AfterContentInit,
1524
ChangeDetectionStrategy,
@@ -268,6 +277,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnI
268277
/** Handle a keyboard event from the menu, delegating to the appropriate action. */
269278
_handleKeydown(event: KeyboardEvent) {
270279
const keyCode = event.keyCode;
280+
const manager = this._keyManager;
271281

272282
switch (keyCode) {
273283
case ESCAPE:
@@ -283,12 +293,19 @@ export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnI
283293
this.closed.emit('keydown');
284294
}
285295
break;
296+
case HOME:
297+
case END:
298+
if (!hasModifierKey(event)) {
299+
keyCode === HOME ? manager.setFirstItemActive() : manager.setLastItemActive();
300+
event.preventDefault();
301+
}
302+
break;
286303
default:
287304
if (keyCode === UP_ARROW || keyCode === DOWN_ARROW) {
288-
this._keyManager.setFocusOrigin('keyboard');
305+
manager.setFocusOrigin('keyboard');
289306
}
290307

291-
this._keyManager.onKeydown(event);
308+
manager.onKeydown(event);
292309
}
293310
}
294311

src/lib/menu/menu.spec.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
} from '@angular/core';
1818
import {Direction, Directionality} from '@angular/cdk/bidi';
1919
import {OverlayContainer, Overlay} from '@angular/cdk/overlay';
20-
import {ESCAPE, LEFT_ARROW, RIGHT_ARROW, DOWN_ARROW, TAB} from '@angular/cdk/keycodes';
20+
import {ESCAPE, LEFT_ARROW, RIGHT_ARROW, DOWN_ARROW, TAB, HOME, END} from '@angular/cdk/keycodes';
2121
import {
2222
MAT_MENU_DEFAULT_OPTIONS,
2323
MatMenu,
@@ -624,6 +624,104 @@ describe('MatMenu', () => {
624624
expect(overlayContainerElement.textContent).toBe('');
625625
}));
626626

627+
it('should focus the first item when pressing home', fakeAsync(() => {
628+
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
629+
fixture.detectChanges();
630+
631+
fixture.componentInstance.trigger.openMenu();
632+
fixture.detectChanges();
633+
634+
const panel = overlayContainerElement.querySelector('.mat-menu-panel')!;
635+
const items = Array.from(panel.querySelectorAll('.mat-menu-item')) as HTMLElement[];
636+
items.forEach(patchElementFocus);
637+
638+
// Focus the last item since focus starts from the first one.
639+
items[items.length - 1].focus();
640+
fixture.detectChanges();
641+
642+
spyOn(items[0], 'focus').and.callThrough();
643+
644+
const event = dispatchKeyboardEvent(panel, 'keydown', HOME);
645+
fixture.detectChanges();
646+
647+
expect(items[0].focus).toHaveBeenCalled();
648+
expect(event.defaultPrevented).toBe(true);
649+
flush();
650+
}));
651+
652+
it('should not focus the first item when pressing home with a modifier key', fakeAsync(() => {
653+
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
654+
fixture.detectChanges();
655+
656+
fixture.componentInstance.trigger.openMenu();
657+
fixture.detectChanges();
658+
659+
const panel = overlayContainerElement.querySelector('.mat-menu-panel')!;
660+
const items = Array.from(panel.querySelectorAll('.mat-menu-item')) as HTMLElement[];
661+
items.forEach(patchElementFocus);
662+
663+
// Focus the last item since focus starts from the first one.
664+
items[items.length - 1].focus();
665+
fixture.detectChanges();
666+
667+
spyOn(items[0], 'focus').and.callThrough();
668+
669+
const event = createKeyboardEvent('keydown', HOME);
670+
Object.defineProperty(event, 'altKey', {get: () => true});
671+
672+
dispatchEvent(panel, event);
673+
fixture.detectChanges();
674+
675+
expect(items[0].focus).not.toHaveBeenCalled();
676+
expect(event.defaultPrevented).toBe(false);
677+
flush();
678+
}));
679+
680+
it('should focus the last item when pressing end', fakeAsync(() => {
681+
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
682+
fixture.detectChanges();
683+
684+
fixture.componentInstance.trigger.openMenu();
685+
fixture.detectChanges();
686+
687+
const panel = overlayContainerElement.querySelector('.mat-menu-panel')!;
688+
const items = Array.from(panel.querySelectorAll('.mat-menu-item')) as HTMLElement[];
689+
items.forEach(patchElementFocus);
690+
691+
spyOn(items[items.length - 1], 'focus').and.callThrough();
692+
693+
const event = dispatchKeyboardEvent(panel, 'keydown', END);
694+
fixture.detectChanges();
695+
696+
expect(items[items.length - 1].focus).toHaveBeenCalled();
697+
expect(event.defaultPrevented).toBe(true);
698+
flush();
699+
}));
700+
701+
it('should not focus the last item when pressing end with a modifier key', fakeAsync(() => {
702+
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
703+
fixture.detectChanges();
704+
705+
fixture.componentInstance.trigger.openMenu();
706+
fixture.detectChanges();
707+
708+
const panel = overlayContainerElement.querySelector('.mat-menu-panel')!;
709+
const items = Array.from(panel.querySelectorAll('.mat-menu-item')) as HTMLElement[];
710+
items.forEach(patchElementFocus);
711+
712+
spyOn(items[items.length - 1], 'focus').and.callThrough();
713+
714+
const event = createKeyboardEvent('keydown', END);
715+
Object.defineProperty(event, 'altKey', {get: () => true});
716+
717+
dispatchEvent(panel, event);
718+
fixture.detectChanges();
719+
720+
expect(items[items.length - 1].focus).not.toHaveBeenCalled();
721+
expect(event.defaultPrevented).toBe(false);
722+
flush();
723+
}));
724+
627725
describe('lazy rendering', () => {
628726
it('should be able to render the menu content lazily', fakeAsync(() => {
629727
const fixture = createComponent(SimpleLazyMenu);

0 commit comments

Comments
 (0)