fix(menu): not handling keyboard events when opened by mouse (#4843)

Fixes the menu keyboard interactions not working when it is opened by a click.

Fixes #4991.
This commit is contained in:
Kristiyan Kostadinov
2017-10-11 01:37:07 +02:00
committed by Andrew Seguin
parent a9600e7ce9
commit d822a3948e
7 changed files with 42 additions and 20 deletions
+2 -2
View File
@@ -97,9 +97,9 @@ describe('menu', () => {
expectFocusOn(page.items(0));
});
it('should not focus the first item when opened with mouse', () => {
it('should focus the panel when opened by mouse', () => {
page.trigger().click();
expectFocusOn(page.trigger());
expectFocusOn(page.menu());
});
it('should focus subsequent items when down arrow is pressed', () => {
+8
View File
@@ -205,6 +205,14 @@ export class MatMenu implements AfterContentInit, MatMenuPanel, OnDestroy {
this._keyManager.setFirstItemActive();
}
/**
* Resets the active item in the menu. This is used when the menu is opened by mouse,
* allowing the user to start from the first option when pressing the down arrow.
*/
resetActiveItem() {
this._keyManager.setActiveItem(-1);
}
/**
* It's necessary to set position-based classes to ensure the menu panel animation
* folds out from the correct direction.
+1
View File
@@ -19,6 +19,7 @@ export interface MatMenuPanel {
parentMenu?: MatMenuPanel | undefined;
direction?: Direction;
focusFirstItem: () => void;
resetActiveItem: () => void;
setPositionClasses: (x: MenuPositionX, y: MenuPositionY) => void;
setElevation?(depth: number): void;
}
+10 -4
View File
@@ -225,10 +225,16 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
this._setMenuElevation();
this._setIsMenuOpen(true);
// Should only set focus if opened via the keyboard, so keyboard users can
// can easily navigate menu items. According to spec, mouse users should not
// see the focus style.
if (!this._openedByMouse) {
// If the menu was opened by mouse, we focus the root node, which allows for the keyboard
// interactions to work. Otherwise, if the menu was opened by keyboard, we focus the first item.
if (this._openedByMouse) {
let rootNode = this._overlayRef!.overlayElement.firstElementChild as HTMLElement;
if (rootNode) {
this.menu.resetActiveItem();
rootNode.focus();
}
} else {
this.menu.focusFirstItem();
}
}
+1
View File
@@ -6,6 +6,7 @@
(click)="close.emit('click')"
[@transformMenu]="_panelAnimationState"
(@transformMenu.done)="_onAnimationDone($event)"
tabindex="-1"
role="menu">
<div class="mat-menu-content" [@fadeInItems]="'showing'">
<ng-content></ng-content>
+1
View File
@@ -14,6 +14,7 @@ $mat-menu-submenu-indicator-size: 10px !default;
@include mat-menu-positions();
max-height: calc(100vh - #{$mat-menu-item-height});
border-radius: $mat-menu-border-radius;
outline: 0;
// Prevent the user from interacting while the panel is still animating.
// This avoids issues where the user could accidentally open a sub-menu,
+19 -14
View File
@@ -33,6 +33,7 @@ import {
dispatchEvent,
createKeyboardEvent,
createMouseEvent,
dispatchFakeEvent,
} from '@angular/cdk/testing';
@@ -191,6 +192,23 @@ describe('MatMenu', () => {
expect(fixture.componentInstance.items.last.getLabel()).toBe('Item with an icon');
});
it('should focus the menu panel root node when it was opened by mouse', () => {
const fixture = TestBed.createComponent(SimpleMenu);
fixture.detectChanges();
const triggerEl = fixture.componentInstance.triggerEl.nativeElement;
dispatchFakeEvent(triggerEl, 'mousedown');
triggerEl.click();
fixture.detectChanges();
const panel = overlayContainerElement.querySelector('.mat-menu-panel');
expect(panel).toBeTruthy('Expected the panel to be rendered.');
expect(document.activeElement).toBe(panel, 'Expected the panel to be focused.');
});
describe('positions', () => {
let fixture: ComponentFixture<PositionedMenu>;
let panel: HTMLElement;
@@ -811,20 +829,6 @@ describe('MatMenu', () => {
.toBe(true, 'Expected focus to be back inside the root menu');
});
it('should not shift focus to the sub-menu when it was opened by hover', () => {
compileTestComponent();
instance.rootTriggerEl.nativeElement.click();
fixture.detectChanges();
const levelOneTrigger = overlay.querySelector('#level-one-trigger')! as HTMLElement;
dispatchMouseEvent(levelOneTrigger, 'mouseenter');
fixture.detectChanges();
expect(overlay.querySelectorAll('.mat-menu-panel')[1].contains(document.activeElement))
.toBe(false, 'Expected focus to not be inside the nested menu');
});
it('should position the sub-menu to the right edge of the trigger in ltr', () => {
compileTestComponent();
instance.rootTriggerEl.nativeElement.style.position = 'fixed';
@@ -1179,6 +1183,7 @@ class CustomMenuPanel implements MatMenuPanel {
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
@Output() close = new EventEmitter<void | 'click' | 'keydown'>();
focusFirstItem = () => {};
resetActiveItem = () => {};
setPositionClasses = () => {};
}