Skip to content

bug(aria/menu): any DOM change inside an open submenu moves focus up and collapses it #33976

Description

@vasylchyzh

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:

  1. Menu starts _collection.startObserving(this.element) in afterNextRender. The MutationObserver in SortedCollection bumps _version on any childList change in the menu's subtree.
  2. orderedItems has no equal, so every bump produces a new array, and Menu._items / _itemPatterns change identity too.
  3. The afterRenderEffect in Menu's constructor that calls this._pattern.listBehavior.goto(activeItem) tracks the item list by accident: ListFocus.focus() reads inputs.items() through isListDisabled(). So it re-runs and focuses the menu's active item.
  4. Focus moves from the open submenu to an item in a menu above it, and MenuPattern.onFocusOut closes the submenu.

Possible fixes (any one of these avoids it):

  • Give SortedCollection.orderedItems an element-wise equal, so a version bump that changes no order isn't propagated.
  • Read the item list untracked in the goto(activeItem) effect; it only needs to re-run when visible() changes.
  • Bump _version only when an added node contains a registered item (the TODO(ok7sai) in SortedCollection already 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:

  1. Focus Open and press Enter.
  2. Press ArrowDown, ArrowRight, ArrowDown, ArrowRight. Focus is now on Last item, with both submenus open.
  3. Wait up to a second, until the @if renders 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

  1. self-assigned this
    on Oct 22, 2026
  2. added
    P3An issue that is relevant to core functions, but does not impede progress. Important, but not urgent
    and removed
    gemini-triagedLabel noting that an issue has been triaged by gemini
    on Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P3An issue that is relevant to core functions, but does not impede progress. Important, but not urgentarea: aria/menu

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions