Skip to content

Commit cd532b2

Browse files
authored
fix(cdk/menu): picking up items from child menu (angular#31684)
Fixes that the CDK menu was picking up items from nested menu instances which in turn were throwing off the keyboard navigation. Fixes angular#31678.
1 parent 6cbe4b2 commit cd532b2

File tree

4 files changed

+53
-5
lines changed

4 files changed

+53
-5
lines changed

goldens/cdk/menu/index.api.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export class CdkMenuBar extends CdkMenuBase implements AfterContentInit {
8484

8585
// @public
8686
export abstract class CdkMenuBase extends CdkMenuGroup implements Menu, AfterContentInit, OnDestroy {
87+
protected _allItems: QueryList<CdkMenuItem>;
8788
protected closeOpenMenu(menu: MenuStackItem, options?: {
8889
focusParentTrigger?: boolean;
8990
}): void;
@@ -110,7 +111,7 @@ export abstract class CdkMenuBase extends CdkMenuGroup implements Menu, AfterCon
110111
setActiveMenuItem(item: number | CdkMenuItem): void;
111112
protected triggerItem?: CdkMenuItem;
112113
// (undocumented)
113-
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkMenuBase, never, never, { "id": { "alias": "id"; "required": false; }; }, {}, ["items"], never, true, never>;
114+
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkMenuBase, never, never, { "id": { "alias": "id"; "required": false; }; }, {}, ["_allItems"], never, true, never>;
114115
// (undocumented)
115116
static ɵfac: i0.ɵɵFactoryDeclaration<CdkMenuBase, never>;
116117
}
@@ -146,6 +147,7 @@ export class CdkMenuItem implements FocusableOption, FocusableElement, Toggler,
146147
// (undocumented)
147148
protected _ngZone: NgZone;
148149
_onKeydown(event: KeyboardEvent): void;
150+
readonly _parentMenu: Menu | null;
149151
_resetTabIndex(): void;
150152
_setTabIndex(event?: MouseEvent): void;
151153
_tabindex: 0 | -1;

src/cdk/menu/menu-base.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,15 @@ export abstract class CdkMenuBase
6767
/** The directionality (text direction) of the current page. */
6868
protected readonly dir = inject(Directionality, {optional: true});
6969

70+
/** All items inside the menu, including ones that belong to other menus. */
71+
@ContentChildren(CdkMenuItem, {descendants: true})
72+
protected _allItems: QueryList<CdkMenuItem>;
73+
7074
/** The id of the menu's host element. */
7175
@Input() id: string = inject(_IdGenerator).getId('cdk-menu-');
7276

73-
/** All child MenuItem elements nested in this Menu. */
74-
@ContentChildren(CdkMenuItem, {descendants: true})
75-
readonly items: QueryList<CdkMenuItem>;
77+
/** All child MenuItem elements belonging to this Menu. */
78+
readonly items: QueryList<CdkMenuItem> = new QueryList();
7679

7780
/** The direction items in the menu flow. */
7881
orientation: 'horizontal' | 'vertical' = 'vertical';
@@ -107,6 +110,7 @@ export abstract class CdkMenuBase
107110
if (!this.isInline) {
108111
this.menuStack.push(this);
109112
}
113+
this._setItems();
110114
this._setKeyManager();
111115
this._handleFocus();
112116
this._subscribeToMenuStackHasFocus();
@@ -178,6 +182,18 @@ export abstract class CdkMenuBase
178182
}
179183
}
180184

185+
/** Sets up the subscription that keeps the items list in sync. */
186+
private _setItems() {
187+
// Since the items query has `descendants: true`, we need
188+
// to filter out items belonging to a different menu.
189+
this._allItems.changes
190+
.pipe(startWith(this._allItems), takeUntil(this.destroyed))
191+
.subscribe((items: QueryList<CdkMenuItem>) => {
192+
this.items.reset(items.filter(item => item._parentMenu === this));
193+
this.items.notifyOnChanges();
194+
});
195+
}
196+
181197
/** Setup the FocusKeyManager with the correct orientation for the menu. */
182198
private _setKeyManager() {
183199
this.keyManager = new FocusKeyManager(this.items).withWrap().withTypeAhead().withHomeAndEnd();

src/cdk/menu/menu-item.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export class CdkMenuItem implements FocusableOption, FocusableElement, Toggler,
6363
private readonly _menuStack = inject(MENU_STACK);
6464

6565
/** The parent menu in which this menuitem resides. */
66-
private readonly _parentMenu = inject(CDK_MENU, {optional: true});
66+
readonly _parentMenu = inject(CDK_MENU, {optional: true});
6767

6868
/** Reference to the CdkMenuItemTrigger directive if one is added to the same element */
6969
private readonly _menuTrigger = inject(CdkMenuTrigger, {optional: true, self: true});

src/cdk/menu/menu.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,16 @@ describe('Menu', () => {
499499
expect(document.activeElement).toEqual(nativeMenuItems[2]);
500500
});
501501
});
502+
503+
it('should not pick up items from nested menu', () => {
504+
const getItemsText = (menu: CdkMenu) =>
505+
menu.items.map(i => i._elementRef.nativeElement.textContent?.trim());
506+
const fixture = TestBed.createComponent(NestedMenuDefinition);
507+
fixture.detectChanges();
508+
509+
expect(getItemsText(fixture.componentInstance.root)).toEqual(['One', 'Two']);
510+
expect(getItemsText(fixture.componentInstance.inner)).toEqual(['Three', 'Four', 'Five']);
511+
});
502512
});
503513

504514
@Component({
@@ -667,3 +677,23 @@ class WithComplexNestedMenusOnBottom {
667677
class MenuWithActiveItem {
668678
@ViewChild(CdkMenu) menu: CdkMenu;
669679
}
680+
681+
@Component({
682+
template: `
683+
<div cdkMenu #root>
684+
<button cdkMenuItem>One</button>
685+
<button cdkMenuItem>Two</button>
686+
687+
<div cdkMenu #inner>
688+
<button cdkMenuItem>Three</button>
689+
<button cdkMenuItem>Four</button>
690+
<button cdkMenuItem>Five</button>
691+
</div>
692+
</div>
693+
`,
694+
imports: [CdkMenuModule],
695+
})
696+
class NestedMenuDefinition {
697+
@ViewChild('root', {read: CdkMenu}) root: CdkMenu;
698+
@ViewChild('inner', {read: CdkMenu}) inner: CdkMenu;
699+
}

0 commit comments

Comments
 (0)