Skip to content

Commit 278e25a

Browse files
crisbetojelbourn
authored andcommitted
fix(menu): set appropriate origin when restoring focus (#9303)
Sets the correct focus origin depending on the way a menu has been opened. Fixes #9292.
1 parent 8972bf4 commit 278e25a

File tree

6 files changed

+88
-17
lines changed

6 files changed

+88
-17
lines changed

src/cdk/a11y/focus-monitor.spec.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import {TAB} from '@angular/cdk/keycodes';
2-
import {dispatchFakeEvent, dispatchKeyboardEvent, dispatchMouseEvent} from '@angular/cdk/testing';
2+
import {
3+
dispatchFakeEvent,
4+
dispatchKeyboardEvent,
5+
dispatchMouseEvent,
6+
patchElementFocus,
7+
} from '@angular/cdk/testing';
38
import {Component} from '@angular/core';
49
import {ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
510
import {By} from '@angular/platform-browser';
@@ -413,14 +418,3 @@ class ComplexComponentWithMonitorElementFocus {}
413418
template: `<div tabindex="0" cdkMonitorSubtreeFocus><button></button></div>`
414419
})
415420
class ComplexComponentWithMonitorSubtreeFocus {}
416-
417-
418-
/**
419-
* Patches an elements focus and blur methods to emit events consistently and predictably.
420-
* This is necessary, because some browsers, like IE11, will call the focus handlers asynchronously,
421-
* while others won't fire them at all if the browser window is not focused.
422-
*/
423-
function patchElementFocus(element: HTMLElement) {
424-
element.focus = () => dispatchFakeEvent(element, 'focus');
425-
element.blur = () => dispatchFakeEvent(element, 'blur');
426-
}

src/cdk/testing/element-focus.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {dispatchFakeEvent} from './dispatch-events';
10+
11+
/**
12+
* Patches an elements focus and blur methods to emit events consistently and predictably.
13+
* This is necessary, because some browsers, like IE11, will call the focus handlers asynchronously,
14+
* while others won't fire them at all if the browser window is not focused.
15+
*/
16+
export function patchElementFocus(element: HTMLElement) {
17+
element.focus = () => dispatchFakeEvent(element, 'focus');
18+
element.blur = () => dispatchFakeEvent(element, 'blur');
19+
}

src/cdk/testing/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ export * from './event-objects';
1111
export * from './type-in-element';
1212
export * from './wrapped-error-message';
1313
export * from './mock-ng-zone';
14+
export * from './element-focus';

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';
@@ -21,6 +22,7 @@ import {A11yModule} from '@angular/cdk/a11y';
2122
imports: [
2223
OverlayModule,
2324
CommonModule,
25+
A11yModule,
2426
MatRippleModule,
2527
MatCommonModule,
2628
A11yModule,

src/lib/menu/menu-trigger.ts

Lines changed: 19 additions & 5 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();
@@ -207,9 +210,16 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
207210
this.menu.close.emit();
208211
}
209212

210-
/** Focuses the menu trigger. */
211-
focus() {
212-
this._element.nativeElement.focus();
213+
/**
214+
* Focuses the menu trigger.
215+
* @param origin Source of the menu trigger's focus.
216+
*/
217+
focus(origin: FocusOrigin = 'program') {
218+
if (this._focusMonitor) {
219+
this._focusMonitor.focusVia(this._element.nativeElement, origin);
220+
} else {
221+
this._element.nativeElement.focus();
222+
}
213223
}
214224

215225
/** Closes the menu and does the necessary cleanup. */
@@ -262,8 +272,12 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
262272
// We should reset focus if the user is navigating using a keyboard or
263273
// if we have a top-level trigger which might cause focus to be lost
264274
// when clicking on the backdrop.
265-
if (!this._openedByMouse || !this.triggersSubmenu()) {
275+
if (!this._openedByMouse) {
276+
// Note that the focus style will show up both for `program` and
277+
// `keyboard` so we don't have to specify which one it is.
266278
this.focus();
279+
} else if (!this.triggersSubmenu()) {
280+
this.focus('mouse');
267281
}
268282

269283
this._openedByMouse = false;

src/lib/menu/menu.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ import {
3434
createKeyboardEvent,
3535
createMouseEvent,
3636
dispatchFakeEvent,
37+
patchElementFocus,
3738
} from '@angular/cdk/testing';
3839
import {Subject} from 'rxjs/Subject';
3940
import {ScrollDispatcher} from '@angular/cdk/scrolling';
41+
import {FocusMonitor} from '@angular/cdk/a11y';
4042

4143

4244
describe('MatMenu', () => {
@@ -142,6 +144,45 @@ describe('MatMenu', () => {
142144
expect(document.activeElement).toBe(triggerEl);
143145
}));
144146

147+
it('should set the proper focus origin when restoring focus after opening by keyboard',
148+
fakeAsync(inject([FocusMonitor], (focusMonitor: FocusMonitor) => {
149+
const fixture = TestBed.createComponent(SimpleMenu);
150+
fixture.detectChanges();
151+
const triggerEl = fixture.componentInstance.triggerEl.nativeElement;
152+
153+
patchElementFocus(triggerEl);
154+
focusMonitor.monitor(triggerEl, false);
155+
triggerEl.click(); // A click without a mousedown before it is considered a keyboard open.
156+
fixture.detectChanges();
157+
fixture.componentInstance.trigger.closeMenu();
158+
fixture.detectChanges();
159+
tick(500);
160+
fixture.detectChanges();
161+
162+
expect(triggerEl.classList).toContain('cdk-program-focused');
163+
focusMonitor.stopMonitoring(triggerEl);
164+
})));
165+
166+
it('should set the proper focus origin when restoring focus after opening by mouse',
167+
fakeAsync(inject([FocusMonitor], (focusMonitor: FocusMonitor) => {
168+
const fixture = TestBed.createComponent(SimpleMenu);
169+
fixture.detectChanges();
170+
const triggerEl = fixture.componentInstance.triggerEl.nativeElement;
171+
172+
dispatchFakeEvent(triggerEl, 'mousedown');
173+
triggerEl.click();
174+
fixture.detectChanges();
175+
patchElementFocus(triggerEl);
176+
focusMonitor.monitor(triggerEl, false);
177+
fixture.componentInstance.trigger.closeMenu();
178+
fixture.detectChanges();
179+
tick(500);
180+
fixture.detectChanges();
181+
182+
expect(triggerEl.classList).toContain('cdk-mouse-focused');
183+
focusMonitor.stopMonitoring(triggerEl);
184+
})));
185+
145186
it('should close the menu when pressing ESCAPE', fakeAsync(() => {
146187
const fixture = TestBed.createComponent(SimpleMenu);
147188
fixture.detectChanges();

0 commit comments

Comments
 (0)