Skip to content

Commit 61239fd

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 c20bec8 commit 61239fd

File tree

9 files changed

+713
-106
lines changed

9 files changed

+713
-106
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: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
ViewEncapsulation,
2121
ElementRef,
2222
ChangeDetectionStrategy,
23+
Directive,
2324
} from '@angular/core';
2425
import {AnimationEvent} from '@angular/animations';
2526
import {MenuPositionX, MenuPositionY} from './menu-positions';
@@ -29,7 +30,10 @@ import {FocusKeyManager} from '../core/a11y/focus-key-manager';
2930
import {MdMenuPanel} from './menu-panel';
3031
import {Subscription} from 'rxjs/Subscription';
3132
import {transformMenu, fadeInItems} from './menu-animations';
32-
import {ESCAPE} from '../core/keyboard/keycodes';
33+
import {ESCAPE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes';
34+
import {merge} from 'rxjs/observable/merge';
35+
import {Observable} from 'rxjs/Observable';
36+
import {Direction} from '../core';
3337

3438

3539
@Component({
@@ -59,6 +63,12 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
5963
/** Current state of the panel animation. */
6064
_panelAnimationState: 'void' | 'enter-start' | 'enter' = 'void';
6165

66+
/** Whether the menu is a sub-menu or a top-level menu. */
67+
isSubmenu: boolean = false;
68+
69+
/** Layout direction of the menu. */
70+
direction: Direction;
71+
6272
/** Position of the menu in the X axis. */
6373
@Input()
6474
get xPosition() { return this._xPosition; }
@@ -115,24 +125,39 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
115125

116126
ngAfterContentInit() {
117127
this._keyManager = new FocusKeyManager(this.items).withWrap();
118-
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this._emitCloseEvent());
128+
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.close.emit());
119129
}
120130

121131
ngOnDestroy() {
122132
if (this._tabSubscription) {
123133
this._tabSubscription.unsubscribe();
124134
}
125135

126-
this._emitCloseEvent();
136+
this.close.emit();
127137
this.close.complete();
128138
}
129139

140+
/** Stream that emits whenever the hovered menu item changes. */
141+
hover(): Observable<MdMenuItem> {
142+
return merge(...this.items.map(item => item.hover));
143+
}
144+
130145
/** Handle a keyboard event from the menu, delegating to the appropriate action. */
131146
_handleKeydown(event: KeyboardEvent) {
132147
switch (event.keyCode) {
133148
case ESCAPE:
134-
this._emitCloseEvent();
135-
return;
149+
this.close.emit();
150+
break;
151+
case LEFT_ARROW:
152+
if (this.isSubmenu && this.direction === 'ltr') {
153+
this.close.emit();
154+
}
155+
break;
156+
case RIGHT_ARROW:
157+
if (this.isSubmenu && this.direction === 'rtl') {
158+
this.close.emit();
159+
}
160+
break;
136161
default:
137162
this._keyManager.onKeydown(event);
138163
}
@@ -146,14 +171,6 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
146171
this._keyManager.setFirstItemActive();
147172
}
148173

149-
/**
150-
* This emits a close event to which the trigger is subscribed. When emitted, the
151-
* trigger will close the menu.
152-
*/
153-
_emitCloseEvent(): void {
154-
this.close.emit();
155-
}
156-
157174
/**
158175
* It's necessary to set position-based classes to ensure the menu panel animation
159176
* 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, ChangeDetectionStrategy} from '@angular/core';
9+
import {Component, ElementRef, OnDestroy, ChangeDetectionStrategy} 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,16 +27,23 @@ 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
changeDetection: ChangeDetectionStrategy.OnPush,
3538
templateUrl: 'menu-item.html',
3639
exportAs: 'mdMenuItem',
3740
})
38-
export class MdMenuItem extends _MdMenuItemMixinBase implements Focusable, CanDisable {
41+
export class MdMenuItem extends _MdMenuItemMixinBase implements Focusable, CanDisable, OnDestroy {
42+
/** Stream that emits when the menu item is hovered. */
43+
hover: Subject<MdMenuItem> = new Subject();
44+
45+
/** Whether the menu item is highlighted. */
46+
_highlighted: boolean = false;
3947

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

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

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)