Spec-Zone.ru › Angular

Меню

Шаблон меню ARIA Справочник API меню

Обзор

Меню предлагает пользователям список действий или параметров и обычно появляется в ответ на щелчок кнопкой или правый щелчок. Меню поддерживают навигацию с помощью клавиатуры и клавиш со стрелками, подменю, флажки, переключатели и отключённые элементы.

import {Component, viewChild} from '@angular/core';
import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';
import {OverlayModule} from '@angular/cdk/overlay';

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],
})
export class App {
  formatMenu = viewChild<Menu<string>>('formatMenu');
  categorizeMenu = viewChild<Menu<string>>('categorizeMenu');
}
<button ngMenuTrigger #origin #trigger="ngMenuTrigger" [menu]="formatMenu()">Open Menu</button>

<ng-template
  [cdkConnectedOverlayOpen]="trigger.expanded()"
  [cdkConnectedOverlay]="{origin, usePopover: 'inline'}"
  [cdkConnectedOverlayPositions]="[
    {originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4},
  ]"
  cdkAttachPopoverAsChild
>
  <div ngMenu class="menu" #formatMenu="ngMenu">
    <ng-template ngMenuContent>
      <div ngMenuItem value="Mark as read">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >mark_email_read</span
        >
        <span class="label">Mark as read</span>
      </div>

      <div ngMenuItem value="Snooze">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">snooze</span>
        <span class="label">Snooze</span>
      </div>

      <div role="separator" aria-orientation="horizontal" class="separator"></div>

      <div
        ngMenuItem
        class="menu-item"
        value="Categorize"
        #categorizeItem
        [submenu]="categorizeMenu()"
      >
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >category</span
        >
        <span class="label">Categorize</span>
        <span class="icon material-symbols-outlined arrow" translate="no" aria-hidden="true"
          >arrow_right</span
        >
      </div>

      <ng-template
        [cdkConnectedOverlayOpen]="formatMenu.visible()"
        [cdkConnectedOverlay]="{origin: categorizeItem, usePopover: 'inline'}"
        [cdkConnectedOverlayPositions]="[
          {originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6},
        ]"
        cdkAttachPopoverAsChild
      >
        <div ngMenu class="menu" #categorizeMenu="ngMenu">
          <ng-template ngMenuContent>
            <div ngMenuItem value="Mark as important">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >label_important</span
              >
              <span class="label">Mark as important</span>
            </div>

            <div ngMenuItem value="Star">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >star</span
              >
              <span class="label">Star</span>
            </div>

            <div ngMenuItem value="Label">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >label</span
              >
              <span class="label">Label</span>
            </div>
          </ng-template>
        </div>
      </ng-template>

      <div role="separator" aria-orientation="horizontal" class="separator"></div>

      <div ngMenuItem value="Archive">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >archive</span
        >
        <span class="label">Archive</span>
      </div>

      <div ngMenuItem value="Report spam">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">report</span>
        <span class="label">Report spam</span>
      </div>

      <div ngMenuItem value="Delete">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">delete</span>
        <span class="label">Delete</span>
      </div>
    </ng-template>
  </div>
</ng-template>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  font-family: var(--inter-font);
  --border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background));
}

[ngMenuTrigger] {
  display: flex;
  cursor: pointer;
  align-items: center;
  padding: 0.6rem 2rem;
  border-radius: 0.5rem;
  color: var(--primary-contrast);
  border: 1px solid var(--border-color);
  background-color: var(--page-background);
}

[ngMenuTrigger] .icon {
  font-size: 1.5rem;
  opacity: 0.875;
}

[ngMenu] {
  margin: 0;
  width: 15rem;
  padding: 0.25rem;
  border-radius: 0.5rem;
  border: 1px solid var(--border-color);
  background-color: var(--page-background);
}

[ngMenu][data-visible='false'] {
  display: none;
}

[ngMenuItem] {
  outline: none;
  display: flex;
  cursor: pointer;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem;
  font-size: 0.875rem;
  border-radius: 0.25rem;
}

[ngMenuTrigger]:not([disabled]):hover,
[ngMenuTrigger][aria-expanded='true'],
[ngMenuItem]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='true'] {
  background: color-mix(in srgb, var(--border-color) 10%, transparent);
}

[ngMenuItem]:focus,
[ngMenuTrigger]:focus {
  outline: 2px solid var(--vivid-pink);
}

[ngMenuItem] .icon {
  opacity: 0.875;
  font-size: 1.25rem;
}

[ngMenuItem] .label {
  flex: 1;
  opacity: 0.875;
  font-size: 0.875rem;
}

[ngMenuItem]:not([aria-expanded='true']) .arrow {
  opacity: 0.5;
}

[ngMenu] .separator {
  border-top: 1px solid var(--border-color);
  margin: 0.25rem 0;
  opacity: 0.25;
}
import {Component, viewChild} from '@angular/core';
import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';
import {OverlayModule} from '@angular/cdk/overlay';

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],
})
export class App {
  formatMenu = viewChild<Menu<string>>('formatMenu');
  categorizeMenu = viewChild<Menu<string>>('categorizeMenu');
}
<button ngMenuTrigger #origin #trigger="ngMenuTrigger" [menu]="formatMenu()">Open Menu</button>

<ng-template
  [cdkConnectedOverlayOpen]="trigger.expanded()"
  [cdkConnectedOverlay]="{origin, usePopover: 'inline'}"
  [cdkConnectedOverlayPositions]="[
    {originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4},
  ]"
  cdkAttachPopoverAsChild
>
  <div ngMenu class="menu" #formatMenu="ngMenu">
    <ng-template ngMenuContent>
      <div class="group">
        <div ngMenuItem value="Mark as read">
          <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
            >mark_email_read</span
          >
          <span class="label">Mark as read</span>
        </div>

        <div ngMenuItem value="Snooze">
          <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
            >snooze</span
          >
          <span class="label">Snooze</span>
        </div>

        <div ngMenuItem value="Delete">
          <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
            >delete</span
          >
          <span class="label">Delete</span>
        </div>
      </div>

      <div class="group">
        <div
          ngMenuItem
          class="menu-item"
          value="Categorize"
          #categorizeItem
          [submenu]="categorizeMenu()"
        >
          <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
            >category</span
          >
          <span class="label">Categorize</span>
          <span class="icon material-symbols-outlined arrow" translate="no" aria-hidden="true"
            >arrow_right</span
          >
        </div>
      </div>

      <ng-template
        [cdkConnectedOverlayOpen]="formatMenu.visible()"
        [cdkConnectedOverlay]="{origin: categorizeItem, usePopover: 'inline'}"
        [cdkConnectedOverlayPositions]="[
          {originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6},
        ]"
        cdkAttachPopoverAsChild
      >
        <div ngMenu class="menu" #categorizeMenu="ngMenu">
          <ng-template ngMenuContent>
            <div class="group">
              <div ngMenuItem value="Mark as important">
                <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                  >label_important</span
                >
                <span class="label">Mark as important</span>
              </div>

              <div ngMenuItem value="Star">
                <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                  >star</span
                >
                <span class="label">Star</span>
              </div>

              <div ngMenuItem value="Label">
                <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                  >label</span
                >
                <span class="label">Label</span>
              </div>
            </div>
          </ng-template>
        </div>
      </ng-template>
    </ng-template>
  </div>
</ng-template>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  font-family: var(--inter-font);
}

[ngMenuTrigger] {
  display: flex;
  cursor: pointer;
  align-items: center;
  padding: 0.6rem 2rem;
  border-radius: 0.5rem;
  border: 1px solid transparent;
  background-color: color-mix(in srgb, var(--vivid-pink) 5%, transparent);
  color: color-mix(in srgb, var(--vivid-pink) 70%, var(--primary-contrast));
}

[ngMenuTrigger] .icon {
  font-size: 1.5rem;
  opacity: 0.875;
}

[ngMenu] {
  gap: 3px;
  width: 15rem;
  display: flex;
  flex-direction: column;
}

[ngMenu] .group {
  padding: 0.25rem;
  border-radius: 0.25rem;
  background-color: var(--page-background);
  box-shadow: 0 1px 2px 1px color-mix(in srgb, var(--primary-contrast) 25%, transparent);
}

[ngMenu] .group:first-of-type {
  border-top-left-radius: 1rem;
  border-top-right-radius: 1rem;
}

