Skip to content

Commit 6cf7d5b

Browse files
committed
feat(menu): add nested menu functionality
Adds the ability for an `md-menu-item` inside of a `md-menu` to trigger another `md-menu`. This is a first step towards a `md-toolbar` component. Fixes #1429.
1 parent 6e0b848 commit 6cf7d5b

File tree

9 files changed

+711
-111
lines changed

9 files changed

+711
-111
lines changed

src/demo-app/menu/menu-demo.html

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,61 @@
1515
</md-menu>
1616
</div>
1717
<div class="menu-section">
18-
<p> Clicking these will navigate:</p>
18+
<p>Nested menu</p>
19+
20+
<md-toolbar>
21+
<button md-icon-button [mdMenuTriggerFor]="animals">
22+
<md-icon>more_vert</md-icon>
23+
</button>
24+
</md-toolbar>
25+
26+
<md-menu #animals="mdMenu">
27+
<button md-menu-item [mdMenuTriggerFor]="vertebrates">Vertebrates</button>
28+
<button md-menu-item [mdMenuTriggerFor]="invertebrates">Invertebrates</button>
29+
</md-menu>
30+
31+
<md-menu #vertebrates="mdMenu">
32+
<button md-menu-item [mdMenuTriggerFor]="fish">Fishes</button>
33+
<button md-menu-item [mdMenuTriggerFor]="amphibians">Amphibians</button>
34+
<button md-menu-item [mdMenuTriggerFor]="reptiles">Reptiles</button>
35+
<button md-menu-item>Birds</button>
36+
<button md-menu-item>Mammals</button>
37+
</md-menu>
38+
39+
<md-menu #invertebrates="mdMenu">
40+
<button md-menu-item>Insects</button>
41+
<button md-menu-item>Molluscs</button>
42+
<button md-menu-item>Crustaceans</button>
43+
<button md-menu-item>Corals</button>
44+
<button md-menu-item>Arachnids</button>
45+
<button md-menu-item>Velvet worms</button>
46+
<button md-menu-item>Horseshoe crabs</button>
47+
</md-menu>
48+
49+
<md-menu #fish="mdMenu">
50+
<button md-menu-item>Baikal oilfish</button>
51+
<button md-menu-item>Bala shark</button>
52+
<button md-menu-item>Ballan wrasse</button>
53+
<button md-menu-item>Bamboo shark</button>
54+
<button md-menu-item>Banded killifish</button>
55+
</md-menu>
56+
57+
<md-menu #amphibians="mdMenu">
58+
<button md-menu-item>Sonoran desert toad</button>
59+
<button md-menu-item>Western toad</button>
60+
<button md-menu-item>Arroyo toad</button>
61+
<button md-menu-item>Yosemite toad</button>
62+
</md-menu>
63+
64+
<md-menu #reptiles="mdMenu">
65+
<button md-menu-item>Banded Day Gecko</button>
66+
<button md-menu-item>Banded Gila Monster</button>
67+
<button md-menu-item>Black Tree Monitor</button>
68+
<button md-menu-item>Blue Spiny Lizard</button>
69+
</md-menu>
70+
</div>
71+
<div class="menu-section">
72+
<p>Clicking these will navigate:</p>
1973
<md-toolbar>
2074
<button md-icon-button [mdMenuTriggerFor]="anchorMenu" aria-label="Open anchor menu">
2175
<md-icon>more_vert</md-icon>

src/lib/menu/_menu-theme.scss

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@
2424
vertical-align: middle;
2525
}
2626

27-
&:hover:not([disabled]), &:focus:not([disabled]) {
27+
}
28+
29+
.mat-menu-item:hover,
30+
.mat-menu-item:focus,
31+
.mat-menu-item-highlighted {
32+
&:not([disabled]) {
2833
background: mat-color($background, 'hover');
2934
}
3035
}

src/lib/menu/menu-directive.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
ViewChild,
2020
ViewEncapsulation,
2121
ElementRef,
22+
Directive,
2223
} from '@angular/core';
2324
import {MenuPositionX, MenuPositionY} from './menu-positions';
2425
import {throwMdMenuInvalidPositionX, throwMdMenuInvalidPositionY} from './menu-errors';
@@ -27,7 +28,10 @@ import {FocusKeyManager} from '../core/a11y/focus-key-manager';
2728
import {MdMenuPanel} from './menu-panel';
2829
import {Subscription} from 'rxjs/Subscription';
2930
import {transformMenu, fadeInItems} from './menu-animations';
30-
import {ESCAPE} from '../core/keyboard/keycodes';
31+
import {ESCAPE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes';
32+
import {merge} from 'rxjs/observable/merge';
33+
import {Observable} from 'rxjs/Observable';
34+
import {Direction} from '../core';
3135

3236

3337
@Component({
@@ -53,6 +57,12 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
5357
/** Config object to be passed into the menu's ngClass */
5458
_classList: any = {};
5559

60+
/** Whether the menu is a sub-menu or a top-level menu. */
61+
isSubmenu: boolean = false;
62+
63+
/** Layout direction of the menu. */
64+
direction: Direction;
65+
5666
/** Position of the menu in the X axis. */
5767
@Input()
5868
get xPosition() { return this._xPosition; }
@@ -109,7 +119,7 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
109119

110120
ngAfterContentInit() {
111121
this._keyManager = new FocusKeyManager(this.items).withWrap();
112-
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this._emitCloseEvent());
122+
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.close.emit());
113123
}
114124

115125
ngOnDestroy() {
@@ -118,12 +128,27 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
118128
}
119129
}
120130

