Repository navigation
bug(aria/menu): any DOM change inside an open submenu moves focus up and collapses it #33976
Description
Is this a regression?
- Yes, this behavior used to work in the previous version
The previous version in which this bug was not present was
No response
Description
Any DOM change inside an open ngMenu (a ripple, an icon that renders late, an @if inside an item) moves focus back up the menu tree and collapses the open submenu, even though no menu item was added, removed or moved.
What happens:
Menustarts_collection.startObserving(this.element)inafterNextRender. TheMutationObserverinSortedCollectionbumps_versionon anychildListchange in the menu's subtree.orderedItemshas noequal, so every bump produces a new array, andMenu._items/_itemPatternschange identity too.- The
afterRenderEffectinMenu's constructor that callsthis._pattern.listBehavior.goto(activeItem)tracks the item list by accident:ListFocus.focus()readsinputs.items()throughisListDisabled(). So it re-runs and focuses the menu's active item. - Focus moves from the open submenu to an item in a menu above it, and
MenuPattern.onFocusOutcloses the submenu.
Possible fixes (any one of these avoids it):
- Give
SortedCollection.orderedItemsan element-wiseequal, so a version bump that changes no order isn't propagated. - Read the item list
untrackedin thegoto(activeItem)effect; it only needs to re-run whenvisible()changes. - Bump
_versiononly when an added node contains a registered item (theTODO(ok7sai)inSortedCollectionalready plans to replace the observer).
Reproduction
StackBlitz link: not attached. The standalone component below is the whole reproduction; it needs only @angular/aria (bootstrapped with provideZonelessChangeDetection()).
import { Component, signal } from '@angular/core';
import { Menu, MenuItem, MenuTrigger } from '@angular/aria/menu';
@Component({
selector: 'app-root',
imports: [Menu, MenuItem, MenuTrigger],
styles: `[data-visible='false'] { opacity: 0.3; }`,
template: `
<button ngMenuTrigger [menu]="rootMenu">Open</button>
<div ngMenu #rootMenu="ngMenu">
<div ngMenuItem value="a">Item A</div>
<div ngMenuItem value="b" [submenu]="menuB">Item B</div>
<div ngMenu #menuB="ngMenu">
<div ngMenuItem value="b1">Item B1</div>
<div ngMenuItem value="b2" [submenu]="menuB2">Item B2</div>
<div ngMenu #menuB2="ngMenu">
<div ngMenuItem value="last">
Last item
<!-- Any DOM change inside the open submenu triggers the bug. -->
@if (menuB2.visible() && tick() % 2) {
<span>•</span>
}
</div>
</div>
</div>
</div>
`,
})
export class App {
protected readonly tick = signal(0);
constructor() {
setInterval(() => this.tick.update((n) => n + 1), 1000);
}
}Steps to reproduce:
- Focus Open and press Enter.
- Press ArrowDown, ArrowRight, ArrowDown, ArrowRight. Focus is now on Last item, with both submenus open.
- Wait up to a second, until the
@ifrenders its dot.
Expected Behavior
The submenus stay open and focus stays on Last item: the change adds no menu item, so the menu state shouldn't change.
Actual Behavior
Item B2 collapses (aria-expanded="false") and focus moves to Item B2. Without the @if (or with the interval removed), both submenus stay open and focus stays on Last item. It reproduces every time, and the same way with a third submenu level.
Environment
- Angular: 22.1.4
- CDK/Material: @angular/cdk 22.1.4; @angular/aria 22.1.4 and 22.2.3 (the code involved is unchanged in 22.3.0-next.2)
- Browser(s): Chromium 153
- Operating System (e.g. Windows, macOS, Ubuntu): macOS
Activity