[ngMenu] .group:last-of-type {
  border-bottom-left-radius: 1rem;
  border-bottom-right-radius: 1rem;
}

[ngMenu][data-visible='false'] {
  display: none;
}

[ngMenuItem] {
  outline: none;
  display: flex;
  cursor: pointer;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem;
  font-size: 0.875rem;
  border-radius: 0.75rem;
}

[ngMenuTrigger]:hover,
[ngMenuTrigger][aria-expanded='true'] {
  background: color-mix(in srgb, var(--vivid-pink) 10%, transparent);
}

[ngMenuItem]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='true'] {
  color: color-mix(in srgb, var(--vivid-pink) 70%, var(--primary-contrast));
  background: color-mix(in srgb, var(--vivid-pink) 5%, transparent);
}

[ngMenuItem]:focus,
[ngMenuTrigger]:focus {
  outline: 2px solid var(--vivid-pink);
}

[ngMenuItem] .icon {
  opacity: 0.875;
  font-size: 1.25rem;
}

[ngMenuItem] .label {
  flex: 1;
  opacity: 0.875;
  font-size: 0.875rem;
}

[ngMenuItem]:not([aria-expanded='true']) .arrow {
  opacity: 0.5;
}

[ngMenu] .separator {
  border-top: 1px solid var(--quaternary-contrast);
  margin: 0.25rem 0;
  opacity: 0.25;
}
import {Component, viewChild} from '@angular/core';
import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';
import {OverlayModule} from '@angular/cdk/overlay';

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],
})
export class App {
  formatMenu = viewChild<Menu<string>>('formatMenu');
  categorizeMenu = viewChild<Menu<string>>('categorizeMenu');
}
<button class="retro-trigger" ngMenuTrigger #trigger="ngMenuTrigger" #origin [menu]="formatMenu()">
  Open Menu
</button>

<ng-template
  [cdkConnectedOverlayOpen]="trigger.expanded()"
  [cdkConnectedOverlay]="{origin, usePopover: 'inline'}"
  [cdkConnectedOverlayPositions]="[
    {originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4},
  ]"
  cdkAttachPopoverAsChild
>
  <div ngMenu class="menu" #formatMenu="ngMenu">
    <ng-template ngMenuContent>
      <div ngMenuItem value="Mark as read">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >mark_email_read</span
        >
        <span class="label">Mark as read</span>
      </div>

      <div ngMenuItem value="Snooze">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">snooze</span>
        <span class="label">Snooze</span>
      </div>

      <div role="separator" aria-orientation="horizontal" class="separator"></div>

      <div
        ngMenuItem
        class="menu-item"
        value="Categorize"
        #categorizeItem
        [submenu]="categorizeMenu()"
      >
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >category</span
        >
        <span class="label">Categorize</span>
        <span class="icon material-symbols-outlined arrow" translate="no" aria-hidden="true"
          >arrow_right</span
        >
      </div>

      <ng-template
        [cdkConnectedOverlayOpen]="formatMenu.visible()"
        [cdkConnectedOverlay]="{origin: categorizeItem, usePopover: 'inline'}"
        [cdkConnectedOverlayPositions]="[
          {originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6},
        ]"
        cdkAttachPopoverAsChild
      >
        <div ngMenu class="menu" #categorizeMenu="ngMenu">
          <ng-template ngMenuContent>
            <div ngMenuItem value="Mark as important">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >label_important</span
              >
              <span class="label">Mark as important</span>
            </div>

            <div ngMenuItem value="Star">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >star</span
              >
              <span class="label">Star</span>
            </div>

            <div ngMenuItem value="Label">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >label</span
              >
              <span class="label">Label</span>
            </div>
          </ng-template>
        </div>
      </ng-template>

      <div role="separator" aria-orientation="horizontal" class="separator"></div>

      <div ngMenuItem value="Archive">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >archive</span
        >
        <span class="label">Archive</span>
      </div>

      <div ngMenuItem value="Report spam">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">report</span>
        <span class="label">Report spam</span>
      </div>

      <div ngMenuItem value="Delete">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">delete</span>
        <span class="label">Delete</span>
      </div>
    </ng-template>
  </div>
</ng-template>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  font-size: 0.8rem;
  --border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background));

  font-family: 'Press Start 2P';

  --retro-button-color: var(--vivid-pink);
  --retro-shadow-light: color-mix(in srgb, #fff 20%, transparent);
  --retro-shadow-dark: color-mix(in srgb, #000 20%, transparent);
  --retro-elevated-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-light),
    inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast);
  --retro-flat-shadow:
    4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
    -4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
  --retro-clickable-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-light),
    inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
  --retro-pressed-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-dark),
    inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}

[ngMenuTrigger] {
  display: flex;
  cursor: pointer;
  align-items: center;
  padding: 0.6rem 2rem;
  font-family: 'Press Start 2P';
  color: #000;
  background-color: var(--vivid-pink);
  box-shadow: var(--retro-clickable-shadow);
  transition:
    transform 0.1s,
    box-shadow 0.1s;
}

[ngMenuTrigger]:hover {
  transform: translate(1px, 1px);
}

[ngMenuTrigger]:active {
  background-color: color-mix(in srgb, var(--vivid-pink) 80%, #fff);
  box-shadow: var(--retro-pressed-shadow);
  transform: translate(4px, 4px);
}

[ngMenuTrigger] .icon {
  font-size: 1.5rem;
  opacity: 0.875;
}

[ngMenu] {
  margin-top: 8px;
  width: 15rem;
  padding: 0.25rem;
  background-color: var(--page-background);
  box-shadow: var(--retro-flat-shadow);
}

[ngMenu][data-visible='false'] {
  display: none;
}

[ngMenuItem] {
  outline: none;
  display: flex;
  cursor: pointer;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem;
  font-size: 0.875rem;
}

[ngMenuItem]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='true'] {
  background: color-mix(in srgb, var(--border-color) 10%, transparent);
}

[ngMenuTrigger]:focus {
  outline: 4px dashed var(--vivid-pink);
  outline-offset: 8px;
}

[ngMenuItem]:focus {
  outline: 4px dashed var(--vivid-pink);
  outline-offset: -4px;
}

[ngMenuItem] .icon {
  opacity: 0.875;
  font-size: 1.25rem;
}

[ngMenuItem] .label {
  flex: 1;
  opacity: 0.875;
  font-size: 0.875rem;
}

[ngMenuItem]:not([aria-expanded='true']) .arrow {
  opacity: 0.5;
}

[ngMenu] .separator {
  border-top: 4px solid #000;
  margin: 0.25rem 0;
  opacity: 0.25;
}

Использование

Меню хорошо подходят для представления списков действий или команд, из которых пользователи могут выбирать.

Используйте меню, если:

  • Создаёте меню команд приложения (Файл, Правка, Вид)
  • Создаёте контекстные меню (действия по правому щелчку)
  • Показываете выпадающие списки действий
  • Реализуете выпадающие меню панели инструментов
  • Организуете настройки или параметры

Не используйте меню, если:

  • Создаёте навигацию по сайту (вместо этого используйте области навигации)
  • Создаёте элементы выбора в форме (используйте компонент Select)
  • Переключаетесь между панелями содержимого (используйте Tabs)
  • Показываете сворачиваемое содержимое (используйте Accordion)

Возможности

  • Навигация с помощью клавиатуры — клавиши со стрелками, Home/End и поиск по символам для эффективной навигации
  • Подменю — поддержка вложенных меню с автоматическим позиционированием
  • Типы меню — автономные меню, меню с триггером и панели меню
  • Флажки и переключатели — элементы меню для переключения и выбора
  • Отключённые элементы — мягкое или полное отключение с управлением фокусом
  • Автоматическое закрытие — возможность настроить закрытие при выборе
  • Поддержка RTL — навигация для языков с письмом справа налево

Примеры

Меню с триггером

Создайте выпадающее меню, объединив кнопку-триггер с меню. Триггер открывает и закрывает меню.

import {Component, viewChild} from '@angular/core';
import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';
import {OverlayModule} from '@angular/cdk/overlay';

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],
})
export class App {
  formatMenu = viewChild<Menu<string>>('formatMenu');
  categorizeMenu = viewChild<Menu<string>>('categorizeMenu');
}
<button ngMenuTrigger #origin #trigger="ngMenuTrigger" [menu]="formatMenu()">Open Menu</button>

<ng-template
  [cdkConnectedOverlayOpen]="trigger.expanded()"
  [cdkConnectedOverlay]="{origin, usePopover: 'inline'}"
  [cdkConnectedOverlayPositions]="[
    {originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4},
  ]"
  cdkAttachPopoverAsChild
>
  <div ngMenu class="menu" #formatMenu="ngMenu">
    <ng-template ngMenuContent>
      <div ngMenuItem value="Mark as read">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >mark_email_read</span
        >
        <span class="label">Mark as read</span>
      </div>

      <div ngMenuItem value="Snooze">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">snooze</span>
        <span class="label">Snooze</span>
      </div>

      <div role="separator" aria-orientation="horizontal" class="separator"></div>

      <div
        ngMenuItem
        class="menu-item"
        value="Categorize"
        #categorizeItem
        [submenu]="categorizeMenu()"
      >
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >category</span
        >
        <span class="label">Categorize</span>
        <span class="icon material-symbols-outlined arrow" translate="no" aria-hidden="true"
          >arrow_right</span
        >
      </div>

      <ng-template
        [cdkConnectedOverlayOpen]="formatMenu.visible()"
        [cdkConnectedOverlay]="{origin: categorizeItem, usePopover: 'inline'}"
        [cdkConnectedOverlayPositions]="[
          {originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6},
        ]"
        cdkAttachPopoverAsChild
      >
        <div ngMenu class="menu" #categorizeMenu="ngMenu">
          <ng-template ngMenuContent>
            <div ngMenuItem value="Mark as important">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >label_important</span
              >
              <span class="label">Mark as important</span>
            </div>

            <div ngMenuItem value="Star">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >star</span
              >
              <span class="label">Star</span>
            </div>

            <div ngMenuItem value="Label">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >label</span
              >
              <span class="label">Label</span>
            </div>
          </ng-template>
        </div>
      </ng-template>

      <div role="separator" aria-orientation="horizontal" class="separator"></div>

      <div ngMenuItem value="Archive">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >archive</span
        >
        <span class="label">Archive</span>
      </div>

      <div ngMenuItem value="Report spam">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">report</span>
        <span class="label">Report spam</span>
      </div>

      <div ngMenuItem value="Delete">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">delete</span>
        <span class="label">Delete</span>
      </div>
    </ng-template>
  </div>
</ng-template>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  font-family: var(--inter-font);
  --border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background));
}

[ngMenuTrigger] {
  display: flex;
  cursor: pointer;
  align-items: center;
  padding: 0.6rem 2rem;
  border-radius: 0.5rem;
  color: var(--primary-contrast);
  border: 1px solid var(--border-color);
  background-color: var(--page-background);
}

[ngMenuTrigger] .icon {
  font-size: 1.5rem;
  opacity: 0.875;
}

[ngMenu] {
  margin: 0;
  width: 15rem;
  padding: 0.25rem;
  border-radius: 0.5rem;
  border: 1px solid var(--border-color);
  background-color: var(--page-background);
}

[ngMenu][data-visible='false'] {
  display: none;
}

[ngMenuItem] {
  outline: none;
  display: flex;
  cursor: pointer;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem;
  font-size: 0.875rem;
  border-radius: 0.25rem;
}

[ngMenuTrigger]:not([disabled]):hover,
[ngMenuTrigger][aria-expanded='true'],
[ngMenuItem]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='true'] {
  background: color-mix(in srgb, var(--border-color) 10%, transparent);
}

[ngMenuItem]:focus,
[ngMenuTrigger]:focus {
  outline: 2px solid var(--vivid-pink);
}

[ngMenuItem] .icon {
  opacity: 0.875;
  font-size: 1.25rem;
}

[ngMenuItem] .label {
  flex: 1;
  opacity: 0.875;
  font-size: 0.875rem;
}

[ngMenuItem]:not([aria-expanded='true']) .arrow {
  opacity: 0.5;
}

[ngMenu] .separator {
  border-top: 1px solid var(--border-color);
  margin: 0.25rem 0;
  opacity: 0.25;
}
import {Component, viewChild} from '@angular/core';
import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';
import {OverlayModule} from '@angular/cdk/overlay';

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],
})
export class App {
  formatMenu = viewChild<Menu<string>>('formatMenu');
  categorizeMenu = viewChild<Menu<string>>('categorizeMenu');
}
<button ngMenuTrigger #origin #trigger="ngMenuTrigger" [menu]="formatMenu()">Open Menu</button>

<ng-template
  [cdkConnectedOverlayOpen]="trigger.expanded()"
  [cdkConnectedOverlay]="{origin, usePopover: 'inline'}"
  [cdkConnectedOverlayPositions]="[
    {originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4},
  ]"
  cdkAttachPopoverAsChild
>
  <div ngMenu class="menu" #formatMenu="ngMenu">
    <ng-template ngMenuContent>
      <div class="group">
        <div ngMenuItem value="Mark as read">
          <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
            >mark_email_read</span
          >
          <span class="label">Mark as read</span>
        </div>

        <div ngMenuItem value="Snooze">
          <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
            >snooze</span
          >
          <span class="label">Snooze</span>
        </div>

        <div ngMenuItem value="Delete">
          <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
            >delete</span
          >
          <span class="label">Delete</span>
        </div>
      </div>

      <div class="group">
        <div
          ngMenuItem
          class="menu-item"
          value="Categorize"
          #categorizeItem
          [submenu]="categorizeMenu()"
        >
          <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
            >category</span
          >
          <span class="label">Categorize</span>
          <span class="icon material-symbols-outlined arrow" translate="no" aria-hidden="true"
            >arrow_right</span
          >
        </div>
      </div>

      <ng-template
        [cdkConnectedOverlayOpen]="formatMenu.visible()"
        [cdkConnectedOverlay]="{origin: categorizeItem, usePopover: 'inline'}"
        [cdkConnectedOverlayPositions]="[
          {originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6},
        ]"
        cdkAttachPopoverAsChild
      >
        <div ngMenu class="menu" #categorizeMenu="ngMenu">
          <ng-template ngMenuContent>
            <div class="group">
              <div ngMenuItem value="Mark as important">
                <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                  >label_important</span
                >
                <span class="label">Mark as important</span>
              </div>

              <div ngMenuItem value="Star">
                <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                  >star</span
                >
                <span class="label">Star</span>
              </div>

              <div ngMenuItem value="Label">
                <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                  >label</span
                >
                <span class="label">Label</span>
              </div>
            </div>
          </ng-template>
        </div>
      </ng-template>
    </ng-template>
  </div>
</ng-template>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  font-family: var(--inter-font);
}

[ngMenuTrigger] {
  display: flex;
  cursor: pointer;
  align-items: center;
  padding: 0.6rem 2rem;
  border-radius: 0.5rem;
  border: 1px solid transparent;
  background-color: color-mix(in srgb, var(--vivid-pink) 5%, transparent);
  color: color-mix(in srgb, var(--vivid-pink) 70%, var(--primary-contrast));
}

[ngMenuTrigger] .icon {
  font-size: 1.5rem;
  opacity: 0.875;
}

[ngMenu] {
  gap: 3px;
  width: 15rem;
  display: flex;
  flex-direction: column;
}

[ngMenu] .group {
  padding: 0.25rem;
  border-radius: 0.25rem;
  background-color: var(--page-background);
  box-shadow: 0 1px 2px 1px color-mix(in srgb, var(--primary-contrast) 25%, transparent);
}

[ngMenu] .group:first-of-type {
  border-top-left-radius: 1rem;
  border-top-right-radius: 1rem;
}

[ngMenu] .group:last-of-type {
  border-bottom-left-radius: 1rem;
  border-bottom-right-radius: 1rem;
}

[ngMenu][data-visible='false'] {
  display: none;
}

[ngMenuItem] {
  outline: none;
  display: flex;
  cursor: pointer;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem;
  font-size: 0.875rem;
  border-radius: 0.75rem;
}

[ngMenuTrigger]:hover,
[ngMenuTrigger][aria-expanded='true'] {
  background: color-mix(in srgb, var(--vivid-pink) 10%, transparent);
}

[ngMenuItem]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='true'] {
  color: color-mix(in srgb, var(--vivid-pink) 70%, var(--primary-contrast));
  background: color-mix(in srgb, var(--vivid-pink) 5%, transparent);
}

[ngMenuItem]:focus,
[ngMenuTrigger]:focus {
  outline: 2px solid var(--vivid-pink);
}

[ngMenuItem] .icon {
  opacity: 0.875;
  font-size: 1.25rem;
}

[ngMenuItem] .label {
  flex: 1;
  opacity: 0.875;
  font-size: 0.875rem;
}

[ngMenuItem]:not([aria-expanded='true']) .arrow {
  opacity: 0.5;
}

[ngMenu] .separator {
  border-top: 1px solid var(--quaternary-contrast);
  margin: 0.25rem 0;
  opacity: 0.25;
}
import {Component, viewChild} from '@angular/core';
import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';
import {OverlayModule} from '@angular/cdk/overlay';

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],
})
export class App {
  formatMenu = viewChild<Menu<string>>('formatMenu');
  categorizeMenu = viewChild<Menu<string>>('categorizeMenu');
}
<button class="retro-trigger" ngMenuTrigger #trigger="ngMenuTrigger" #origin [menu]="formatMenu()">
  Open Menu
</button>

<ng-template
  [cdkConnectedOverlayOpen]="trigger.expanded()"
  [cdkConnectedOverlay]="{origin, usePopover: 'inline'}"
  [cdkConnectedOverlayPositions]="[
    {originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4},
  ]"
  cdkAttachPopoverAsChild
>
  <div ngMenu class="menu" #formatMenu="ngMenu">
    <ng-template ngMenuContent>
      <div ngMenuItem value="Mark as read">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >mark_email_read</span
        >
        <span class="label">Mark as read</span>
      </div>

      <div ngMenuItem value="Snooze">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">snooze</span>
        <span class="label">Snooze</span>
      </div>

      <div role="separator" aria-orientation="horizontal" class="separator"></div>

      <div
        ngMenuItem
        class="menu-item"
        value="Categorize"
        #categorizeItem
        [submenu]="categorizeMenu()"
      >
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >category</span
        >
        <span class="label">Categorize</span>
        <span class="icon material-symbols-outlined arrow" translate="no" aria-hidden="true"
          >arrow_right</span
        >
      </div>

      <ng-template
        [cdkConnectedOverlayOpen]="formatMenu.visible()"
        [cdkConnectedOverlay]="{origin: categorizeItem, usePopover: 'inline'}"
        [cdkConnectedOverlayPositions]="[
          {originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6},
        ]"
        cdkAttachPopoverAsChild
      >
        <div ngMenu class="menu" #categorizeMenu="ngMenu">
          <ng-template ngMenuContent>
            <div ngMenuItem value="Mark as important">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >label_important</span
              >
              <span class="label">Mark as important</span>
            </div>

            <div ngMenuItem value="Star">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >star</span
              >
              <span class="label">Star</span>
            </div>

            <div ngMenuItem value="Label">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >label</span
              >
              <span class="label">Label</span>
            </div>
          </ng-template>
        </div>
      </ng-template>

      <div role="separator" aria-orientation="horizontal" class="separator"></div>

      <div ngMenuItem value="Archive">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >archive</span
        >
        <span class="label">Archive</span>
      </div>

      <div ngMenuItem value="Report spam">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">report</span>
        <span class="label">Report spam</span>
      </div>

      <div ngMenuItem value="Delete">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">delete</span>
        <span class="label">Delete</span>
      </div>
    </ng-template>
  </div>
</ng-template>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  font-size: 0.8rem;
  --border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background));

  font-family: 'Press Start 2P';

  --retro-button-color: var(--vivid-pink);
  --retro-shadow-light: color-mix(in srgb, #fff 20%, transparent);
  --retro-shadow-dark: color-mix(in srgb, #000 20%, transparent);
  --retro-elevated-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-light),
    inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast);
  --retro-flat-shadow:
    4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
    -4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
  --retro-clickable-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-light),
    inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
  --retro-pressed-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-dark),
    inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}

[ngMenuTrigger] {
  display: flex;
  cursor: pointer;
  align-items: center;
  padding: 0.6rem 2rem;
  font-family: 'Press Start 2P';
  color: #000;
  background-color: var(--vivid-pink);
  box-shadow: var(--retro-clickable-shadow);
  transition:
    transform 0.1s,
    box-shadow 0.1s;
}

[ngMenuTrigger]:hover {
  transform: translate(1px, 1px);
}

[ngMenuTrigger]:active {
  background-color: color-mix(in srgb, var(--vivid-pink) 80%, #fff);
  box-shadow: var(--retro-pressed-shadow);
  transform: translate(4px, 4px);
}

[ngMenuTrigger] .icon {
  font-size: 1.5rem;
  opacity: 0.875;
}

[ngMenu] {
  margin-top: 8px;
  width: 15rem;
  padding: 0.25rem;
  background-color: var(--page-background);
  box-shadow: var(--retro-flat-shadow);
}

[ngMenu][data-visible='false'] {
  display: none;
}

[ngMenuItem] {
  outline: none;
  display: flex;
  cursor: pointer;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem;
  font-size: 0.875rem;
}

[ngMenuItem]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='true'] {
  background: color-mix(in srgb, var(--border-color) 10%, transparent);
}

[ngMenuTrigger]:focus {
  outline: 4px dashed var(--vivid-pink);
  outline-offset: 8px;
}

[ngMenuItem]:focus {
  outline: 4px dashed var(--vivid-pink);
  outline-offset: -4px;
}

[ngMenuItem] .icon {
  opacity: 0.875;
  font-size: 1.25rem;
}

[ngMenuItem] .label {
  flex: 1;
  opacity: 0.875;
  font-size: 0.875rem;
}

[ngMenuItem]:not([aria-expanded='true']) .arrow {
  opacity: 0.5;
}

[ngMenu] .separator {
  border-top: 4px solid #000;
  margin: 0.25rem 0;
  opacity: 0.25;
}

Меню автоматически закрывается, когда пользователь выбирает элемент или нажимает Escape.

Контекстное меню

Контекстные меню появляются в месте расположения курсора, когда пользователь щёлкает элемент правой кнопкой мыши.

import {Component, viewChild, signal} from '@angular/core';
import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';
import {OverlayModule} from '@angular/cdk/overlay';

@Component({
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
  imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],
})
export class App {
  trigger = viewChild.required<MenuTrigger<string>>('trigger');
  contextMenu = viewChild<Menu<string>>('contextMenu');

  // Position of the context menu
  menuPosition = signal({x: 0, y: 0});

  onContextMenu(event: MouseEvent) {
    event.preventDefault();

    // Update position coordinates
    this.menuPosition.set({x: event.clientX, y: event.clientY});

    // Open the menu via the trigger
    this.trigger().open();
  }

  onItemSelected(value: string) {
    console.log(`Action selected: ${value}`);
  }
}
<!-- Target area for right-click -->
<div (contextmenu)="onContextMenu($event)" class="context-zone">
  Right-click inside this area to open the context menu.
</div>

<!-- Hidden trigger positioned dynamically at cursor coordinates -->
<div
  #triggerEl
  ngMenuTrigger
  #trigger="ngMenuTrigger"
  [menu]="contextMenu()"
  style="position: fixed; visibility: hidden"
  [style.left.px]="menuPosition().x"
  [style.top.px]="menuPosition().y"
></div>

<!-- Overlay container for the menu -->
<ng-template
  [cdkConnectedOverlayOpen]="trigger.expanded()"
  [cdkConnectedOverlay]="{origin: triggerEl, usePopover: 'inline'}"
  cdkAttachPopoverAsChild
>
  <div ngMenu #contextMenu="ngMenu" (itemSelected)="onItemSelected($event)">
    <ng-template ngMenuContent>
      <div ngMenuItem value="Edit">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">edit</span>
        <span class="label">Edit</span>
      </div>

      <div ngMenuItem value="Copy">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >content_copy</span
        >
        <span class="label">Copy</span>
      </div>

      <div role="separator" class="separator"></div>

      <div ngMenuItem value="Delete" style="color: var(--vivid-pink)">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">delete</span>
        <span class="label">Delete</span>
      </div>
    </ng-template>
  </div>
</ng-template>

Установите положение меню с помощью координат события contextmenu.

Автономное меню

Автономному меню не требуется триггер, и оно остаётся видимым в интерфейсе.

import {Component, signal, viewChild} from '@angular/core';
import {Menu, MenuContent, MenuItem} from '@angular/aria/menu';
import {OverlayModule} from '@angular/cdk/overlay';

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Menu, MenuContent, MenuItem, OverlayModule],
})
export class App {
  hasInteracted = signal(false);
  updateMenu = viewChild<Menu<string>>('updateMenu');
}
<div ngMenu (mouseover)="hasInteracted.set(true)" (focusin)="hasInteracted.set(true)">
  <ng-template ngMenuContent>
    <span id="security-label" class="heading">SECURITY</span>

    <div role="group" aria-labelledby="security-label">
      <div ngMenuItem value="Change password">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >lock_open</span
        >
        <span class="label">Change password</span>
      </div>

      <div ngMenuItem value="Two-factor authentication">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >security_key</span
        >
        <span class="label">Two-factor authentication</span>
      </div>

      <div ngMenuItem value="Reset" #resetItem [submenu]="updateMenu()">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >refresh</span
        >
        <span class="label">Reset</span>
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >arrow_right</span
        >
      </div>

      <ng-template
        [cdkConnectedOverlayOpen]="hasInteracted()"
        [cdkConnectedOverlay]="{origin: resetItem, usePopover: 'inline'}"
        [cdkConnectedOverlayPositions]="[
          {originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6},
        ]"
        cdkAttachPopoverAsChild
      >
        <div ngMenu #updateMenu="ngMenu">
          <ng-template ngMenuContent>
            <div ngMenuItem value="Email address">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >email</span
              >
              <span class="label">Email address</span>
            </div>

            <div ngMenuItem value="Phone number">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >phone</span
              >
              <span class="label">Phone number</span>
            </div>

            <div ngMenuItem value="Password">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >vpn_key</span
              >
              <span class="label">Password</span>
            </div>
          </ng-template>
        </div>
      </ng-template>
    </div>

    <div role="separator" class="separator"></div>

    <span id="help-label" class="heading">HELP</span>

    <div role="group" aria-labelledby="help-label">
      <div ngMenuItem value="Support">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">help</span>
        <span class="label">Support</span>
      </div>

      <div ngMenuItem value="Feedback">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >feedback</span
        >
        <span class="label">Feedback</span>
      </div>
    </div>

    <div role="separator" class="separator"></div>

    <div ngMenuItem value="Logout">
      <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">logout</span>
      <span class="label">Logout</span>
    </div>
  </ng-template>
</div>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  font-family: var(--inter-font);
  --border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background));
}

[ngMenu] {
  margin: 0;
  width: 30rem;
  padding: 0.25rem;
  border-radius: 0.5rem;
  border: 1px solid var(--border-color);
  background-color: var(--page-background);
}

[ngMenu] [ngMenu] {
  width: 15rem;
}

[ngMenu][data-visible='false'] {
  display: none;
}

[ngMenuItem] {
  outline: none;
  display: flex;
  cursor: pointer;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem;
  font-size: 0.875rem;
  border-radius: 0.25rem;
}

[ngMenuItem]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='true'] {
  background: color-mix(in srgb, var(--border-color) 10%, transparent);
}

[ngMenuItem]:focus {
  outline: 2px solid var(--vivid-pink);
}

[ngMenuItem] .icon {
  opacity: 0.875;
  font-size: 1.25rem;
}

[ngMenuItem] .label {
  flex: 1;
  opacity: 0.875;
  font-size: 0.875rem;
}

[ngMenuItem]:not([aria-expanded='true']) .arrow {
  opacity: 0.5;
}

[ngMenu] .separator {
  border-top: 1px solid var(--border-color);
  margin: 0.25rem 0;
  opacity: 0.25;
}

[ngMenu] .heading {
  display: block;
  font-weight: bold;
  opacity: 0.6;
  font-size: 0.75rem;
  padding: 0.75rem;
  letter-spacing: 0.05rem;
}
import {Component, signal, viewChild} from '@angular/core';
import {Menu, MenuContent, MenuItem} from '@angular/aria/menu';
import {OverlayModule} from '@angular/cdk/overlay';

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Menu, MenuContent, MenuItem, OverlayModule],
})
export class App {
  hasInteracted = signal(false);
  updateMenu = viewChild<Menu<string>>('updateMenu');
}
<div
  ngMenu
  class="material-menu"
  (mouseover)="hasInteracted.set(true)"
  (focusin)="hasInteracted.set(true)"
>
  <ng-template ngMenuContent>
    <span id="security-label" class="heading">SECURITY</span>

    <div role="group" aria-labelledby="security-label">
      <div ngMenuItem value="Change password">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >lock_open</span
        >
        <span class="label">Change password</span>
      </div>

      <div ngMenuItem value="Two-factor authentication">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >security_key</span
        >
        <span class="label">Two-factor authentication</span>
      </div>

      <div ngMenuItem value="Reset" #resetItem [submenu]="updateMenu()">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >refresh</span
        >
        <span class="label">Reset</span>
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >arrow_right</span
        >
      </div>

      <ng-template
        [cdkConnectedOverlayOpen]="hasInteracted()"
        [cdkConnectedOverlay]="{origin: resetItem, usePopover: 'inline'}"
        [cdkConnectedOverlayPositions]="[
          {originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6},
        ]"
        cdkAttachPopoverAsChild
      >
        <div ngMenu #updateMenu="ngMenu">
          <ng-template ngMenuContent>
            <div ngMenuItem value="Email address">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >email</span
              >
              <span class="label">Email address</span>
            </div>

            <div ngMenuItem value="Phone number">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >phone</span
              >
              <span class="label">Phone number</span>
            </div>

            <div ngMenuItem value="Password">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >vpn_key</span
              >
              <span class="label">Password</span>
            </div>
          </ng-template>
        </div>
      </ng-template>
    </div>

    <div role="separator" class="separator"></div>

    <span id="help-label" class="heading">HELP</span>

    <div role="group" aria-labelledby="help-label">
      <div ngMenuItem value="Support">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">help</span>
        <span class="label">Support</span>
      </div>

      <div ngMenuItem value="Feedback">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >feedback</span
        >
        <span class="label">Feedback</span>
      </div>
    </div>

    <div role="separator" class="separator"></div>

    <div ngMenuItem value="Logout">
      <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">logout</span>
      <span class="label">Logout</span>
    </div>
  </ng-template>
</div>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  font-family: var(--inter-font);
}

[ngMenu] {
  gap: 3px;
  width: 30rem;
  padding: 1rem;
  border-radius: 1rem;
  display: flex;
  flex-direction: column;
  background-color: color-mix(in srgb, var(--vivid-pink) 5%, var(--page-background));
  border: 1px solid color-mix(in srgb, var(--full-contrast) 10%, var(--page-background));
}

[ngMenu] [ngMenu] {
  width: 15rem;
}

[ngMenu] .group {
  padding: 0.25rem;
  border-radius: 0.25rem;
  background-color: var(--page-background);
  box-shadow: 0 1px 2px 1px color-mix(in srgb, var(--primary-contrast) 25%, transparent);
}

[ngMenu] .group:first-of-type {
  border-top-left-radius: 1rem;
  border-top-right-radius: 1rem;
}

[ngMenu] .group:last-of-type {
  border-bottom-left-radius: 1rem;
  border-bottom-right-radius: 1rem;
}

[ngMenu][data-visible='false'] {
  display: none;
}

[ngMenuItem] {
  outline: none;
  display: flex;
  cursor: pointer;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem;
  font-size: 0.875rem;
  border-radius: 0.75rem;
}

[ngMenuItem]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='true'] {
  color: color-mix(in srgb, var(--vivid-pink) 70%, var(--primary-contrast));
  background: color-mix(in srgb, var(--vivid-pink) 5%, transparent);
}

[ngMenuItem]:focus {
  outline: 2px solid var(--vivid-pink);
}

[ngMenuItem] .icon {
  opacity: 0.875;
  font-size: 1.25rem;
}

[ngMenuItem] .label {
  flex: 1;
  opacity: 0.875;
  font-size: 0.875rem;
}

[ngMenuItem]:not([aria-expanded='true']) .arrow {
  opacity: 0.5;
}

[ngMenu] .separator {
  border-top: 1px solid var(--quaternary-contrast);
  margin: 0.25rem 0;
  opacity: 0.25;
}

