Skip to content

Commit 1e0c1fc

Browse files
crisbetokara
authored andcommitted
feat(menu): add nested menu functionality (#5493)
1 parent 8c1e803 commit 1e0c1fc

File tree

9 files changed

+760
-140
lines changed

9 files changed

+760
-140
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 & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ import {FocusKeyManager} from '../core/a11y/focus-key-manager';
2929
import {MdMenuPanel} from './menu-panel';
3030
import {Subscription} from 'rxjs/Subscription';
3131
import {transformMenu, fadeInItems} from './menu-animations';
32-
import {ESCAPE} from '../core/keyboard/keycodes';
32+
import {ESCAPE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes';
33+
import {merge} from 'rxjs/observable/merge';
34+
import {Observable} from 'rxjs/Observable';
35+
import {Direction} from '../core';
3336

3437

3538
@Component({
@@ -59,6 +62,12 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
5962
/** Current state of the panel animation. */
6063
_panelAnimationState: 'void' | 'enter-start' | 'enter' = 'void';
6164

65+
/** Whether the menu is a sub-menu or a top-level menu. */
66+
isSubmenu: boolean = false;
67+
68+
/** Layout direction of the menu. */
69+
direction: Direction;
70+
6271
/** Position of the menu in the X axis. */
6372
@Input()
6473
get xPosition() { return this._xPosition; }
@@ -109,30 +118,45 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
109118
}
110119

111120
/** Event emitted when the menu is closed. */
112-
@Output() close = new EventEmitter<void>();
121+
@Output() close = new EventEmitter<void | 'click' | 'keydown'>();
113122

114123
constructor(private _elementRef: ElementRef) { }
115124

116125
ngAfterContentInit() {
117126
this._keyManager = new FocusKeyManager(this.items).withWrap();
118-
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this._emitCloseEvent());
127+
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.close.emit('keydown'));
119128
}
120129

121130
ngOnDestroy() {
122131
if (this._tabSubscription) {
123132
this._tabSubscription.unsubscribe();
124133
}
125134

126-
this._emitCloseEvent();
135+
this.close.emit();
127136
this.close.complete();
128137
}
129138

139+
/** Stream that emits whenever the hovered menu item changes. */
140+
hover(): Observable<MdMenuItem> {
141+
return merge(...this.items.map(item => item.hover));
142+
}
143+
130144
/** Handle a keyboard event from the menu, delegating to the appropriate action. */
131145
_handleKeydown(event: KeyboardEvent) {
132146
switch (event.keyCode) {
133147
case ESCAPE:
134-
this._emitCloseEvent();
135-
return;
148+
this.close.emit('keydown');
149+
break;
150+
case LEFT_ARROW:
151+
if (this.isSubmenu && this.direction === 'ltr') {
152+
this.close.emit('keydown');
153+
}
154+
break;
155+
case RIGHT_ARROW:
156+
if (this.isSubmenu && this.direction === 'rtl') {
157+
this.close.emit('keydown');
158+
}
159+
break;
136160
default:
137161
this._keyManager.onKeydown(event);
138162
}
@@ -146,14 +170,6 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
146170
this._keyManager.setFirstItemActive();
147171
}
148172

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-
157173
/**
158174
* It's necessary to set position-based classes to ensure the menu panel animation
159175
* 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: 4 additions & 2 deletions
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>;
17-
close: EventEmitter<void>;
18+
close: EventEmitter<void | 'click' | 'keydown'>;
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)