Vue d'ensemble
Un menu propose une liste d'actions ou d'options aux utilisateurs et apparaît généralement en réponse à un clic sur un bouton ou à un clic droit. Les menus prennent en charge la navigation au clavier avec les touches fléchées, les sous-menus, les cases à cocher, les boutons radio et les éléments désactivés.
app.ts
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');
}
app.html
<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>
app.css
@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;
}
app.ts
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');
}
app.html
<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>
app.css
@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;
}
app.ts
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');
}
app.html
<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>
app.css
@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;
}
Utilisation
Les menus conviennent bien pour présenter des listes d'actions ou de commandes parmi lesquelles les utilisateurs peuvent choisir.
Utilisez un menu pour :
- Construire des menus de commandes d'application (Fichier, Édition, Affichage)
- Créer des menus contextuels (actions au clic droit)
- Afficher des listes d'actions déroulantes
- Implémenter des listes déroulantes dans une toolbar
- Organiser des paramètres ou des options
Évitez les menus pour :
- Construire la navigation d'un site (utilisez plutôt des landmarks de navigation)
- Créer des listes de sélection dans un formulaire (utilisez le composant Select)
- Basculer entre des panneaux de contenu (utilisez des tabs)
- Afficher du contenu repliable (utilisez un accordion)
Fonctionnalités
- Navigation au clavier - Touches fléchées, Home/End et recherche par caractère pour une navigation efficace
- Sous-menus - Prise en charge des menus imbriqués avec positionnement automatique
- Types de menus - Menus standalone, menus avec déclencheur et menubars
- Cases à cocher et boutons radio - Éléments de menu à bascule et de sélection
- Éléments désactivés - États désactivés souples ou stricts avec gestion du focus
- Fermeture automatique - Fermeture configurable lors de la sélection
- Prise en charge du RTL - Navigation pour les langues de droite à gauche
Exemples
Menu avec déclencheur
Créez un menu déroulant en associant un bouton déclencheur à un menu. Le déclencheur ouvre et ferme le menu.
app.ts
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');
}
app.html
<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>
app.css
@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;
}
app.ts
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');
}
app.html
<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>
app.css
@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;
}
app.ts
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');
}
app.html
<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>
app.css
@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;
}
Le menu se ferme automatiquement lorsque l'utilisateur sélectionne un élément ou appuie sur Échap.
Menu contextuel
Les menus contextuels apparaissent à la position du curseur lorsque les utilisateurs font un clic droit sur un élément.
app.ts
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();
}
lastAction = signal<string | null>(null);
onItemSelected(value: string) {
this.lastAction.set(value);
}
onOverlayKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
this.closeContextMenu();
}
}
closeContextMenu() {
if (this.trigger().expanded()) {
this.trigger().close();
}
}
}
app.html
<!-- Target area for right-click -->
<div (contextmenu)="onContextMenu($event)" class="context-zone">
Right-click inside this area to open the context menu.
</div>
@if (lastAction()) {
<p class="last-action">
<span class="material-symbols-outlined" translate="no" aria-hidden="true">check_circle</span>
Action performed: <strong>{{ lastAction() }}</strong>
</p>
}
<!-- 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',
hasBackdrop: true,
backdropClass: 'cdk-overlay-transparent-backdrop',
}"
(backdropClick)="closeContextMenu()"
(overlayKeydown)="onOverlayKeydown($event)"
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>
Positionnez le menu à l'aide des coordonnées de l'événement contextmenu.
Menu standalone
Un menu standalone ne nécessite pas de déclencheur et reste visible dans l'interface.
app.ts
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');
}
app.html
<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>
app.css
@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;
}
app.ts
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');
}
app.html
<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>
app.css
@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;
}
app.ts
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');
}
app.html
<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>
app.css
@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;
}
Les menus standalone conviennent bien aux listes d'actions ou à une navigation toujours visibles.
Éléments de menu désactivés
Désactivez des éléments de menu spécifiques avec l'input disabled. Contrôlez le comportement du focus avec softDisabled.
app.ts
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');
}
app.html
<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>
app.css
@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;
}
app.ts
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');
}
app.html
<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>
app.css
@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;
}
app.ts
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');
}
app.html
<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>
app.css
@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;
}
Avec [softDisabled]="true", les éléments désactivés peuvent recevoir le focus mais ne peuvent pas être activés. Avec [softDisabled]="false", les éléments désactivés sont ignorés pendant la navigation au clavier.
Tests
Angular Aria fournit des harnais de composant pour tester les composants menu. Voici un exemple d'utilisation des harnais dans un test de composant :
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);
});
});
Référence de l'API
Pour une documentation détaillée de l'API, consultez les références suivantes :