Skip to content

Commit e2b3c2c

Browse files
committed
fix(cdk-experimental/menu): fix bug preventing keyboard event handling if opened programmatically
If a menu is opened programmatically, subsequent keyboard events to close it out are not handled. This fixes the issue by tracking the open sub-menu regardless of how it was opened up.
1 parent 49de56c commit e2b3c2c

File tree

3 files changed

+97
-11
lines changed

3 files changed

+97
-11
lines changed

src/cdk-experimental/menu/menu-bar.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,27 @@ describe('MenuBar', () => {
452452

453453
expect(document.activeElement).toEqual(fileMenuNativeItems[1]);
454454
}));
455+
456+
it('should handle keyboard actions if initial menu is opened programmatically', () => {
457+
fixture.debugElement
458+
.queryAll(By.directive(CdkMenuItem))[0]
459+
.injector.get(CdkMenuItem)
460+
.getMenuTrigger()!
461+
.openMenu();
462+
detectChanges();
463+
fixture.debugElement
464+
.queryAll(By.directive(CdkMenuItem))[2]
465+
.injector.get(CdkMenuItem)
466+
.getMenuTrigger()!
467+
.openMenu();
468+
detectChanges();
469+
470+
fileMenuNativeItems[0].focus();
471+
dispatchKeyboardEvent(fileMenuNativeItems[0], 'keydown', TAB);
472+
detectChanges();
473+
474+
expect(nativeMenus.length).toBe(0);
475+
});
455476
});
456477
});
457478

src/cdk-experimental/menu/menu-bar.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ import {
1818
import {Directionality} from '@angular/cdk/bidi';
1919
import {FocusKeyManager, FocusOrigin} from '@angular/cdk/a11y';
2020
import {LEFT_ARROW, RIGHT_ARROW, UP_ARROW, DOWN_ARROW, ESCAPE, TAB} from '@angular/cdk/keycodes';
21+
import {takeUntil, mergeAll, mapTo, take, startWith, mergeMap, switchMap} from 'rxjs/operators';
22+
import {Subject, merge} from 'rxjs';
2123
import {CdkMenuGroup} from './menu-group';
2224
import {CDK_MENU, Menu} from './menu-interface';
2325
import {CdkMenuItem} from './menu-item';
2426
import {MenuStack, MenuStackItem, FocusNext} from './menu-stack';
25-
import {Subject} from 'rxjs';
26-
import {takeUntil} from 'rxjs/operators';
2727

2828
/**
2929
* Directive applied to an element which configures it as a MenuBar by setting the appropriate
@@ -64,6 +64,9 @@ export class CdkMenuBar extends CdkMenuGroup implements Menu, AfterContentInit,
6464
@ContentChildren(CdkMenuItem, {descendants: true})
6565
private readonly _allItems: QueryList<CdkMenuItem>;
6666

67+
/** The Menu Item which triggered the open submenu. */
68+
private _openItem?: CdkMenuItem;
69+
6770
constructor(readonly _menuStack: MenuStack, @Optional() private readonly _dir?: Directionality) {
6871
super();
6972
}
@@ -72,6 +75,7 @@ export class CdkMenuBar extends CdkMenuGroup implements Menu, AfterContentInit,
7275
super.ngAfterContentInit();
7376

7477
this._setKeyManager();
78+
this._subscribeToMenuOpen();
7579
this._subscribeToMenuStack();
7680
}
7781

