Skip to content

Commit

Permalink
fix(menu): support focus first/last item via home/end keys (#14896)
Browse files Browse the repository at this point in the history
Adds support for jumping to the first/last item using the home/end keys.
  • Loading branch information
crisbeto authored and jelbourn committed Feb 22, 2019
1 parent 677db8c commit 0185dd1
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 4 deletions.
23 changes: 20 additions & 3 deletions src/lib/menu/menu-directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@
import {FocusKeyManager, FocusOrigin} from '@angular/cdk/a11y';
import {Direction} from '@angular/cdk/bidi';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {ESCAPE, LEFT_ARROW, RIGHT_ARROW, DOWN_ARROW, UP_ARROW} from '@angular/cdk/keycodes';
import {
ESCAPE,
LEFT_ARROW,
RIGHT_ARROW,
DOWN_ARROW,
UP_ARROW,
HOME,
END,
hasModifierKey,
} from '@angular/cdk/keycodes';
import {
AfterContentInit,
ChangeDetectionStrategy,
Expand Down Expand Up @@ -268,6 +277,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnI
/** Handle a keyboard event from the menu, delegating to the appropriate action. */
_handleKeydown(event: KeyboardEvent) {
const keyCode = event.keyCode;
const manager = this._keyManager;

switch (keyCode) {
case ESCAPE:
Expand All @@ -283,12 +293,19 @@ export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnI
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) {
this._keyManager.setFocusOrigin('keyboard');
manager.setFocusOrigin('keyboard');
}

this._keyManager.onKeydown(event);
manager.onKeydown(event);
}
}

Expand Down
100 changes: 99 additions & 1 deletion src/lib/menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from '@angular/core';
import {Direction, Directionality} from '@angular/cdk/bidi';
import {OverlayContainer, Overlay} from '@angular/cdk/overlay';
import {ESCAPE, LEFT_ARROW, RIGHT_ARROW, DOWN_ARROW, TAB} from '@angular/cdk/keycodes';
import {ESCAPE, LEFT_ARROW, RIGHT_ARROW, DOWN_ARROW, TAB, HOME, END} from '@angular/cdk/keycodes';
import {
MAT_MENU_DEFAULT_OPTIONS,
MatMenu,
Expand Down Expand Up @@ -624,6 +624,104 @@ describe('MatMenu', () => {
expect(overlayContainerElement.textContent).toBe('');
}));

it('should focus the first item when pressing home', fakeAsync(() => {
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
fixture.detectChanges();

fixture.componentInstance.trigger.openMenu();
fixture.detectChanges();

const panel = overlayContainerElement.querySelector('.mat-menu-panel')!;
const items = Array.from(panel.querySelectorAll('.mat-menu-item')) as HTMLElement[];
items.forEach(patchElementFocus);

// Focus the last item since focus starts from the first one.
items[items.length - 1].focus();
fixture.detectChanges();

spyOn(items[0], 'focus').and.callThrough();

const event = dispatchKeyboardEvent(panel, 'keydown', HOME);
fixture.detectChanges();

expect(items[0].focus).toHaveBeenCalled();
expect(event.defaultPrevented).toBe(true);
flush();
}));

it('should not focus the first item when pressing home with a modifier key', fakeAsync(() => {
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
fixture.detectChanges();

fixture.componentInstance.trigger.openMenu();
fixture.detectChanges();

const panel = overlayContainerElement.querySelector('.mat-menu-panel')!;
const items = Array.from(panel.querySelectorAll('.mat-menu-item')) as HTMLElement[];
items.forEach(patchElementFocus);

// Focus the last item since focus starts from the first one.
items[items.length - 1].focus();
fixture.detectChanges();

spyOn(items[0], 'focus').and.callThrough();

const event = createKeyboardEvent('keydown', HOME);
Object.defineProperty(event, 'altKey', {get: () => true});

dispatchEvent(panel, event);
fixture.detectChanges();

expect(items[0].focus).not.toHaveBeenCalled();
expect(event.defaultPrevented).toBe(false);
flush();
}));

it('should focus the last item when pressing end', fakeAsync(() => {
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
fixture.detectChanges();

fixture.componentInstance.trigger.openMenu();
fixture.detectChanges();

const panel = overlayContainerElement.querySelector('.mat-menu-panel')!;
const items = Array.from(panel.querySelectorAll('.mat-menu-item')) as HTMLElement[];
items.forEach(patchElementFocus);

spyOn(items[items.length - 1], 'focus').and.callThrough();

const event = dispatchKeyboardEvent(panel, 'keydown', END);
fixture.detectChanges();

expect(items[items.length - 1].focus).toHaveBeenCalled();
expect(event.defaultPrevented).toBe(true);
flush();
}));

it('should not focus the last item when pressing end with a modifier key', fakeAsync(() => {
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
fixture.detectChanges();

fixture.componentInstance.trigger.openMenu();
fixture.detectChanges();

const panel = overlayContainerElement.querySelector('.mat-menu-panel')!;
const items = Array.from(panel.querySelectorAll('.mat-menu-item')) as HTMLElement[];
items.forEach(patchElementFocus);

spyOn(items[items.length - 1], 'focus').and.callThrough();

const event = createKeyboardEvent('keydown', END);
Object.defineProperty(event, 'altKey', {get: () => true});

dispatchEvent(panel, event);
fixture.detectChanges();

expect(items[items.length - 1].focus).not.toHaveBeenCalled();
expect(event.defaultPrevented).toBe(false);
flush();
}));

describe('lazy rendering', () => {
it('should be able to render the menu content lazily', fakeAsync(() => {
const fixture = createComponent(SimpleLazyMenu);
Expand Down

0 comments on commit 0185dd1

Please sign in to comment.