131+
/** Stream that emits whenever the hovered menu item changes. */
132+
get hover(): Observable<MdMenuItem> {
133+
return merge(...this.items.map(item => item.hover));
134+
}
135+
121136
/** Handle a keyboard event from the menu, delegating to the appropriate action. */
122137
_handleKeydown(event: KeyboardEvent) {
123138
switch (event.keyCode) {
124139
case ESCAPE:
125-
this._emitCloseEvent();
126-
return;
140+
this.close.emit();
141+
break;
142+
case LEFT_ARROW:
143+
if (this.isSubmenu && this.direction === 'ltr') {
144+
this.close.emit();
145+
}
146+
break;
147+
case RIGHT_ARROW:
148+
if (this.isSubmenu && this.direction === 'rtl') {
149+
this.close.emit();
150+
}
151+
break;
127152
default:
128153
this._keyManager.onKeydown(event);
129154
}
@@ -137,14 +162,6 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
137162
this._keyManager.setFirstItemActive();
138163
}
139164

140-
/**
141-
* This emits a close event to which the trigger is subscribed. When emitted, the
142-
* trigger will close the menu.
143-
*/
144-
_emitCloseEvent(): void {
145-
this.close.emit();
146-
}
147-
148165
/**
149166
* It's necessary to set position-based classes to ensure the menu panel animation
150167
* folds out from the correct direction.

src/lib/menu/menu-item.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Component, ElementRef} from '@angular/core';
9+
import {Component, ElementRef, OnDestroy} from '@angular/core';
1010
import {Focusable} from '../core/a11y/focus-key-manager';
1111
import {CanDisable, mixinDisabled} from '../core/common-behaviors/disabled';
12+
import {Subject} from 'rxjs/Subject';
1213

1314
// Boilerplate for applying mixins to MdMenuItem.
1415
/** @docs-private */
@@ -26,15 +27,22 @@ export const _MdMenuItemMixinBase = mixinDisabled(MdMenuItemBase);
2627
host: {
2728
'role': 'menuitem',
2829
'class': 'mat-menu-item',
30+
'[class.mat-menu-item-highlighted]': '_highlighted',
2931
'[attr.tabindex]': '_getTabIndex()',
3032
'[attr.aria-disabled]': 'disabled.toString()',
3133
'[attr.disabled]': 'disabled || null',
3234
'(click)': '_checkDisabled($event)',
35+
'(mouseenter)': '_emitHoverEvent()',
3336
},
3437
templateUrl: 'menu-item.html',
3538
exportAs: 'mdMenuItem'
3639
})
37-
export class MdMenuItem extends _MdMenuItemMixinBase implements Focusable, CanDisable {
40+
export class MdMenuItem extends _MdMenuItemMixinBase implements Focusable, CanDisable, OnDestroy {
41+
/** Stream that emits when the menu item is hovered. */
42+
hover: Subject<MdMenuItem> = new Subject();
43+
44+
/** Whether the menu item is highlighted. */
45+
_highlighted: boolean = false;
3846

3947
constructor(private _elementRef: ElementRef) {
4048
super();
@@ -45,6 +53,10 @@ export class MdMenuItem extends _MdMenuItemMixinBase implements Focusable, CanDi
4553
this._getHostElement().focus();
4654
}
4755

56+
ngOnDestroy() {
57+
this.hover.complete();
58+
}
59+
4860
/** Used to set the `tabindex`. */
4961
_getTabIndex(): string {
5062
return this.disabled ? '-1' : '0';
@@ -62,5 +74,13 @@ export class MdMenuItem extends _MdMenuItemMixinBase implements Focusable, CanDi
6274
event.stopPropagation();
6375
}
6476
}
77+
78+
/** Emits to the hover stream. */
79+
_emitHoverEvent() {
80+
if (!this.disabled) {
81+
this.hover.next(this);
82+
}
83+
}
84+
6585
}
6686

src/lib/menu/menu-panel.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@
88

99
import {EventEmitter, TemplateRef} from '@angular/core';
1010
import {MenuPositionX, MenuPositionY} from './menu-positions';
11+
import {Direction} from '../core';
1112

1213
export interface MdMenuPanel {
1314
xPosition: MenuPositionX;
1415
yPosition: MenuPositionY;
1516
overlapTrigger: boolean;
1617
templateRef: TemplateRef<any>;
1718
close: EventEmitter<void>;
19+
isSubmenu: boolean;
20+
direction: Direction;
1821
focusFirstItem: () => void;
1922
setPositionClasses: (x: MenuPositionX, y: MenuPositionY) => void;
20-
_emitCloseEvent: () => void;
2123
}

0 commit comments

Comments
 (0)