@@ -163,12 +167,13 @@ export class CdkMenuBar extends CdkMenuGroup implements Menu, AfterContentInit,
163167
* Close the open menu if the current active item opened the requested MenuStackItem.
164168
* @param item the MenuStackItem requested to be closed.
165169
*/
166-
private _closeOpenMenu(item: MenuStackItem) {
170+
private _closeOpenMenu(menu: MenuStackItem) {
171+
const trigger = this._openItem;
167172
const keyManager = this._keyManager;
168-
if (item === keyManager.activeItem?.getMenu()) {
169-
keyManager.activeItem.getMenuTrigger()?.closeMenu();
173+
if (menu === trigger?.getMenuTrigger()?.getMenu()) {
174+
trigger.getMenuTrigger()?.closeMenu();
170175
keyManager.setFocusOrigin('keyboard');
171-
keyManager.setActiveItem(keyManager.activeItem);
176+
keyManager.setActiveItem(trigger);
172177
}
173178
}
174179

@@ -207,6 +212,33 @@ export class CdkMenuBar extends CdkMenuGroup implements Menu, AfterContentInit,
207212
return this.orientation === 'horizontal';
208213
}
209214

215+
/**
216+
* Subscribe to the menu trigger's open events in order to track the trigger which opened the menu
217+
* and stop tracking it when the menu is closed closed.
218+
*/
219+
private _subscribeToMenuOpen() {
220+
this._allItems.changes
221+
.pipe(
222+
startWith(this._allItems),
223+
mergeMap((list: QueryList<CdkMenuItem>) =>
224+
list
225+
.filter(item => item.hasMenu())
226+
.map(item =>
227+
item
228+
.getMenuTrigger()!
229+
.opened.pipe(mapTo(item), takeUntil(merge(this._allItems.changes, this._destroyed)))
230+
)
231+
),
232+
mergeAll(),
233+
switchMap((item: CdkMenuItem) => {
234+
this._openItem = item;
235+
return item.getMenuTrigger()!.closed;
236+
}),
237+
takeUntil(this._destroyed)
238+
)
239+
.subscribe(() => (this._openItem = undefined));
240+
}
241+
210242
ngOnDestroy() {
211243
super.ngOnDestroy();
212244
this._destroyed.next();

src/cdk-experimental/menu/menu.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ import {
2929
hasModifierKey,
3030
} from '@angular/cdk/keycodes';
3131
import {Directionality} from '@angular/cdk/bidi';
32-
import {take, takeUntil} from 'rxjs/operators';
32+
import {take, takeUntil, startWith, mergeMap, mapTo, mergeAll, switchMap} from 'rxjs/operators';
33+
import {merge} from 'rxjs';
3334
import {CdkMenuGroup} from './menu-group';
3435
import {CdkMenuPanel} from './menu-panel';
3536
import {Menu, CDK_MENU} from './menu-interface';
@@ -81,6 +82,9 @@ export class CdkMenu extends CdkMenuGroup implements Menu, AfterContentInit, OnI
8182
@ContentChildren(CdkMenuItem, {descendants: true})
8283
private readonly _allItems: QueryList<CdkMenuItem>;
8384

85+
/** The Menu Item which triggered the open submenu. */
86+
private _openItem?: CdkMenuItem;
87+
8488
/**
8589
* A reference to the enclosing parent menu panel.
8690
*
@@ -106,6 +110,7 @@ export class CdkMenu extends CdkMenuGroup implements Menu, AfterContentInit, OnI
106110

107111
this._completeChangeEmitter();
108112
this._setKeyManager();
113+
this._subscribeToMenuOpen();
109114
this._subscribeToMenuStack();
110115
}
111116

@@ -227,12 +232,13 @@ export class CdkMenu extends CdkMenuGroup implements Menu, AfterContentInit, OnI
227232
* Close the open menu if the current active item opened the requested MenuStackItem.
228233
* @param item the MenuStackItem requested to be closed.
229234
*/
230-
private _closeOpenMenu(item: MenuStackItem) {
235+
private _closeOpenMenu(menu: MenuStackItem) {
231236
const keyManager = this._keyManager;
232-
if (item === keyManager.activeItem?.getMenu()) {
233-
keyManager.activeItem.getMenuTrigger()?.closeMenu();
237+
const trigger = this._openItem;
238+
if (menu === trigger?.getMenuTrigger()?.getMenu()) {
239+
trigger.getMenuTrigger()?.closeMenu();
234240
keyManager.setFocusOrigin('keyboard');
235-
keyManager.setActiveItem(keyManager.activeItem);
241+
keyManager.setActiveItem(trigger);
236242
}
237243
}
238244

@@ -259,6 +265,33 @@ export class CdkMenu extends CdkMenuGroup implements Menu, AfterContentInit, OnI
259265
}
260266
}
261267

268+
/**
269+
* Subscribe to the menu trigger's open events in order to track the trigger which opened the menu
270+
* and stop tracking it when the menu is closed closed.
271+
*/
272+
private _subscribeToMenuOpen() {
273+
this._allItems.changes
274+
.pipe(
275+
startWith(this._allItems),
276+
mergeMap((list: QueryList<CdkMenuItem>) =>
277+
list
278+
.filter(item => item.hasMenu())
279+
.map(item =>
280+
item
281+
.getMenuTrigger()!
282+
.opened.pipe(mapTo(item), takeUntil(merge(this._allItems.changes, this.closed)))
283+
)
284+
),
285+
mergeAll(),
286+
switchMap((item: CdkMenuItem) => {
287+
this._openItem = item;
288+
return item.getMenuTrigger()!.closed;
289+
}),
290+
takeUntil(this.closed)
291+
)
292+
.subscribe(() => (this._openItem = undefined));
293+
}
294+
262295
/** Return true if this menu has been configured in a horizontal orientation. */
263296
private _isHorizontal() {
264297
return this.orientation === 'horizontal';

0 commit comments

Comments
 (0)