fix(menu): proper focus styling when opened by tap on a touch device (#13599)

This commit is contained in:
Kristiyan Kostadinov
2018-10-19 17:16:11 +02:00
committed by Joey Perrott
parent dd96369f52
commit e51de157e0
2 changed files with 62 additions and 7 deletions
+8 -7
View File
@@ -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
+54
View File
@@ -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, [