Skip to content

Commit 140c94e

Browse files
crisbetojelbourn
authored andcommitted
fix(menu): closed nested menu chain when tabbing away (#7750)
Currently when tabbing inside a sub-menu, only the sub-menu is closed which restores focus to its parent. This is wrong, because it ends up sending the user back in the tab order. These changes close the whole chain of nested menus instead. For referece: https://www.w3.org/TR/wai-aria-practices-1.1/#menu
1 parent 8830486 commit 140c94e

File tree

4 files changed

+28
-6
lines changed

4 files changed

+28
-6
lines changed

src/lib/menu/menu-directive.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,8 @@ export class MatMenu implements OnInit, AfterContentInit, MatMenuPanel, OnDestro
196196
set classList(classes: string) { this.panelClass = classes; }
197197

198198
/** Event emitted when the menu is closed. */
199-
@Output() readonly closed: EventEmitter<void | 'click' | 'keydown'> =
200-
new EventEmitter<void | 'click' | 'keydown'>();
199+
@Output() readonly closed: EventEmitter<void | 'click' | 'keydown' | 'tab'> =
200+
new EventEmitter<void | 'click' | 'keydown' | 'tab'>();
201201

202202
/**
203203
* Event emitted when the menu is closed.
@@ -217,7 +217,7 @@ export class MatMenu implements OnInit, AfterContentInit, MatMenuPanel, OnDestro
217217

218218
ngAfterContentInit() {
219219
this._keyManager = new FocusKeyManager<MatMenuItem>(this.items).withWrap().withTypeAhead();
220-
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.close.emit('keydown'));
220+
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.close.emit('tab'));
221221
}
222222

223223
ngOnDestroy() {

src/lib/menu/menu-panel.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export interface MatMenuPanel {
2121
yPosition: MenuPositionY;
2222
overlapTrigger: boolean;
2323
templateRef: TemplateRef<any>;
24-
close: EventEmitter<void | 'click' | 'keydown'>;
24+
close: EventEmitter<void | 'click' | 'keydown' | 'tab'>;
2525
parentMenu?: MatMenuPanel | undefined;
2626
direction?: Direction;
2727
focusFirstItem: (origin?: FocusOrigin) => void;

src/lib/menu/menu-trigger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
155155
this._destroyMenu();
156156

157157
// If a click closed the menu, we should close the entire chain of nested menus.
158-
if (reason === 'click' && this._parentMenu) {
158+
if ((reason === 'click' || reason === 'tab') && this._parentMenu) {
159159
this._parentMenu.closed.emit(reason);
160160
}
161161
});

src/lib/menu/menu.spec.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
} from '@angular/core';
1616
import {Direction, Directionality} from '@angular/cdk/bidi';
1717
import {OverlayContainer, Overlay} from '@angular/cdk/overlay';
18-
import {ESCAPE, LEFT_ARROW, RIGHT_ARROW} from '@angular/cdk/keycodes';
18+
import {ESCAPE, LEFT_ARROW, RIGHT_ARROW, TAB} from '@angular/cdk/keycodes';
1919
import {
2020
MAT_MENU_DEFAULT_OPTIONS,
2121
MatMenu,
@@ -1168,6 +1168,28 @@ describe('MatMenu', () => {
11681168
expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(0, 'Expected no open menus');
11691169
}));
11701170

1171+
it('should close all of the menus when the user tabs away', fakeAsync(() => {
1172+
compileTestComponent();
1173+
instance.rootTriggerEl.nativeElement.click();
1174+
fixture.detectChanges();
1175+
1176+
instance.levelOneTrigger.openMenu();
1177+
fixture.detectChanges();
1178+
1179+
instance.levelTwoTrigger.openMenu();
1180+
fixture.detectChanges();
1181+
1182+
const menus = overlay.querySelectorAll('.mat-menu-panel');
1183+
1184+
expect(menus.length).toBe(3, 'Expected three open menus');
1185+
1186+
dispatchKeyboardEvent(menus[menus.length - 1], 'keydown', TAB);
1187+
fixture.detectChanges();
1188+
tick(500);
1189+
1190+
expect(overlay.querySelectorAll('.mat-menu-panel').length).toBe(0, 'Expected no open menus');
1191+
}));
1192+
11711193
it('should set a class on the menu items that trigger a sub-menu', () => {
11721194
compileTestComponent();
11731195
instance.rootTrigger.openMenu();

0 commit comments

Comments
 (0)