Skip to content

Commit b085dc6

Browse files
crisbetojelbourn
authored andcommitted
fix(menu): return focus to root trigger when closed by mouse (#8348)
Previously we only restored focus to the trigger if the user opened the menu using a keyboard, however that means that focus can be lost when closing by clicking on the backdrop. These changes switch to also restoring focus when clicking away, but only if the trigger is a top-level trigger. Fixes #8290.
1 parent b0756a2 commit b085dc6

File tree

2 files changed

+63
-3
lines changed

2 files changed

+63
-3
lines changed

src/lib/menu/menu-trigger.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,10 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
273273
private _resetMenu(): void {
274274
this._setIsMenuOpen(false);
275275

276-
// Focus only needs to be reset to the host element if the menu was opened
277-
// by the keyboard and manually shifted to the first menu item.
278-
if (!this._openedByMouse) {
276+
// We should reset focus if the user is navigating using a keyboard or
277+
// if we have a top-level trigger which might cause focus to be lost
278+
// when clicking on the backdrop.
279+
if (!this._openedByMouse || !this.triggersSubmenu()) {
279280
this.focus();
280281
}
281282

src/lib/menu/menu.spec.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,43 @@ describe('MatMenu', () => {
104104
expect(overlayContainerElement.textContent).toBe('');
105105
}));
106106

107+
it('should restore focus to the trigger when the menu was opened by keyboard', fakeAsync(() => {
108+
const fixture = TestBed.createComponent(SimpleMenu);
109+
fixture.detectChanges();
110+
111+
const triggerEl = fixture.componentInstance.triggerEl.nativeElement;
112+
113+
// A click without a mousedown before it is considered a keyboard open.
114+
triggerEl.click();
115+
fixture.detectChanges();
116+
117+
expect(overlayContainerElement.querySelector('.mat-menu-panel')).toBeTruthy();
118+
119+
fixture.componentInstance.trigger.closeMenu();
120+
fixture.detectChanges();
121+
tick(500);
122+
123+
expect(document.activeElement).toBe(triggerEl);
124+
}));
125+
126+
it('should restore focus to the root trigger when the menu was opened by mouse', fakeAsync(() => {
127+
const fixture = TestBed.createComponent(SimpleMenu);
128+
fixture.detectChanges();
129+
130+
const triggerEl = fixture.componentInstance.triggerEl.nativeElement;
131+
dispatchFakeEvent(triggerEl, 'mousedown');
132+
triggerEl.click();
133+
fixture.detectChanges();
134+
135+
expect(overlayContainerElement.querySelector('.mat-menu-panel')).toBeTruthy();
136+
137+
fixture.componentInstance.trigger.closeMenu();
138+
fixture.detectChanges();
139+
tick(500);
140+
141+
expect(document.activeElement).toBe(triggerEl);
142+
}));
143+
107144
it('should close the menu when pressing ESCAPE', fakeAsync(() => {
108145
const fixture = TestBed.createComponent(SimpleMenu);
109146
fixture.detectChanges();
@@ -1096,6 +1133,28 @@ describe('MatMenu', () => {
10961133
expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(2, 'Expected two open menus');
10971134
}));
10981135

1136+
it('should not re-focus a child menu trigger when hovering another trigger', fakeAsync(() => {
1137+
compileTestComponent();
1138+
1139+
dispatchFakeEvent(instance.rootTriggerEl.nativeElement, 'mousedown');
1140+
instance.rootTriggerEl.nativeElement.click();
1141+
fixture.detectChanges();
1142+
1143+
const items = Array.from(overlay.querySelectorAll('.mat-menu-panel [mat-menu-item]'));
1144+
const levelOneTrigger = overlay.querySelector('#level-one-trigger')!;
1145+
1146+
dispatchMouseEvent(levelOneTrigger, 'mouseenter');
1147+
fixture.detectChanges();
1148+
expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(2, 'Expected two open menus');
1149+
1150+
dispatchMouseEvent(items[items.indexOf(levelOneTrigger) + 1], 'mouseenter');
1151+
fixture.detectChanges();
1152+
tick(500);
1153+
1154+
expect(document.activeElement)
1155+
.not.toBe(levelOneTrigger, 'Expected focus not to be returned to the initial trigger.');
1156+
}));
1157+
10991158
});
11001159

11011160
});

0 commit comments

Comments
 (0)