[ngMenu] .heading {
  display: block;
  font-weight: bold;
  opacity: 0.6;
  font-size: 0.75rem;
  padding: 0.75rem;
  letter-spacing: 0.05rem;
}
import {Component, signal, viewChild} from '@angular/core';
import {Menu, MenuContent, MenuItem} from '@angular/aria/menu';
import {OverlayModule} from '@angular/cdk/overlay';

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Menu, MenuContent, MenuItem, OverlayModule],
})
export class App {
  hasInteracted = signal(false);
  updateMenu = viewChild<Menu<string>>('updateMenu');
}
<div
  ngMenu
  class="retro-menu"
  (mouseover)="hasInteracted.set(true)"
  (focusin)="hasInteracted.set(true)"
>
  <ng-template ngMenuContent>
    <span id="security-label" class="heading">SECURITY</span>

    <div role="group" aria-labelledby="security-label">
      <div ngMenuItem value="Change password">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >lock_open</span
        >
        <span class="label">Change password</span>
      </div>

      <div ngMenuItem value="Two-factor authentication">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >security_key</span
        >
        <span class="label">Two-factor authentication</span>
      </div>

      <div ngMenuItem value="Reset" #resetItem [submenu]="updateMenu()">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >refresh</span
        >
        <span class="label">Reset</span>
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >arrow_right</span
        >
      </div>

      <ng-template
        [cdkConnectedOverlayOpen]="hasInteracted()"
        [cdkConnectedOverlay]="{origin: resetItem, usePopover: 'inline'}"
        [cdkConnectedOverlayPositions]="[
          {originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6},
        ]"
        cdkAttachPopoverAsChild
      >
        <div ngMenu #updateMenu="ngMenu">
          <ng-template ngMenuContent>
            <div ngMenuItem value="Email address">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >email</span
              >
              <span class="label">Email address</span>
            </div>

            <div ngMenuItem value="Phone number">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >phone</span
              >
              <span class="label">Phone number</span>
            </div>

            <div ngMenuItem value="Password">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >vpn_key</span
              >
              <span class="label">Password</span>
            </div>
          </ng-template>
        </div>
      </ng-template>
    </div>

    <div role="separator" class="separator"></div>

    <span id="help-label" class="heading">HELP</span>

    <div role="group" aria-labelledby="help-label">
      <div ngMenuItem value="Support">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">help</span>
        <span class="label">Support</span>
      </div>

      <div ngMenuItem value="Feedback">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >feedback</span
        >
        <span class="label">Feedback</span>
      </div>
    </div>

    <div role="separator" class="separator"></div>

    <div ngMenuItem value="Logout">
      <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">logout</span>
      <span class="label">Logout</span>
    </div>
  </ng-template>
</div>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  font-size: 0.8rem;
  --border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background));

  font-family: 'Press Start 2P';

  --retro-button-color: var(--vivid-pink);
  --retro-shadow-light: color-mix(in srgb, #fff 20%, transparent);
  --retro-shadow-dark: color-mix(in srgb, #000 20%, transparent);
  --retro-elevated-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-light),
    inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast);
  --retro-flat-shadow:
    4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
    -4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
  --retro-clickable-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-light),
    inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
  --retro-pressed-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-dark),
    inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}

[ngMenu] {
  margin-top: 8px;
  width: 30rem;
  padding: 0.25rem;
  background-color: var(--page-background);
  box-shadow: var(--retro-flat-shadow);
}

[ngMenu] [ngMenu] {
  width: 15rem;
}

[ngMenu][data-visible='false'] {
  display: none;
}

[ngMenuItem] {
  outline: none;
  display: flex;
  cursor: pointer;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem;
  font-size: 0.875rem;
}

[ngMenuItem]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='true'] {
  background: color-mix(in srgb, var(--border-color) 10%, transparent);
}

[ngMenuItem]:focus {
  outline: 4px dashed var(--vivid-pink);
  outline-offset: -4px;
}

[ngMenuItem] .icon {
  opacity: 0.875;
  font-size: 1.25rem;
}

[ngMenuItem] .label {
  flex: 1;
  opacity: 0.875;
  font-size: 0.875rem;
}

[ngMenuItem]:not([aria-expanded='true']) .arrow {
  opacity: 0.5;
}

[ngMenu] .separator {
  border-top: 4px solid #000;
  margin: 0.25rem 0;
  opacity: 0.25;
}

[ngMenu] .heading {
  display: block;
  font-weight: bold;
  opacity: 0.6;
  font-size: 0.75rem;
  padding: 0.75rem;
  letter-spacing: 0.05rem;
}

Автономные меню хорошо подходят для постоянно видимых списков действий или навигации.

Отключённые элементы меню

Отключайте отдельные элементы меню с помощью входного параметра disabled. Управляйте поведением фокуса с помощью softDisabled.

import {Component, viewChild} from '@angular/core';
import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';
import {OverlayModule} from '@angular/cdk/overlay';

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],
})
export class App {
  formatMenu = viewChild<Menu<string>>('formatMenu');
  categorizeMenu = viewChild<Menu<string>>('categorizeMenu');
}
<button ngMenuTrigger #origin #trigger="ngMenuTrigger" [menu]="formatMenu()">Open Menu</button>

<ng-template
  [cdkConnectedOverlayOpen]="trigger.expanded()"
  [cdkConnectedOverlay]="{origin, usePopover: 'inline'}"
  [cdkConnectedOverlayPositions]="[
    {originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4},
  ]"
  cdkAttachPopoverAsChild
>
  <div ngMenu class="menu" #formatMenu="ngMenu">
    <ng-template ngMenuContent>
      <div ngMenuItem value="Mark as read">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >mark_email_read</span
        >
        <span class="label">Mark as read</span>
      </div>

      <div ngMenuItem value="Snooze" [disabled]="true">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">snooze</span>
        <span class="label">Snooze</span>
      </div>

      <div role="separator" aria-orientation="horizontal" class="separator"></div>

      <div
        ngMenuItem
        class="menu-item"
        value="Categorize"
        #categorizeItem
        [submenu]="categorizeMenu()"
        [disabled]="true"
      >
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >category</span
        >
        <span class="label">Categorize</span>
        <span class="icon material-symbols-outlined arrow" translate="no" aria-hidden="true"
          >arrow_right</span
        >
      </div>

      <ng-template
        [cdkConnectedOverlayOpen]="formatMenu.visible()"
        [cdkConnectedOverlay]="{origin: categorizeItem, usePopover: 'inline'}"
        [cdkConnectedOverlayPositions]="[
          {originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6},
        ]"
        cdkAttachPopoverAsChild
      >
        <div ngMenu class="menu" #categorizeMenu="ngMenu">
          <ng-template ngMenuContent>
            <div ngMenuItem value="Mark as important">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >label_important</span
              >
              <span class="label">Mark as important</span>
            </div>

            <div ngMenuItem value="Star">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >star</span
              >
              <span class="label">Star</span>
            </div>

            <div ngMenuItem value="Label">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >label</span
              >
              <span class="label">Label</span>
            </div>
          </ng-template>
        </div>
      </ng-template>

      <div role="separator" aria-orientation="horizontal" class="separator"></div>

      <div ngMenuItem value="Archive">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >archive</span
        >
        <span class="label">Archive</span>
      </div>

      <div ngMenuItem value="Report spam">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">report</span>
        <span class="label">Report spam</span>
      </div>

      <div ngMenuItem value="Delete">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">delete</span>
        <span class="label">Delete</span>
      </div>
    </ng-template>
  </div>
</ng-template>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  font-family: var(--inter-font);
  --border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background));
}

[ngMenuTrigger] {
  display: flex;
  cursor: pointer;
  align-items: center;
  padding: 0.6rem 2rem;
  border-radius: 0.5rem;
  color: var(--primary-contrast);
  border: 1px solid var(--border-color);
  background-color: var(--page-background);
}

[ngMenuTrigger] .icon {
  font-size: 1.5rem;
  opacity: 0.875;
}

[ngMenu] {
  margin: 0;
  width: 15rem;
  padding: 0.25rem;
  border-radius: 0.5rem;
  border: 1px solid var(--border-color);
  background-color: var(--page-background);
}

[ngMenu][data-visible='false'] {
  display: none;
}

[ngMenuItem] {
  outline: none;
  display: flex;
  cursor: pointer;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem;
  font-size: 0.875rem;
  border-radius: 0.25rem;
}

[ngMenuItem][aria-disabled='true'] {
  opacity: 0.5;
  cursor: default;
}

