Skip to content

feat(aria/menu): Add possibility to create a shared menu component from aria/menu #32731

@alinmateutdev

Description

@alinmateutdev

Feature Description

When building a menu using aria/menu you have to have the following structure:

  1. an for cdkConnectedOverlay
  2. a container element with ngMenu
  3. another for ngMenuContent
  4. only then the actual ngMenuItem elements

This results in 4 structural levels that need to be repeated every time you need a new menu.

I attempted to create a shared menu component (similar to Angular Material’s MatMenu) in order to reduce this complexity to 2 levels and enable an API like:

<button appMenuTrigger #appMenuTrigger="appMenuTrigger"  [appMenu]="appMenu.formatMenu()">
  Open Menu
</button>

<app-menu #appMenu [appMenuTrigger]="appMenuTrigger">
  <app-menu-item value="Mark as read">
    <span class="icon material-symbols-outlined" aria-hidden="true">mark_email_read</span>
    <span class="label">Mark as read</span>
  </app-menu-item>

  <app-menu-item value="Snooze">
    <span class="icon material-symbols-outlined" aria-hidden="true">snooze</span>
    <span class="label">Snooze</span>
  </app-menu-item>
</app-menu>

Internally, app-menu wraps Menu and MenuContent:

@Component({
  selector: 'app-menu',
  imports: [OverlayModule, Menu, MenuContent],
  template: `
    <ng-template
      [cdkConnectedOverlayOpen]="appMenuTrigger().menuTrigger.expanded()"
      [cdkConnectedOverlay]="{ origin: appMenuTrigger().nativeElement, usePopover: 'inline' }"
      [cdkConnectedOverlayPositions]="overlayPositions"
      cdkAttachPopoverAsChild>

      <div ngMenu class="menu" #formatMenu="ngMenu">
        <ng-template ngMenuContent>
          <ng-content />
        </ng-template>
      </div>
    </ng-template>
  `
})
export class AppMenu {
  appMenuTrigger = input.required<AppMenuTrigger>();
  formatMenu = viewChild<Menu<string>>('formatMenu');
  overlayPositions: ConnectedPosition[] = [{originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4}];
}

and menu items are wrapped like this:

@Component({
  selector: 'app-menu-item',
  template: '<ng-content />',
  hostDirectives: [{ directive: MenuItem, inputs: ['value: value'] }]
})
export class AppMenuItem {}

Problem
With this approach, accessibility breaks (keyboard navigation no longer works correctly). This seems to happen because Menu cannot properly register MenuItem children when they are content projected.

Proposed enhancement
Expose a public API on Menu that allows the menu to "refresh" in order to register the items when they are content projected.

Use Case

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureThis issue represents a new feature or feature request rather than a bug or bug fixneeds triageThis issue needs to be triaged by the team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions