Меню
Обзор
Меню предлагает пользователям список действий или параметров и обычно появляется в ответ на щелчок кнопкой или правый щелчок. Меню поддерживают навигацию с помощью клавиатуры и клавиш со стрелками, подменю, флажки, переключатели и отключённые элементы.
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