[ngMenuTrigger]:not([disabled]):hover,
[ngMenuTrigger][aria-expanded='true'],
[ngMenuItem]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='true'] {
  background: color-mix(in srgb, var(--border-color) 10%, transparent);
}

[ngMenuItem]:focus,
[ngMenuTrigger]:focus {
  outline: 2px solid var(--vivid-pink);
}

[ngMenuItem] .icon {
  opacity: 0.875;
  font-size: 1.25rem;
}

[ngMenuItem] .label {
  flex: 1;
  opacity: 0.875;
  font-size: 0.875rem;
}

[ngMenuItem]:not([aria-expanded='true']) .arrow {
  opacity: 0.5;
}

[ngMenu] .separator {
  border-top: 1px solid var(--border-color);
  margin: 0.25rem 0;
  opacity: 0.25;
}
import {Component, viewChild} from '@angular/core';
import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';
import {OverlayModule} from '@angular/cdk/overlay';

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],
})
export class App {
  formatMenu = viewChild<Menu<string>>('formatMenu');
  categorizeMenu = viewChild<Menu<string>>('categorizeMenu');
}
<button ngMenuTrigger #origin #trigger="ngMenuTrigger" [menu]="formatMenu()">Open Menu</button>

<ng-template
  [cdkConnectedOverlayOpen]="trigger.expanded()"
  [cdkConnectedOverlay]="{origin, usePopover: 'inline'}"
  [cdkConnectedOverlayPositions]="[
    {originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4},
  ]"
  cdkAttachPopoverAsChild
>
  <div ngMenu class="menu" #formatMenu="ngMenu">
    <ng-template ngMenuContent>
      <div class="group">
        <div ngMenuItem value="Mark as read">
          <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
            >mark_email_read</span
          >
          <span class="label">Mark as read</span>
        </div>

        <div ngMenuItem value="Snooze">
          <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
            >snooze</span
          >
          <span class="label">Snooze</span>
        </div>

        <div ngMenuItem value="Delete" [disabled]="true">
          <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
            >delete</span
          >
          <span class="label">Delete</span>
        </div>
      </div>

      <div class="group">
        <div
          ngMenuItem
          class="menu-item"
          value="Categorize"
          #categorizeItem
          [submenu]="categorizeMenu()"
        >
          <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
            >category</span
          >
          <span class="label">Categorize</span>
          <span class="icon material-symbols-outlined arrow" translate="no" aria-hidden="true"
            >arrow_right</span
          >
        </div>
      </div>

      <ng-template
        [cdkConnectedOverlayOpen]="formatMenu.visible()"
        [cdkConnectedOverlay]="{origin: categorizeItem, usePopover: 'inline'}"
        [cdkConnectedOverlayPositions]="[
          {originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6},
        ]"
        cdkAttachPopoverAsChild
      >
        <div ngMenu class="menu" #categorizeMenu="ngMenu">
          <ng-template ngMenuContent>
            <div class="group">
              <div ngMenuItem value="Mark as important">
                <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                  >label_important</span
                >
                <span class="label">Mark as important</span>
              </div>

              <div ngMenuItem value="Star">
                <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                  >star</span
                >
                <span class="label">Star</span>
              </div>

              <div ngMenuItem value="Label" [disabled]="true">
                <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                  >label</span
                >
                <span class="label">Label</span>
              </div>
            </div>
          </ng-template>
        </div>
      </ng-template>
    </ng-template>
  </div>
</ng-template>
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  font-family: var(--inter-font);
}

[ngMenuTrigger] {
  display: flex;
  cursor: pointer;
  align-items: center;
  padding: 0.6rem 2rem;
  border-radius: 0.5rem;
  border: 1px solid transparent;
  background-color: color-mix(in srgb, var(--vivid-pink) 5%, transparent);
  color: color-mix(in srgb, var(--vivid-pink) 70%, var(--primary-contrast));
}

[ngMenuTrigger] .icon {
  font-size: 1.5rem;
  opacity: 0.875;
}

[ngMenu] {
  gap: 3px;
  width: 15rem;
  display: flex;
  flex-direction: column;
}

[ngMenu] .group {
  padding: 0.25rem;
  border-radius: 0.25rem;
  background-color: var(--page-background);
  box-shadow: 0 1px 2px 1px color-mix(in srgb, var(--primary-contrast) 25%, transparent);
}

[ngMenu] .group:first-of-type {
  border-top-left-radius: 1rem;
  border-top-right-radius: 1rem;
}

[ngMenu] .group:last-of-type {
  border-bottom-left-radius: 1rem;
  border-bottom-right-radius: 1rem;
}

[ngMenu][data-visible='false'] {
  display: none;
}

[ngMenuItem] {
  outline: none;
  display: flex;
  cursor: pointer;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem;
  font-size: 0.875rem;
  border-radius: 0.75rem;
}

[ngMenuTrigger]:hover,
[ngMenuTrigger][aria-expanded='true'] {
  background: color-mix(in srgb, var(--vivid-pink) 10%, transparent);
}

[ngMenuItem]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='true'] {
  color: color-mix(in srgb, var(--vivid-pink) 70%, var(--primary-contrast));
  background: color-mix(in srgb, var(--vivid-pink) 5%, transparent);
}

[ngMenuItem]:focus,
[ngMenuTrigger]:focus {
  outline: 2px solid var(--vivid-pink);
}

[ngMenuItem] .icon {
  opacity: 0.875;
  font-size: 1.25rem;
}

[ngMenuItem] .label {
  flex: 1;
  opacity: 0.875;
  font-size: 0.875rem;
}

[ngMenuItem]:not([aria-expanded='true']) .arrow {
  opacity: 0.5;
}

[ngMenu] .separator {
  border-top: 1px solid var(--quaternary-contrast);
  margin: 0.25rem 0;
  opacity: 0.25;
}

[ngMenuItem][aria-disabled='true'] {
  opacity: 0.5;
  cursor: default;
}
import {Component, viewChild} from '@angular/core';
import {Menu, MenuContent, MenuItem, MenuTrigger} from '@angular/aria/menu';
import {OverlayModule} from '@angular/cdk/overlay';

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrl: 'app.css',
  imports: [Menu, MenuContent, MenuItem, MenuTrigger, OverlayModule],
})
export class App {
  formatMenu = viewChild<Menu<string>>('formatMenu');
  categorizeMenu = viewChild<Menu<string>>('categorizeMenu');
}
<button class="retro-trigger" ngMenuTrigger #trigger="ngMenuTrigger" #origin [menu]="formatMenu()">
  Open Menu
</button>

<ng-template
  [cdkConnectedOverlayOpen]="trigger.expanded()"
  [cdkConnectedOverlay]="{origin, usePopover: 'inline'}"
  [cdkConnectedOverlayPositions]="[
    {originX: 'start', originY: 'bottom', overlayX: 'start', overlayY: 'top', offsetY: 4},
  ]"
  cdkAttachPopoverAsChild
>
  <div ngMenu class="menu" #formatMenu="ngMenu">
    <ng-template ngMenuContent>
      <div ngMenuItem value="Mark as read" [disabled]="true">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >mark_email_read</span
        >
        <span class="label">Mark as read</span>
      </div>

      <div ngMenuItem value="Snooze">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">snooze</span>
        <span class="label">Snooze</span>
      </div>

      <div role="separator" aria-orientation="horizontal" class="separator"></div>

      <div
        ngMenuItem
        class="menu-item"
        value="Categorize"
        #categorizeItem
        [submenu]="categorizeMenu()"
      >
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >category</span
        >
        <span class="label">Categorize</span>
        <span class="icon material-symbols-outlined arrow" translate="no" aria-hidden="true"
          >arrow_right</span
        >
      </div>

      <ng-template
        [cdkConnectedOverlayOpen]="formatMenu.visible()"
        [cdkConnectedOverlay]="{origin: categorizeItem, usePopover: 'inline'}"
        [cdkConnectedOverlayPositions]="[
          {originX: 'end', originY: 'top', overlayY: 'top', overlayX: 'start', offsetX: 6},
        ]"
        cdkAttachPopoverAsChild
      >
        <div ngMenu class="menu" #categorizeMenu="ngMenu">
          <ng-template ngMenuContent>
            <div ngMenuItem value="Mark as important">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >label_important</span
              >
              <span class="label">Mark as important</span>
            </div>

            <div ngMenuItem value="Star">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >star</span
              >
              <span class="label">Star</span>
            </div>

            <div ngMenuItem value="Label">
              <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
                >label</span
              >
              <span class="label">Label</span>
            </div>
          </ng-template>
        </div>
      </ng-template>

      <div role="separator" aria-orientation="horizontal" class="separator"></div>

      <div ngMenuItem value="Archive">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true"
          >archive</span
        >
        <span class="label">Archive</span>
      </div>

      <div ngMenuItem value="Report spam">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">report</span>
        <span class="label">Report spam</span>
      </div>

      <div ngMenuItem value="Delete">
        <span class="icon material-symbols-outlined" translate="no" aria-hidden="true">delete</span>
        <span class="label">Delete</span>
      </div>
    </ng-template>
  </div>
