Skip to content

fix(cdk/menu): update docs to reflect current implementation and add correct role for triggers #24884

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/cdk/menu/menu-item-checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {CdkMenuItem} from './menu-item';
exportAs: 'cdkMenuItemCheckbox',
host: {
'role': 'menuitemcheckbox',
'[class.cdk-menu-item-checkbox]': 'true',
},
providers: [
{provide: CdkMenuItemSelectable, useExisting: CdkMenuItemCheckbox},
Expand Down
1 change: 1 addition & 0 deletions src/cdk/menu/menu-item-radio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ let nextId = 0;
exportAs: 'cdkMenuItemRadio',
host: {
'role': 'menuitemradio',
'[class.cdk-menu-item-radio]': 'true',
},
providers: [
{provide: CdkMenuItemSelectable, useExisting: CdkMenuItemRadio},
Expand Down
10 changes: 10 additions & 0 deletions src/cdk/menu/menu-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnDestroy {
@Optional() private readonly _directionality?: Directionality,
) {
super(injector, viewContainerRef, menuStack);
this._setRole();
this._registerCloseHandler();
this._subscribeToMenuStackClosed();
this._subscribeToMouseEnter();
Expand Down Expand Up @@ -326,4 +327,13 @@ export class CdkMenuTrigger extends CdkMenuTriggerBase implements OnDestroy {
});
}
}

/** Sets the role attribute for this trigger if needed. */
private _setRole() {
// If this trigger is part of another menu, the cdkMenuItem directive will handle setting the
// role, otherwise this is a standalone trigger, and we should ensure it has role="button".
if (!this._parentMenu) {
this._elementRef.nativeElement.setAttribute('role', 'button');
}
}
}
231 changes: 122 additions & 109 deletions src/cdk/menu/menu.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,68 +10,113 @@ directives apply their associated ARIA roles to their host element.
The directives in `@angular/cdk/menu` set the appropriate roles on their host element.

| Directive | ARIA Role |
| ------------------- | ---------------- |
|---------------------|------------------|
| CdkMenuBar | menubar |
| CdkMenu | menu |
| CdkMenuGroup | group |
| CdkMenuItem | menuitem |
| CdkMenuItemRadio | menuitemradio |
| CdkMenuItemCheckbox | menuitemcheckbox |
| CdkMenuTrigger | button |

### CSS Styles and Classes

The `@angular/cdk/menu` is designed to be highly customizable to your needs. It therefore does not
make any assumptions about how elements should be styled. You are expected to apply any required
CSS styles, but the directives do apply CSS classes to make it easier for you to add custom styles.
The available CSS classes are listed below, by directive.

