Skip to content

Commit 408a05f

Browse files
committed
fix(menu): not handling keyboard events when opened by mouse
Fixes the menu keyboard interactions not working when it is opened by a click. Fixes #4991.
1 parent 85a6fff commit 408a05f

File tree

7 files changed

+42
-20
lines changed

7 files changed

+42
-20
lines changed

e2e/components/menu-e2e.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,9 @@ describe('menu', () => {
9696
expectFocusOn(page.items(0));
9797
});
9898

99-
it('should not focus the first item when opened with mouse', () => {
99+
it('should focus the panel when opened by mouse', () => {
100100
page.trigger().click();
101-
expectFocusOn(page.trigger());
101+
expectFocusOn(page.menu());
102102
});
103103

104104
it('should focus subsequent items when down arrow is pressed', () => {

src/lib/menu/menu-directive.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,14 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
193193
this._keyManager.setFirstItemActive();
194194
}
195195

196+
/**
197+
* Resets the active item in the menu. This is used when the menu is opened by mouse,
198+
* allowing the user to start from the first option when pressing the down arrow.
199+
*/
200+
resetActiveItem() {
201+
this._keyManager.setActiveItem(-1);
202+
}
203+
196204
/**
197205
* It's necessary to set position-based classes to ensure the menu panel animation
198206
* folds out from the correct direction.

src/lib/menu/menu-panel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface MdMenuPanel {
1919
parentMenu?: MdMenuPanel | undefined;
2020
direction?: Direction;
2121
focusFirstItem: () => void;
22+
resetActiveItem: () => void;
2223
setPositionClasses: (x: MenuPositionX, y: MenuPositionY) => void;
2324
setElevation?(depth: number): void;
2425
}

src/lib/menu/menu-trigger.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,16 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
239239
this._setMenuElevation();
240240
this._setIsMenuOpen(true);
241241

242-
// Should only set focus if opened via the keyboard, so keyboard users can
243-
// can easily navigate menu items. According to spec, mouse users should not
244-
// see the focus style.
245-
if (!this._openedByMouse) {
242+
// If the menu was opened by mouse, we focus the root node, which allows for the keyboard
243+
// interactions to work. Otherwise, if the menu was opened by keyboard, we focus the first item.
244+
if (this._openedByMouse) {
245+
let rootNode = this._overlayRef!.overlayElement.firstElementChild as HTMLElement;
246+
247+
if (rootNode) {
248+
this.menu.resetActiveItem();
249+
rootNode.focus();
250+
}
251+
} else {
246252
this.menu.focusFirstItem();
247253
}
248254
}

src/lib/menu/menu.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
(click)="close.emit('click')"
77
[@transformMenu]="_panelAnimationState"
88
(@transformMenu.done)="_onAnimationDone($event)"
9+
tabindex="-1"
910
role="menu">
1011
<div class="mat-menu-content" [@fadeInItems]="'showing'">
1112
<ng-content></ng-content>

src/lib/menu/menu.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ $mat-menu-submenu-indicator-size: 10px !default;
1414
@include mat-menu-positions();
1515
max-height: calc(100vh - #{$mat-menu-item-height});
1616
border-radius: $mat-menu-border-radius;
17+
outline: 0;
1718

1819
// Prevent the user from interacting while the panel is still animating.
1920
// This avoids issues where the user could accidentally open a sub-menu,

src/lib/menu/menu.spec.ts

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
dispatchMouseEvent,
3030
dispatchEvent,
3131
createKeyboardEvent,
32+
dispatchFakeEvent,
3233
} from '@angular/cdk/testing';
3334

3435

@@ -173,6 +174,23 @@ describe('MdMenu', () => {
173174
expect(fixture.destroy.bind(fixture)).not.toThrow();
174175
});
175176

177+
it('should focus the menu panel root node when it was opened by mouse', () => {
178+
const fixture = TestBed.createComponent(SimpleMenu);
179+
180+
fixture.detectChanges();
181+
182+
const triggerEl = fixture.componentInstance.triggerEl.nativeElement;
183+
184+
dispatchFakeEvent(triggerEl, 'mousedown');
185+
triggerEl.click();
186+
fixture.detectChanges();
187+
188+
const panel = overlayContainerElement.querySelector('.mat-menu-panel');
189+
190+
expect(panel).toBeTruthy('Expected the panel to be rendered.');
191+
expect(document.activeElement).toBe(panel, 'Expected the panel to be focused.');
192+
});
193+
176194
describe('positions', () => {
177195
let fixture: ComponentFixture<PositionedMenu>;
178196
let panel: HTMLElement;
@@ -770,20 +788,6 @@ describe('MdMenu', () => {
770788
.toBe(true, 'Expected focus to be back inside the root menu');
771789
});
772790

773-
it('should not shift focus to the sub-menu when it was opened by hover', () => {
774-
compileTestComponent();
775-
instance.rootTriggerEl.nativeElement.click();
776-
fixture.detectChanges();
777-
778-
const levelOneTrigger = overlay.querySelector('#level-one-trigger')! as HTMLElement;
779-
780-
dispatchMouseEvent(levelOneTrigger, 'mouseenter');
781-
fixture.detectChanges();
782-
783-
expect(overlay.querySelectorAll('.mat-menu-panel')[1].contains(document.activeElement))
784-
.toBe(false, 'Expected focus to not be inside the nested menu');
785-
});
786-
787791
it('should position the sub-menu to the right edge of the trigger in ltr', () => {
788792
compileTestComponent();
789793
instance.rootTriggerEl.nativeElement.style.position = 'fixed';
@@ -1057,6 +1061,7 @@ class CustomMenuPanel implements MdMenuPanel {
10571061
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
10581062
@Output() close = new EventEmitter<void | 'click' | 'keydown'>();
10591063
focusFirstItem = () => {};
1064+
resetActiveItem = () => {};
10601065
setPositionClasses = () => {};
10611066
}
10621067

0 commit comments

Comments
 (0)