Skip to content

Commit 30cced2

Browse files
committed
fix(menu): set appropriate origin when restoring focus
Sets the correct focus origin depending on the way a menu has been opened. Fixes #9292.
1 parent af44b9d commit 30cced2

File tree

3 files changed

+53
-30
lines changed

3 files changed

+53
-30
lines changed

src/lib/menu/menu-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {NgModule} from '@angular/core';
1010
import {CommonModule} from '@angular/common';
1111
import {MatCommonModule} from '@angular/material/core';
1212
import {OverlayModule} from '@angular/cdk/overlay';
13+
import {A11yModule} from '@angular/cdk/a11y';
1314
import {MatMenu, MAT_MENU_DEFAULT_OPTIONS} from './menu-directive';
1415
import {MatMenuItem} from './menu-item';
1516
import {MatMenuTrigger, MAT_MENU_SCROLL_STRATEGY_PROVIDER} from './menu-trigger';
@@ -20,6 +21,7 @@ import {MatRippleModule} from '@angular/material/core';
2021
imports: [
2122
OverlayModule,
2223
CommonModule,
24+
A11yModule,
2325
MatRippleModule,
2426
MatCommonModule,
2527
],

src/lib/menu/menu-trigger.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {throwMatMenuMissingError} from './menu-errors';
4343
import {MatMenuItem} from './menu-item';
4444
import {MatMenuPanel} from './menu-panel';
4545
import {MenuPositionX, MenuPositionY} from './menu-positions';
46+
import {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y';
4647

4748
/** Injection token that determines the scroll handling while the menu is open. */
4849
export const MAT_MENU_SCROLL_STRATEGY =
@@ -130,7 +131,9 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
130131
@Inject(MAT_MENU_SCROLL_STRATEGY) private _scrollStrategy,
131132
@Optional() private _parentMenu: MatMenu,
132133
@Optional() @Self() private _menuItemInstance: MatMenuItem,
133-
@Optional() private _dir: Directionality) {
134+
@Optional() private _dir: Directionality,
135+
// TODO(crisbeto): make the _focusMonitor required when doing breaking changes.
136+
private _focusMonitor?: FocusMonitor) {
134137

135138
if (_menuItemInstance) {
136139
_menuItemInstance._triggersSubmenu = this.triggersSubmenu();
@@ -208,8 +211,12 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
208211
}
209212

210213
/** Focuses the menu trigger. */
211-
focus() {
212-
this._element.nativeElement.focus();
214+
focus(origin: FocusOrigin = 'program') {
215+
if (this._focusMonitor) {
216+
this._focusMonitor.focusVia(this._element.nativeElement, origin);
217+
} else {
218+
this._element.nativeElement.focus();
219+
}
213220
}
214221

215222
/** Closes the menu and does the necessary cleanup. */
@@ -274,8 +281,10 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
274281
// We should reset focus if the user is navigating using a keyboard or
275282
// if we have a top-level trigger which might cause focus to be lost
276283
// when clicking on the backdrop.
277-
if (!this._openedByMouse || !this.triggersSubmenu()) {
284+
if (!this._openedByMouse) {
278285
this.focus();
286+
} else if (!this.triggersSubmenu()) {
287+
this.focus('mouse');
279288
}
280289

281290
this._openedByMouse = false;

src/lib/menu/menu.spec.ts

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
} from '@angular/cdk/testing';
3838
import {Subject} from 'rxjs/Subject';
3939
import {ScrollDispatcher} from '@angular/cdk/scrolling';
40+
import {FocusMonitor} from '@angular/cdk/a11y';
4041

4142

4243
describe('MatMenu', () => {
@@ -105,42 +106,53 @@ describe('MatMenu', () => {
105106
expect(overlayContainerElement.textContent).toBe('');
106107
}));
107108

108-
it('should restore focus to the trigger when the menu was opened by keyboard', fakeAsync(() => {
109-
const fixture = TestBed.createComponent(SimpleMenu);
110-
fixture.detectChanges();
109+
it('should restore focus to the trigger when the menu was opened by keyboard',
110+
fakeAsync(inject([FocusMonitor], (focusMonitor: FocusMonitor) => {
111+
const fixture = TestBed.createComponent(SimpleMenu);
112+
fixture.detectChanges();
111113

112-
const triggerEl = fixture.componentInstance.triggerEl.nativeElement;
114+
const triggerEl = fixture.componentInstance.triggerEl.nativeElement;
113115

114-
// A click without a mousedown before it is considered a keyboard open.
115-
triggerEl.click();
116-
fixture.detectChanges();
116+
// A click without a mousedown before it is considered a keyboard open.
117+
focusMonitor.monitor(triggerEl, false);
118+
triggerEl.click();
119+
fixture.detectChanges();
117120

118-
expect(overlayContainerElement.querySelector('.mat-menu-panel')).toBeTruthy();
121+
expect(overlayContainerElement.querySelector('.mat-menu-panel')).toBeTruthy();
119122

120-
fixture.componentInstance.trigger.closeMenu();
121-
fixture.detectChanges();
122-
tick(500);
123+
fixture.componentInstance.trigger.closeMenu();
124+
fixture.detectChanges();
125+
tick(500);
126+
fixture.detectChanges();
123127

124-
expect(document.activeElement).toBe(triggerEl);
125-
}));
128+
expect(document.activeElement).toBe(triggerEl);
129+
expect(triggerEl.classList).toContain('cdk-program-focused');
130+
focusMonitor.stopMonitoring(triggerEl);
131+
})));
126132

127-
it('should restore focus to the root trigger when the menu was opened by mouse', fakeAsync(() => {
128-
const fixture = TestBed.createComponent(SimpleMenu);
129-
fixture.detectChanges();
133+
it('should restore focus to the root trigger when the menu was opened by mouse',
134+
fakeAsync(inject([FocusMonitor], (focusMonitor: FocusMonitor) => {
135+
const fixture = TestBed.createComponent(SimpleMenu);
136+
fixture.detectChanges();
130137

131-
const triggerEl = fixture.componentInstance.triggerEl.nativeElement;
132-
dispatchFakeEvent(triggerEl, 'mousedown');
133-
triggerEl.click();
134-
fixture.detectChanges();
138+
const triggerEl = fixture.componentInstance.triggerEl.nativeElement;
135139

136-
expect(overlayContainerElement.querySelector('.mat-menu-panel')).toBeTruthy();
140+
dispatchFakeEvent(triggerEl, 'mousedown');
141+
triggerEl.click();
142+
fixture.detectChanges();
137143

138-
fixture.componentInstance.trigger.closeMenu();
139-
fixture.detectChanges();
140-
tick(500);
144+
expect(overlayContainerElement.querySelector('.mat-menu-panel')).toBeTruthy();
141145

142-
expect(document.activeElement).toBe(triggerEl);
143-
}));
146+
focusMonitor.monitor(triggerEl, false);
147+
fixture.componentInstance.trigger.closeMenu();
148+
fixture.detectChanges();
149+
tick(500);
150+
fixture.detectChanges();
151+
152+
expect(document.activeElement).toBe(triggerEl);
153+
expect(triggerEl.classList).toContain('cdk-mouse-focused');
154+
focusMonitor.stopMonitoring(triggerEl);
155+
})));
144156

145157
it('should close the menu when pressing ESCAPE', fakeAsync(() => {
146158
const fixture = TestBed.createComponent(SimpleMenu);

0 commit comments

Comments
 (0)