| Directive | CSS Class | Applied... |
|:----------------------|--------------------------|------------------------------------------------|
| `cdkMenu` | `cdk-menu` | Always |
| `cdkMenu` | `cdk-menu-inline` | If the menu is an [inline menu](#menu-content) |
| `cdkMenuBar` | `cdk-menu-bar` | Always |
| `cdkMenuGroup` | `cdk-menu-group` | Always |
| `cdkMenuItem` | `cdk-menu-item` | Always |
| `cdkMenuItemCheckbox` | `cdk-menu-item` | Always |
| `cdkMenuItemCheckbox` | `cdk-menu-item-checkbox` | Always |
| `cdkMenuItemRadio` | `cdk-menu-item` | Always |
| `cdkMenuItemRadio` | `cdk-menu-item-radio` | Always |
| `cdkMenuTriggerFor` | `cdk-menu-trigger` | Always |

### Getting started

Import the `CdkMenuModule` into the `NgModule` in which you want to create menus. You can then apply
menu directives to build your custom menu. A typical menu consists of the following directives:

- `cdkMenuTriggerFor` - links a trigger button to a menu you intend to open
- `cdkMenuPanel` - wraps the menu and provides a link between the `cdkMenuTriggerFor` and the
`cdkMenu`
- `cdkMenu` - the actual menu you want to open
- `cdkMenuItem` - added to each button
- `cdkMenuTriggerFor` - links a trigger element to an `ng-template` containing the menu to be opened
- `cdkMenu` - creates the menu content opened by the trigger
- `cdkMenuItem` - added to each item in the menu

<!-- example({
"example": "cdk-menu-standalone-menu",
"file": "cdk-menu-standalone-menu-example.html"
"example": "cdk-menu-standalone-trigger",
"file": "cdk-menu-standalone-trigger-example.html"
}) -->

Most menu interactions consist of two parts: a trigger and a menu panel.

#### Triggers

You must add the `cdkMenuItem` and `cdkMenuTriggerFor` directives to triggers like so,
You can add `cdkMenuTriggerFor` to any button to make it a trigger for the given menu, or any menu
item to make it a trigger for a submenu. When adding this directive, be sure to pass a reference to
the template containing the menu it should open. Users can toggle the associated menu using a mouse
or keyboard.

<!-- example({"example":"cdk-menu-standalone-trigger",
"file":"cdk-menu-standalone-trigger-example.html",
"region":"trigger"}) -->

```html
<button cdkMenuItem [cdkMenuTriggerFor]="menu">Click me!</button>
```
When creating a submenu trigger, add both `cdkMenuItem` and `cdkMenuTriggerFor` like so,

Adding `cdkMenuItem` gives you keyboard navigation and focus management. Associating a trigger with
a menu is done through the `cdkMenuTriggerFor` directive and you must provide a template reference
variable to it. Once both of these directives are set, you can toggle the associated menu
programmatically, using a mouse or using a keyboard.
<!-- example({"example":"cdk-menu-menubar",
"file":"cdk-menu-menubar-example.html",
"region":"file-trigger"}) -->

#### Menu panels
#### Menu content

You must wrap pop-up menus with an `ng-template` with the `cdkMenuPanel` directive and a reference
variable which must be of type `cdkMenuPanel`. Further, the `cdkMenu` must also reference the
`cdkMenuPanel`.
There are two types of menus:
* _inline menus_ are always present on the page
* _pop-up menus_ can be toggled to hide or show by the user

```html
<ng-template cdkMenuPanel #panel="cdkMenuPanel">
<div cdkMenu [cdkMenuPanel]="panel">
<!-- some content -->
</div>
</ng-template>
```
You can create menus by marking their content element with the `cdkMenu` or `cdkMenuBar`
directives. You can create several types of menu interaction which are discussed below.

All type of menus should exclusively contain elements with role `menuitem`, `menuitemcheckbox`,
`menuitemradio`, or `group`. Supporting directives that automatically apply these roles are
discussed below.

Note that Angular CDK provides no styles; you must add styles as part of building your custom menu.

### Inline Menus

An _inline menu_ is a menu that lives directly on the page rather than in a pop-up associated with a
trigger. You can use an inline menu when you want a persistent menu interaction on a page. Menu
items within an inline menus are logically grouped together, and you can navigate through them
using your keyboard. You can create an inline menu by adding the `cdkMenu` directive to the element
you want to serve as the menu content.

<!-- example({
"example": "cdk-menu-inline",
"file": "cdk-menu-inline-example.html"
}) -->

### Pop-up Menus

You can create pop-up menus using the `cdkMenu` directive as well. Add this directive to the
element you want to serve as the content for your pop-up menu. Then wrap the content element in an
`ng-template` and reference the template from the `cdkMenuTriggerFor` property of the trigger. This
will allow the trigger to show and hide the menu content as needed.

<!-- example({
"example": "cdk-menu-standalone-menu",
"file": "cdk-menu-standalone-menu-example.html"
}) -->

### Menu Bars

The `CdkMenuBar` directive follows the [ARIA menubar][menubar] spec and behaves similar to a desktop
app menubar. It consists of at least one `CdkMenuItem` which triggers a submenu. A menubar can be
layed out horizontally or vertically (defaulting to horizontal). If the layout changes, you must set
the `orientation` attribute to match in order for the keyboard navigation to work properly and for
menus to open up in the correct location.
Menu bars are a type of inline menu that you can create using the `cdkMenuBar` directive. They
follow the [ARIA menubar][menubar] spec and behave similarly to a desktop application menubar. Each
bar consists of at least one `cdkMenuItem` that triggers a submenu.

<!-- example({
"example": "cdk-menu-menubar",
Expand All @@ -80,7 +125,8 @@ menus to open up in the correct location.

### Context Menus

A context menu opens when a user right-clicks within some container element. You can mark a
A context menus is a type of pop-up menu that doesn't have a traditional trigger element, instead
it is triggered when a user right-clicks within some container element. You can mark a
container element with the `cdkContextMenuTriggerFor`, which behaves like `cdkMenuTriggerFor` except
that it responds to the browser's native `contextmenu` event. Custom context menus appear next to
the cursor, similarly to native context menus.
Expand All @@ -93,69 +139,58 @@ the cursor, similarly to native context menus.
You can nest context menu container elements. Upon right-click, the menu associated with the closest
container element will open.

```html
<div [cdkContextMenuTriggerFor]="outer">
My outer context
<div [cdkContextMenuTriggerFor]="inner">My inner context</div>
</div>
```

In the example above, right clicking on "My inner context" will open up the "inner" menu and right
clicking inside "My outer context" will open up the "outer" menu.

### Inline Menus

An _inline menu_ is a menu that lives directly on the page rather than a pop-up associated with a
trigger. You can use an inline menu when you want a persistent menu interaction on a page. Menu
items within an inline menus are logically grouped together and you can navigate through them using
your keyboard.

<!-- example({
"example": "cdk-menu-inline",
"file": "cdk-menu-inline-example.html"
"example": "cdk-menu-nested-context",
"file": "cdk-menu-nested-context-example.html",
"region": "triggers"
}) -->

### Menu Items

Both menu and menubar elements should exclusively contain menuitem elements. This directive allows
the items to be navigated to via keyboard interaction.
In the example above, right-clicking on "Inner context menu" will open up the "inner" menu and
right-clicking inside "Outer context menu" will open up the "outer" menu.

A menuitem by itself can provide some user defined action by hooking into the `cdkMenuItemTriggered`
output. An example may be a close button which performs some closing logic.
### Menu Items

```html
<ng-template cdkMenuPanel #panel="cdkMenuPanel">
<div cdkMenu [cdkMenuPanel]="panel">
<button cdkMenuItem (cdkMenuItemTriggered)="closeApp()">Close</button>
</div>
</ng-template>
```
The `cdkMenuItem` directive allows users to navigate menu items via keyboard.
You can add a custom action to a menu item with the `cdkMenuItemTriggered` output.

You can create nested menus by using a menuitem as the trigger for another menu.
<!-- example({"example":"cdk-menu-standalone-stateful-menu",
"file":"cdk-menu-standalone-stateful-menu-example.html",
"region":"reset-item"}) -->

```html
<ng-template cdkMenuPanel #panel="cdkMenuPanel">
<div cdkMenu [cdkMenuPanel]="panel">
<button cdkMenuItem [cdkMenuTriggerFor]="submenu">Open Submenu</button>
</div>
</ng-template>
```
You can create nested menus by using a menu item as the trigger for another menu.

A menuitem also has two sub-types, neither of which should trigger a menu: CdkMenuItemCheckbox and
CdkMenuItemRadio
<!-- example({"example":"cdk-menu-menubar",
"file":"cdk-menu-menubar-example.html",
"region":"file-trigger"}) -->

#### Menu Item Checkboxes

A `cdkMenuItemCheckbox` is a special type of menuitem that behaves as a checkbox. You can use this
type of menuitem to toggle items on and off. An element with the `cdkMenuItemCheckbox` directive
A `cdkMenuItemCheckbox` is a special type of menu item that behaves as a checkbox. You can use this
type of menu item to toggle items on and off. An element with the `cdkMenuItemCheckbox` directive
does not need the additional `cdkMenuItem` directive.

Checkbox items do not track their own state. You must bind the checked state using the
`cdkMenuItemChecked` input and listen to `cdkMenuItemTriggered` to know when it is toggled. If you
don't bind the state it will reset when the menu is closed and re-opened.

<!-- example({"example":"cdk-menu-standalone-stateful-menu",
"file":"cdk-menu-standalone-stateful-menu-example.html",
"region":"bold-item"}) -->

#### Menu Item Radios

A `cdkMenuItemRadio` is a special type of menuitem that behaves as a radio button. You can use this
type of menuitem for menus with exclusively selectable items. An element with the `cdkMenuItemRadio`
A `cdkMenuItemRadio` is a special type of menu item that behaves as a radio button. You can use this
type of menu item for menus with exclusively selectable items. An element with the `cdkMenuItemRadio`
directive does not need the additional `cdkMenuItem` directive.

As with checkbox items, radio items do not track their own state, but you can track it by binding
`cdkMenuItemChecked` and listening for `cdkMenuItemTriggered`. If you do not bind the state the
selection will reset when the menu is closed and reopened.

<!-- example({"example":"cdk-menu-standalone-stateful-menu",
"file":"cdk-menu-standalone-stateful-menu-example.html",
"region":"size-items"}) -->

#### Groups

By default `cdkMenu` acts as a group for `cdkMenuItemRadio` elements. Elements with
Expand All @@ -165,40 +200,18 @@ can have the checked state.
If you would like to have unrelated groups of radio buttons within a single menu you should use the
`cdkMenuGroup` directive.

```html
<ng-template cdkMenuPanel #panel="cdkMenuPanel">
<div cdkMenu [cdkMenuPanel]="panel">
<!-- Font size -->
<div cdkMenuGroup>
<button cdkMenuItemRadio>Small</button>
<button cdkMenuItemRadio>Medium</button>
<button cdkMenuItemRadio>Large</button>
</div>
<hr />
<!-- Paragraph alignment -->
<div cdkMenuGroup>
<button cdkMenuItemRadio>Left</button>
<button cdkMenuItemRadio>Center</button>
<button cdkMenuItemRadio>Right</button>
</div>
</div>
</ng-template>
```

Note however that when the menu is closed and reopened any state is lost. You must subscribe to the
groups `change` output, or to `cdkMenuItemToggled` on each radio item and track changes your self.
Finally, you can provide state for each item using the `checked` attribute.

<!-- example({
"example": "cdk-menu-standalone-stateful-menu",
"file": "cdk-menu-standalone-stateful-menu-example.html"
}) -->

### Smart Menu Aim

`@angular/cdk/menu` intelligently predicts when a user intends to navigate to an open submenu and
prevent premature closeouts. This functionality prevents users from having to hunt through the open
menus in a maze-like fashion to reach their destination.
`@angular/cdk/menu` is capable of intelligently predicting when a user intends to navigate to an
open submenu and preventing premature closeouts. This functionality prevents users from having to
hunt through the open menus in a maze-like fashion to reach their destination. To enable this
feature for a menu and its sub-menus, add the `cdkMenuTargetAim` directive to the `cdkMenu` or
`cdkMenuBar` element.

![menu aim diagram][diagram]

Expand All @@ -208,8 +221,8 @@ service if it can perform its close actions. In order to determine if the curren
closed out, the Menu Aim service calculates the slope between a selected target coordinate in the
submenu and the previous mouse point, and the slope between the target and the current mouse point.
If the slope of the current mouse point is greater than the slope of the previous that means the
user is moving towards the submenu and we shouldn't close out. Users however may sometimes stop
short in a sibling item after moving towards the submenu. The service is intelligent enough the
user is moving towards the submenu, so we shouldn't close out. Users however may sometimes stop
short in a sibling item after moving towards the submenu. The service is intelligent enough to
detect this intention and will trigger the next menu.

### Accessibility
Expand All @@ -227,4 +240,4 @@ Finally, keyboard interaction is supported as defined in the [ARIA menubar keybo
[keyboard]:
https://www.w3.org/TR/wai-aria-practices-1.1/#keyboard-interaction-12
'ARIA Menubar Keyboard Interaction'
[diagram]: menuaim.png 'Menu Aim Diagram'
[diagram]: https://material.angular.io/assets/img/menuaim.png 'Menu Aim Diagram'
Binary file removed src/cdk/menu/menuaim.png
Binary file not shown.
Loading