Skip to content

fix(cdk/menu): picking up items from child menu #31684

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion goldens/cdk/menu/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export class CdkMenuBar extends CdkMenuBase implements AfterContentInit {

// @public
export abstract class CdkMenuBase extends CdkMenuGroup implements Menu, AfterContentInit, OnDestroy {
protected _allItems: QueryList<CdkMenuItem>;
protected closeOpenMenu(menu: MenuStackItem, options?: {
focusParentTrigger?: boolean;
}): void;
Expand All @@ -110,7 +111,7 @@ export abstract class CdkMenuBase extends CdkMenuGroup implements Menu, AfterCon
setActiveMenuItem(item: number | CdkMenuItem): void;
protected triggerItem?: CdkMenuItem;
// (undocumented)
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkMenuBase, never, never, { "id": { "alias": "id"; "required": false; }; }, {}, ["items"], never, true, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<CdkMenuBase, never, never, { "id": { "alias": "id"; "required": false; }; }, {}, ["_allItems"], never, true, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<CdkMenuBase, never>;
}
Expand Down Expand Up @@ -146,6 +147,7 @@ export class CdkMenuItem implements FocusableOption, FocusableElement, Toggler,
// (undocumented)
protected _ngZone: NgZone;
_onKeydown(event: KeyboardEvent): void;
readonly _parentMenu: Menu | null;
_resetTabIndex(): void;
_setTabIndex(event?: MouseEvent): void;
_tabindex: 0 | -1;
Expand Down
22 changes: 19 additions & 3 deletions src/cdk/menu/menu-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,15 @@ export abstract class CdkMenuBase
/** The directionality (text direction) of the current page. */
protected readonly dir = inject(Directionality, {optional: true});

/** All items inside the menu, including ones that belong to other menus. */
@ContentChildren(CdkMenuItem, {descendants: true})
protected _allItems: QueryList<CdkMenuItem>;

/** The id of the menu's host element. */
@Input() id: string = inject(_IdGenerator).getId('cdk-menu-');

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

/** The direction items in the menu flow. */
orientation: 'horizontal' | 'vertical' = 'vertical';
Expand Down Expand Up @@ -107,6 +110,7 @@ export abstract class CdkMenuBase
if (!this.isInline) {
this.menuStack.push(this);
}
this._setItems();
this._setKeyManager();
this._handleFocus();
this._subscribeToMenuStackHasFocus();
Expand Down Expand Up @@ -178,6 +182,18 @@ export abstract class CdkMenuBase
}
}

/** Sets up the subscription that keeps the items list in sync. */
private _setItems() {
// Since the items query has `descendants: true`, we need
// to filter out items belonging to a different menu.
this._allItems.changes
.pipe(startWith(this._allItems), takeUntil(this.destroyed))
.subscribe((items: QueryList<CdkMenuItem>) => {
this.items.reset(items.filter(item => item._parentMenu === this));
this.items.notifyOnChanges();
});
}

/** Setup the FocusKeyManager with the correct orientation for the menu. */
private _setKeyManager() {
this.keyManager = new FocusKeyManager(this.items).withWrap().withTypeAhead().withHomeAndEnd();
Expand Down
2 changes: 1 addition & 1 deletion src/cdk/menu/menu-item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export class CdkMenuItem implements FocusableOption, FocusableElement, Toggler,
private readonly _menuStack = inject(MENU_STACK);

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

/** Reference to the CdkMenuItemTrigger directive if one is added to the same element */
private readonly _menuTrigger = inject(CdkMenuTrigger, {optional: true, self: true});
Expand Down
30 changes: 30 additions & 0 deletions src/cdk/menu/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,16 @@ describe('Menu', () => {
expect(document.activeElement).toEqual(nativeMenuItems[2]);
});
});

it('should not pick up items from nested menu', () => {
const getItemsText = (menu: CdkMenu) =>
menu.items.map(i => i._elementRef.nativeElement.textContent?.trim());
const fixture = TestBed.createComponent(NestedMenuDefinition);
fixture.detectChanges();

expect(getItemsText(fixture.componentInstance.root)).toEqual(['One', 'Two']);
expect(getItemsText(fixture.componentInstance.inner)).toEqual(['Three', 'Four', 'Five']);
});
});

@Component({
Expand Down Expand Up @@ -667,3 +677,23 @@ class WithComplexNestedMenusOnBottom {
class MenuWithActiveItem {
@ViewChild(CdkMenu) menu: CdkMenu;
}

@Component({
template: `
<div cdkMenu #root>
<button cdkMenuItem>One</button>
<button cdkMenuItem>Two</button>

<div cdkMenu #inner>
<button cdkMenuItem>Three</button>
<button cdkMenuItem>Four</button>
<button cdkMenuItem>Five</button>
</div>
</div>
`,
imports: [CdkMenuModule],
})
class NestedMenuDefinition {
@ViewChild('root', {read: CdkMenu}) root: CdkMenu;
@ViewChild('inner', {read: CdkMenu}) inner: CdkMenu;
}
Loading