</ng-template>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');

:host {
  display: flex;
  justify-content: center;
  font-size: 0.8rem;
  --border-color: color-mix(in srgb, var(--full-contrast) 20%, var(--page-background));

  font-family: 'Press Start 2P';

  --retro-button-color: var(--vivid-pink);
  --retro-shadow-light: color-mix(in srgb, #fff 20%, transparent);
  --retro-shadow-dark: color-mix(in srgb, #000 20%, transparent);
  --retro-elevated-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-light),
    inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast);
  --retro-flat-shadow:
    4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
    -4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
  --retro-clickable-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-light),
    inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
  --retro-pressed-shadow:
    inset 4px 4px 0px 0px var(--retro-shadow-dark),
    inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
    0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
    0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}

[ngMenuTrigger] {
  display: flex;
  cursor: pointer;
  align-items: center;
  padding: 0.6rem 2rem;
  font-family: 'Press Start 2P';
  color: #000;
  background-color: var(--vivid-pink);
  box-shadow: var(--retro-clickable-shadow);
  transition:
    transform 0.1s,
    box-shadow 0.1s;
}

[ngMenuTrigger]:hover {
  transform: translate(1px, 1px);
}

[ngMenuTrigger]:active {
  background-color: color-mix(in srgb, var(--vivid-pink) 80%, #fff);
  box-shadow: var(--retro-pressed-shadow);
  transform: translate(4px, 4px);
}

[ngMenuTrigger] .icon {
  font-size: 1.5rem;
  opacity: 0.875;
}

[ngMenu] {
  margin-top: 8px;
  width: 15rem;
  padding: 0.25rem;
  background-color: var(--page-background);
  box-shadow: var(--retro-flat-shadow);
}

[ngMenu][data-visible='false'] {
  display: none;
}

[ngMenuItem] {
  outline: none;
  display: flex;
  cursor: pointer;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem;
  font-size: 0.875rem;
}

[ngMenuItem]:not([aria-disabled='true']):hover,
[ngMenuItem]:not([aria-disabled='true']):focus,
[ngMenuItem][aria-expanded='true'] {
  background: color-mix(in srgb, var(--border-color) 10%, transparent);
}

[ngMenuTrigger]:focus {
  outline: 4px dashed var(--vivid-pink);
  outline-offset: 8px;
}

[ngMenuItem]:focus {
  outline: 4px dashed var(--vivid-pink);
  outline-offset: -4px;
}

[ngMenuItem] .icon {
  opacity: 0.875;
  font-size: 1.25rem;
}

[ngMenuItem] .label {
  flex: 1;
  opacity: 0.875;
  font-size: 0.875rem;
}

[ngMenuItem]:not([aria-expanded='true']) .arrow {
  opacity: 0.5;
}

[ngMenu] .separator {
  border-top: 4px solid #000;
  margin: 0.25rem 0;
  opacity: 0.25;
}

[ngMenuItem][aria-disabled='true'] {
  opacity: 0.5;
  cursor: default;
}

Если включён параметр [softDisabled]="true", отключённые элементы могут получать фокус, но их нельзя активировать. Если включён параметр [softDisabled]="false", отключённые элементы пропускаются при навигации с помощью клавиатуры.

Тестирование

Angular Aria предоставляет средства-обёртки компонентов для тестирования компонентов меню. Ниже приведён пример использования этих средств-обёрток в тесте компонента:

import {ComponentFixture, TestBed} from '@angular/core/testing';
import {HarnessLoader} from '@angular/cdk/testing';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {MenuHarness, MenuItemHarness} from '@angular/aria/menu/testing';
import {MyMenuComponent} from './my-menu'; // Your component

describe('MyMenuComponent', () => {
  let fixture: ComponentFixture<MyMenuComponent>;
  let loader: HarnessLoader;

  beforeEach(async () => {
    TestBed.configureTestingModule({
      imports: [MyMenuComponent],
    });

    fixture = TestBed.createComponent(MyMenuComponent);
    await fixture.whenStable();
    loader = TestbedHarnessEnvironment.loader(fixture);
  });

  it('should open menu and click item', async () => {
    // Load the menu harness by its trigger text
    const menu = await loader.getHarness(MenuHarness.with({triggerText: 'Open Menu'}));

    // Verify initial state
    expect(await menu.isOpen()).toBe(false);

    // Open the menu
    await menu.open();
    expect(await menu.isOpen()).toBe(true);

    // Get items
    const items = await menu.getItems();
    expect(items.length).toBe(3);
    expect(await items[0].getText()).toBe('Item 1');

    // Click first item
    await items[0].click();

    // Menu should close after selection (depending on your implementation)
    expect(await menu.isOpen()).toBe(false);
  });

  it('should interact with submenus', async () => {
    const menu = await loader.getHarness(MenuHarness.with({triggerText: 'Open Menu'}));
    await menu.open();

    // Get the item that triggers a submenu
    const subItem = await loader.getHarness(MenuItemHarness.with({text: 'Submenu'}));
    expect(await subItem.hasSubmenu()).toBe(true);

    // Open submenu
    await subItem.click();
    const submenu = await subItem.getSubmenu();
    expect(submenu).toBeTruthy();
    expect(await submenu!.isOpen()).toBe(true);

    // Interact with submenu items
    const subItems = await submenu!.getItems();
    expect(subItems.length).toBe(1);
  });
});

API

Menu

Директива-контейнер для элементов меню.

Входные параметры

Свойство Тип По умолчанию Описание
disabled boolean false Отключает все элементы меню
wrap boolean true Определяет, зацикливается ли навигация с помощью клавиатуры на краях
softDisabled boolean true Если включён параметр true, отключённые элементы могут получать фокус, но не реагируют на взаимодействие

Методы

Метод Параметры Описание
close нет Закрывает меню

MenuBar

Горизонтальный контейнер для нескольких меню.

Входные параметры

Свойство Тип По умолчанию Описание
disabled boolean false Отключает всю панель меню
wrap boolean true Определяет, зацикливается ли навигация с помощью клавиатуры на краях
softDisabled boolean true Если включён параметр true, отключённые элементы могут получать фокус, но не реагируют на взаимодействие

MenuItem

Отдельный элемент меню.

Входные параметры

Свойство Тип По умолчанию Описание
value any — Обязательный. Значение этого элемента
role 'menuitem' | 'menuitemcheckbox' | 'menuitemradio' 'menuitem' Роль ARIA для элемента меню
disabled boolean false Отключает этот элемент меню
submenu Menu — Ссылка на подменю
searchTerm string '' Поисковый запрос для поиска по мере ввода (поддерживает двустороннюю привязку)

Сигналы

Свойство Тип Описание
active Signal<boolean> Определяет, находится ли элемент сейчас в фокусе
expanded Signal<boolean> Определяет, раскрыто ли подменю
hasPopup Signal<boolean> Определяет, связано ли с элементом подменю

ПРИМЕЧАНИЕ: MenuItem не предоставляет общедоступных методов. Используйте входной параметр submenu, чтобы связать подменю с элементами меню.

MenuTrigger

Кнопка или элемент, открывающий меню.

Входные параметры

Свойство Тип По умолчанию Описание
menu Menu — Обязательный. Меню, которое нужно открыть
disabled boolean false Отключает триггер
softDisabled boolean true Если включён параметр true, отключённый триггер может получать фокус

Сигналы

Свойство Тип Описание
expanded Signal<boolean> Определяет, открыто ли сейчас меню
hasPopup Signal<boolean> Определяет, связано ли с триггером меню

Методы

Метод Параметры Описание
open нет Открывает меню
close нет Закрывает меню
toggle нет Открывает или закрывает меню

Super-powered by Google ©2010–2025.
Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0.
https://angular.dev/guide/aria/menu

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API