mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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.
This commit is contained in:
committed by
Jeremy Elbourn
parent
8972bf4e79
commit
278e25ac04
@@ -1,5 +1,10 @@
|
||||
import {TAB} from '@angular/cdk/keycodes';
|
||||
import {dispatchFakeEvent, dispatchKeyboardEvent, dispatchMouseEvent} from '@angular/cdk/testing';
|
||||
import {
|
||||
dispatchFakeEvent,
|
||||
dispatchKeyboardEvent,
|
||||
dispatchMouseEvent,
|
||||
patchElementFocus,
|
||||
} from '@angular/cdk/testing';
|
||||
import {Component} from '@angular/core';
|
||||
import {ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
|
||||
import {By} from '@angular/platform-browser';
|
||||
@@ -413,14 +418,3 @@ class ComplexComponentWithMonitorElementFocus {}
|
||||
template: `<div tabindex="0" cdkMonitorSubtreeFocus><button></button></div>`
|
||||
})
|
||||
class ComplexComponentWithMonitorSubtreeFocus {}
|
||||
|
||||
|
||||
/**
|
||||
* Patches an elements focus and blur methods to emit events consistently and predictably.
|
||||
* This is necessary, because some browsers, like IE11, will call the focus handlers asynchronously,
|
||||
* while others won't fire them at all if the browser window is not focused.
|
||||
*/
|
||||
function patchElementFocus(element: HTMLElement) {
|
||||
element.focus = () => dispatchFakeEvent(element, 'focus');
|
||||
element.blur = () => dispatchFakeEvent(element, 'blur');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {dispatchFakeEvent} from './dispatch-events';
|
||||
|
||||
/**
|
||||
* Patches an elements focus and blur methods to emit events consistently and predictably.
|
||||
* This is necessary, because some browsers, like IE11, will call the focus handlers asynchronously,
|
||||
* while others won't fire them at all if the browser window is not focused.
|
||||
*/
|
||||
export function patchElementFocus(element: HTMLElement) {
|
||||
element.focus = () => dispatchFakeEvent(element, 'focus');
|
||||
element.blur = () => dispatchFakeEvent(element, 'blur');
|
||||
}
|
||||
@@ -11,3 +11,4 @@ export * from './event-objects';
|
||||
export * from './type-in-element';
|
||||
export * from './wrapped-error-message';
|
||||
export * from './mock-ng-zone';
|
||||
export * from './element-focus';
|
||||
|
||||
@@ -10,6 +10,7 @@ import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {MatCommonModule} from '@angular/material/core';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {A11yModule} from '@angular/cdk/a11y';
|
||||
import {MatMenu, MAT_MENU_DEFAULT_OPTIONS} from './menu-directive';
|
||||
import {MatMenuItem} from './menu-item';
|
||||
import {MatMenuTrigger, MAT_MENU_SCROLL_STRATEGY_PROVIDER} from './menu-trigger';
|
||||
@@ -21,6 +22,7 @@ import {A11yModule} from '@angular/cdk/a11y';
|
||||
imports: [
|
||||
OverlayModule,
|
||||
CommonModule,
|
||||
A11yModule,
|
||||
MatRippleModule,
|
||||
MatCommonModule,
|
||||
A11yModule,
|
||||
|
||||
@@ -43,6 +43,7 @@ import {throwMatMenuMissingError} from './menu-errors';
|
||||
import {MatMenuItem} from './menu-item';
|
||||
import {MatMenuPanel} from './menu-panel';
|
||||
import {MenuPositionX, MenuPositionY} from './menu-positions';
|
||||
import {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y';
|
||||
|
||||
/** Injection token that determines the scroll handling while the menu is open. */
|
||||
export const MAT_MENU_SCROLL_STRATEGY =
|
||||
@@ -130,7 +131,9 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
|
||||
@Inject(MAT_MENU_SCROLL_STRATEGY) private _scrollStrategy,
|
||||
@Optional() private _parentMenu: MatMenu,
|
||||
@Optional() @Self() private _menuItemInstance: MatMenuItem,
|
||||
@Optional() private _dir: Directionality) {
|
||||
@Optional() private _dir: Directionality,
|
||||
// TODO(crisbeto): make the _focusMonitor required when doing breaking changes.
|
||||
private _focusMonitor?: FocusMonitor) {
|
||||
|
||||
if (_menuItemInstance) {
|
||||
_menuItemInstance._triggersSubmenu = this.triggersSubmenu();
|
||||
@@ -207,9 +210,16 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
|
||||
this.menu.close.emit();
|
||||
}
|
||||
|
||||
/** Focuses the menu trigger. */
|
||||
focus() {
|
||||
this._element.nativeElement.focus();
|
||||
/**
|
||||
* Focuses the menu trigger.
|
||||
* @param origin Source of the menu trigger's focus.
|
||||
*/
|
||||
focus(origin: FocusOrigin = 'program') {
|
||||
if (this._focusMonitor) {
|
||||
this._focusMonitor.focusVia(this._element.nativeElement, origin);
|
||||
} else {
|
||||
this._element.nativeElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/** Closes the menu and does the necessary cleanup. */
|
||||
@@ -262,8 +272,12 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
|
||||
// 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()) {
|
||||
if (!this._openedByMouse) {
|
||||
// Note that the focus style will show up both for `program` and
|
||||
// `keyboard` so we don't have to specify which one it is.
|
||||
this.focus();
|
||||
} else if (!this.triggersSubmenu()) {
|
||||
this.focus('mouse');
|
||||
}
|
||||
|
||||
this._openedByMouse = false;
|
||||
|
||||
@@ -34,9 +34,11 @@ import {
|
||||
createKeyboardEvent,
|
||||
createMouseEvent,
|
||||
dispatchFakeEvent,
|
||||
patchElementFocus,
|
||||
} from '@angular/cdk/testing';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import {ScrollDispatcher} from '@angular/cdk/scrolling';
|
||||
import {FocusMonitor} from '@angular/cdk/a11y';
|
||||
|
||||
|
||||
describe('MatMenu', () => {
|
||||
@@ -142,6 +144,45 @@ describe('MatMenu', () => {
|
||||
expect(document.activeElement).toBe(triggerEl);
|
||||
}));
|
||||
|
||||
it('should set the proper focus origin when restoring focus after opening by keyboard',
|
||||
fakeAsync(inject([FocusMonitor], (focusMonitor: FocusMonitor) => {
|
||||
const fixture = TestBed.createComponent(SimpleMenu);
|
||||
fixture.detectChanges();
|
||||
const triggerEl = fixture.componentInstance.triggerEl.nativeElement;
|
||||
|
||||
patchElementFocus(triggerEl);
|
||||
focusMonitor.monitor(triggerEl, false);
|
||||
triggerEl.click(); // A click without a mousedown before it is considered a keyboard open.
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.trigger.closeMenu();
|
||||
fixture.detectChanges();
|
||||
tick(500);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(triggerEl.classList).toContain('cdk-program-focused');
|
||||
focusMonitor.stopMonitoring(triggerEl);
|
||||
})));
|
||||
|
||||
it('should set the proper focus origin when restoring focus after opening by mouse',
|
||||
fakeAsync(inject([FocusMonitor], (focusMonitor: FocusMonitor) => {
|
||||
const fixture = TestBed.createComponent(SimpleMenu);
|
||||
fixture.detectChanges();
|
||||
const triggerEl = fixture.componentInstance.triggerEl.nativeElement;
|
||||
|
||||
dispatchFakeEvent(triggerEl, 'mousedown');
|
||||
triggerEl.click();
|
||||
fixture.detectChanges();
|
||||
patchElementFocus(triggerEl);
|
||||
focusMonitor.monitor(triggerEl, false);
|
||||
fixture.componentInstance.trigger.closeMenu();
|
||||
fixture.detectChanges();
|
||||
tick(500);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(triggerEl.classList).toContain('cdk-mouse-focused');
|
||||
focusMonitor.stopMonitoring(triggerEl);
|
||||
})));
|
||||
|
||||
it('should close the menu when pressing ESCAPE', fakeAsync(() => {
|
||||
const fixture = TestBed.createComponent(SimpleMenu);
|
||||
fixture.detectChanges();
|
||||
|
||||
Reference in New Issue
Block a user