From 0185dd1bcc15876aa4701ae178ea0d0e984bff45 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 22 Feb 2019 16:05:05 +0100 Subject: [PATCH] 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. --- src/lib/menu/menu-directive.ts | 23 +++++++- src/lib/menu/menu.spec.ts | 100 ++++++++++++++++++++++++++++++++- 2 files changed, 119 insertions(+), 4 deletions(-) diff --git a/src/lib/menu/menu-directive.ts b/src/lib/menu/menu-directive.ts index e1803cc09da0..78700a5584ee 100644 --- a/src/lib/menu/menu-directive.ts +++ b/src/lib/menu/menu-directive.ts @@ -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, @@ -268,6 +277,7 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, 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: @@ -283,12 +293,19 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, 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); } } diff --git a/src/lib/menu/menu.spec.ts b/src/lib/menu/menu.spec.ts index ae13b56e76f4..3d047007bad3 100644 --- a/src/lib/menu/menu.spec.ts +++ b/src/lib/menu/menu.spec.ts @@ -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, @@ -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);