diff --git a/src/lib/menu/menu-trigger.ts b/src/lib/menu/menu-trigger.ts index cb2e4fc85b2b..303bf557329d 100644 --- a/src/lib/menu/menu-trigger.ts +++ b/src/lib/menu/menu-trigger.ts @@ -273,9 +273,10 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy { private _resetMenu(): void { this._setIsMenuOpen(false); - // Focus only needs to be reset to the host element if the menu was opened - // by the keyboard and manually shifted to the first menu item. - if (!this._openedByMouse) { + // We should reset focus if the user is navigating using a keyboard or + // if we have a top-level trigger which might cause focus to be lost + // when clicking on the backdrop. + if (!this._openedByMouse || !this.triggersSubmenu()) { this.focus(); } diff --git a/src/lib/menu/menu.spec.ts b/src/lib/menu/menu.spec.ts index 082257508200..26ed9299419d 100644 --- a/src/lib/menu/menu.spec.ts +++ b/src/lib/menu/menu.spec.ts @@ -104,6 +104,43 @@ describe('MatMenu', () => { expect(overlayContainerElement.textContent).toBe(''); })); + it('should restore focus to the trigger when the menu was opened by keyboard', fakeAsync(() => { + const fixture = TestBed.createComponent(SimpleMenu); + fixture.detectChanges(); + + const triggerEl = fixture.componentInstance.triggerEl.nativeElement; + + // A click without a mousedown before it is considered a keyboard open. + triggerEl.click(); + fixture.detectChanges(); + + expect(overlayContainerElement.querySelector('.mat-menu-panel')).toBeTruthy(); + + fixture.componentInstance.trigger.closeMenu(); + fixture.detectChanges(); + tick(500); + + expect(document.activeElement).toBe(triggerEl); + })); + + it('should restore focus to the root trigger when the menu was opened by mouse', fakeAsync(() => { + const fixture = TestBed.createComponent(SimpleMenu); + fixture.detectChanges(); + + const triggerEl = fixture.componentInstance.triggerEl.nativeElement; + dispatchFakeEvent(triggerEl, 'mousedown'); + triggerEl.click(); + fixture.detectChanges(); + + expect(overlayContainerElement.querySelector('.mat-menu-panel')).toBeTruthy(); + + fixture.componentInstance.trigger.closeMenu(); + fixture.detectChanges(); + tick(500); + + expect(document.activeElement).toBe(triggerEl); + })); + it('should close the menu when pressing ESCAPE', fakeAsync(() => { const fixture = TestBed.createComponent(SimpleMenu); fixture.detectChanges(); @@ -1082,6 +1119,28 @@ describe('MatMenu', () => { expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(2, 'Expected two open menus'); })); + it('should not re-focus a child menu trigger when hovering another trigger', fakeAsync(() => { + compileTestComponent(); + + dispatchFakeEvent(instance.rootTriggerEl.nativeElement, 'mousedown'); + instance.rootTriggerEl.nativeElement.click(); + fixture.detectChanges(); + + const items = Array.from(overlay.querySelectorAll('.mat-menu-panel [mat-menu-item]')); + const levelOneTrigger = overlay.querySelector('#level-one-trigger')!; + + dispatchMouseEvent(levelOneTrigger, 'mouseenter'); + fixture.detectChanges(); + expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(2, 'Expected two open menus'); + + dispatchMouseEvent(items[items.indexOf(levelOneTrigger) + 1], 'mouseenter'); + fixture.detectChanges(); + tick(500); + + expect(document.activeElement) + .not.toBe(levelOneTrigger, 'Expected focus not to be returned to the initial trigger.'); + })); + }); });