mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(menu): proper focus styling when opened by tap on a touch device (#13599)
This commit is contained in:
committed by
Joey Perrott
parent
dd96369f52
commit
e51de157e0
@@ -72,6 +72,7 @@ export const MENU_PANEL_TOP_PADDING = 8;
|
||||
'aria-haspopup': 'true',
|
||||
'[attr.aria-expanded]': 'menuOpen || null',
|
||||
'(mousedown)': '_handleMousedown($event)',
|
||||
'(touchstart)': '_openedBy = "touch"',
|
||||
'(keydown)': '_handleKeydown($event)',
|
||||
'(click)': '_handleClick($event)',
|
||||
},
|
||||
@@ -87,7 +88,7 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
|
||||
|
||||
// Tracking input type is necessary so it's possible to only auto-focus
|
||||
// the first item of the list when the menu is opened via the keyboard
|
||||
private _openedByMouse: boolean = false;
|
||||
_openedBy: 'mouse' | 'touch' | null = null;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
@@ -281,7 +282,7 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
|
||||
this.menu.direction = this.dir;
|
||||
this._setMenuElevation();
|
||||
this._setIsMenuOpen(true);
|
||||
this.menu.focusFirstItem(this._openedByMouse ? 'mouse' : 'program');
|
||||
this.menu.focusFirstItem(this._openedBy || 'program');
|
||||
}
|
||||
|
||||
/** Updates the menu elevation based on the amount of parent menus that it has. */
|
||||
@@ -309,15 +310,15 @@ 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) {
|
||||
if (!this._openedBy) {
|
||||
// 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.focus(this._openedBy);
|
||||
}
|
||||
|
||||
this._openedByMouse = false;
|
||||
this._openedBy = null;
|
||||
}
|
||||
|
||||
// set state rather than toggle to support triggers sharing a menu
|
||||
@@ -464,7 +465,7 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
|
||||
if (!isFakeMousedownFromScreenReader(event)) {
|
||||
// Since right or middle button clicks won't trigger the `click` event,
|
||||
// we shouldn't consider the menu as opened by mouse in those cases.
|
||||
this._openedByMouse = event.button === 0;
|
||||
this._openedBy = event.button === 0 ? 'mouse' : null;
|
||||
|
||||
// Since clicking on the trigger won't close the menu if it opens a sub-menu,
|
||||
// we should prevent focus from moving onto it via click to avoid the
|
||||
@@ -513,7 +514,7 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
|
||||
delay(0, asapScheduler)
|
||||
)
|
||||
.subscribe(() => {
|
||||
this._openedByMouse = true;
|
||||
this._openedBy = 'mouse';
|
||||
|
||||
// If the same menu is used between multiple triggers, it might still be animating
|
||||
// while the new trigger tries to re-open it. Wait for the animation to finish
|
||||
|
||||
@@ -164,6 +164,24 @@ describe('MatMenu', () => {
|
||||
expect(document.activeElement).toBe(triggerEl);
|
||||
}));
|
||||
|
||||
it('should restore focus to the root trigger when the menu was opened by touch', fakeAsync(() => {
|
||||
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
|
||||
fixture.detectChanges();
|
||||
|
||||
const triggerEl = fixture.componentInstance.triggerEl.nativeElement;
|
||||
dispatchFakeEvent(triggerEl, 'touchstart');
|
||||
triggerEl.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(overlayContainerElement.querySelector('.mat-menu-panel')).toBeTruthy();
|
||||
|
||||
fixture.componentInstance.trigger.closeMenu();
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
expect(document.activeElement).toBe(triggerEl);
|
||||
}));
|
||||
|
||||
it('should scroll the panel to the top on open, when it is scrollable', fakeAsync(() => {
|
||||
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
|
||||
fixture.detectChanges();
|
||||
@@ -247,6 +265,27 @@ describe('MatMenu', () => {
|
||||
focusMonitor.stopMonitoring(triggerEl);
|
||||
}));
|
||||
|
||||
it('should set the proper focus origin when restoring focus after opening by touch',
|
||||
fakeAsync(() => {
|
||||
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
|
||||
fixture.detectChanges();
|
||||
const triggerEl = fixture.componentInstance.triggerEl.nativeElement;
|
||||
|
||||
dispatchMouseEvent(triggerEl, 'touchstart');
|
||||
triggerEl.click();
|
||||
fixture.detectChanges();
|
||||
patchElementFocus(triggerEl);
|
||||
focusMonitor.monitor(triggerEl, false);
|
||||
fixture.componentInstance.trigger.closeMenu();
|
||||
fixture.detectChanges();
|
||||
tick(500);
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
expect(triggerEl.classList).toContain('cdk-touch-focused');
|
||||
focusMonitor.stopMonitoring(triggerEl);
|
||||
}));
|
||||
|
||||
it('should close the menu when pressing ESCAPE', fakeAsync(() => {
|
||||
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
|
||||
fixture.detectChanges();
|
||||
@@ -378,6 +417,21 @@ describe('MatMenu', () => {
|
||||
expect(fixture.componentInstance.items.first.focus).toHaveBeenCalledWith('mouse');
|
||||
}));
|
||||
|
||||
it('should set the proper focus origin when opening by touch', fakeAsync(() => {
|
||||
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
|
||||
fixture.detectChanges();
|
||||
spyOn(fixture.componentInstance.items.first, 'focus').and.callThrough();
|
||||
|
||||
const triggerEl = fixture.componentInstance.triggerEl.nativeElement;
|
||||
|
||||
dispatchMouseEvent(triggerEl, 'touchstart');
|
||||
triggerEl.click();
|
||||
fixture.detectChanges();
|
||||
flush();
|
||||
|
||||
expect(fixture.componentInstance.items.first.focus).toHaveBeenCalledWith('touch');
|
||||
}));
|
||||
|
||||
it('should close the menu when using the CloseScrollStrategy', fakeAsync(() => {
|
||||
const scrolledSubject = new Subject();
|
||||
const fixture = createComponent(SimpleMenu, [
|
||||
|
||||
Reference in New Issue
Block a user