Skip to content

Commit

Permalink
feat(menu): add nested menu functionality (#5493)
Browse files Browse the repository at this point in the history
  • Loading branch information
crisbeto authored and kara committed Jul 20, 2017
1 parent 8c1e803 commit 1e0c1fc
Show file tree
Hide file tree
Showing 9 changed files with 760 additions and 140 deletions.
56 changes: 55 additions & 1 deletion src/demo-app/menu/menu-demo.html
Expand Up @@ -15,7 +15,61 @@
</md-menu>
</div>
<div class="menu-section">
<p> Clicking these will navigate:</p>
<p>Nested menu</p>

<md-toolbar>
<button md-icon-button [mdMenuTriggerFor]="animals">
<md-icon>more_vert</md-icon>
</button>
</md-toolbar>

<md-menu #animals="mdMenu">
<button md-menu-item [mdMenuTriggerFor]="vertebrates">Vertebrates</button>
<button md-menu-item [mdMenuTriggerFor]="invertebrates">Invertebrates</button>
</md-menu>

<md-menu #vertebrates="mdMenu">
<button md-menu-item [mdMenuTriggerFor]="fish">Fishes</button>
<button md-menu-item [mdMenuTriggerFor]="amphibians">Amphibians</button>
<button md-menu-item [mdMenuTriggerFor]="reptiles">Reptiles</button>
<button md-menu-item>Birds</button>
<button md-menu-item>Mammals</button>
</md-menu>

<md-menu #invertebrates="mdMenu">
<button md-menu-item>Insects</button>
<button md-menu-item>Molluscs</button>
<button md-menu-item>Crustaceans</button>
<button md-menu-item>Corals</button>
<button md-menu-item>Arachnids</button>
<button md-menu-item>Velvet worms</button>
<button md-menu-item>Horseshoe crabs</button>
</md-menu>

<md-menu #fish="mdMenu">
<button md-menu-item>Baikal oilfish</button>
<button md-menu-item>Bala shark</button>
<button md-menu-item>Ballan wrasse</button>
<button md-menu-item>Bamboo shark</button>
<button md-menu-item>Banded killifish</button>
</md-menu>

<md-menu #amphibians="mdMenu">
<button md-menu-item>Sonoran desert toad</button>
<button md-menu-item>Western toad</button>
<button md-menu-item>Arroyo toad</button>
<button md-menu-item>Yosemite toad</button>
</md-menu>

<md-menu #reptiles="mdMenu">
<button md-menu-item>Banded Day Gecko</button>
<button md-menu-item>Banded Gila Monster</button>
<button md-menu-item>Black Tree Monitor</button>
<button md-menu-item>Blue Spiny Lizard</button>
</md-menu>
</div>
<div class="menu-section">
<p>Clicking these will navigate:</p>
<md-toolbar>
<button md-icon-button [mdMenuTriggerFor]="anchorMenu" aria-label="Open anchor menu">
<md-icon>more_vert</md-icon>
Expand Down
7 changes: 6 additions & 1 deletion src/lib/menu/_menu-theme.scss
Expand Up @@ -24,7 +24,12 @@
vertical-align: middle;
}

&:hover:not([disabled]), &:focus:not([disabled]) {
}

.mat-menu-item:hover,
.mat-menu-item:focus,
.mat-menu-item-highlighted {
&:not([disabled]) {
background: mat-color($background, 'hover');
}
}
Expand Down
44 changes: 30 additions & 14 deletions src/lib/menu/menu-directive.ts
Expand Up @@ -29,7 +29,10 @@ import {FocusKeyManager} from '../core/a11y/focus-key-manager';
import {MdMenuPanel} from './menu-panel';
import {Subscription} from 'rxjs/Subscription';
import {transformMenu, fadeInItems} from './menu-animations';
import {ESCAPE} from '../core/keyboard/keycodes';
import {ESCAPE, LEFT_ARROW, RIGHT_ARROW} from '../core/keyboard/keycodes';
import {merge} from 'rxjs/observable/merge';
import {Observable} from 'rxjs/Observable';
import {Direction} from '../core';


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

/** Whether the menu is a sub-menu or a top-level menu. */
isSubmenu: boolean = false;

/** Layout direction of the menu. */
direction: Direction;

/** Position of the menu in the X axis. */
@Input()
get xPosition() { return this._xPosition; }
Expand Down Expand Up @@ -109,30 +118,45 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
}

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

constructor(private _elementRef: ElementRef) { }

ngAfterContentInit() {
this._keyManager = new FocusKeyManager(this.items).withWrap();
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this._emitCloseEvent());
this._tabSubscription = this._keyManager.tabOut.subscribe(() => this.close.emit('keydown'));
}

ngOnDestroy() {
if (this._tabSubscription) {
this._tabSubscription.unsubscribe();
}

this._emitCloseEvent();
this.close.emit();
this.close.complete();
}

/** Stream that emits whenever the hovered menu item changes. */
hover(): Observable<MdMenuItem> {
return merge(...this.items.map(item => item.hover));
}

/** Handle a keyboard event from the menu, delegating to the appropriate action. */
_handleKeydown(event: KeyboardEvent) {
switch (event.keyCode) {
case ESCAPE:
this._emitCloseEvent();
return;
this.close.emit('keydown');
break;
case LEFT_ARROW:
if (this.isSubmenu && this.direction === 'ltr') {
this.close.emit('keydown');
}
break;
case RIGHT_ARROW:
if (this.isSubmenu && this.direction === 'rtl') {
this.close.emit('keydown');
}
break;
default:
this._keyManager.onKeydown(event);
}
Expand All @@ -146,14 +170,6 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
this._keyManager.setFirstItemActive();
}

/**
* This emits a close event to which the trigger is subscribed. When emitted, the
* trigger will close the menu.
*/
_emitCloseEvent(): void {
this.close.emit();
}

/**
* It's necessary to set position-based classes to ensure the menu panel animation
* folds out from the correct direction.
Expand Down
24 changes: 22 additions & 2 deletions src/lib/menu/menu-item.ts
Expand Up @@ -6,9 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/

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

// Boilerplate for applying mixins to MdMenuItem.
/** @docs-private */
Expand All @@ -26,16 +27,23 @@ export const _MdMenuItemMixinBase = mixinDisabled(MdMenuItemBase);
host: {
'role': 'menuitem',
'class': 'mat-menu-item',
'[class.mat-menu-item-highlighted]': '_highlighted',
'[attr.tabindex]': '_getTabIndex()',
'[attr.aria-disabled]': 'disabled.toString()',
'[attr.disabled]': 'disabled || null',
'(click)': '_checkDisabled($event)',
'(mouseenter)': '_emitHoverEvent()',
},
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'menu-item.html',
exportAs: 'mdMenuItem',
})
export class MdMenuItem extends _MdMenuItemMixinBase implements Focusable, CanDisable {
export class MdMenuItem extends _MdMenuItemMixinBase implements Focusable, CanDisable, OnDestroy {
/** Stream that emits when the menu item is hovered. */
hover: Subject<MdMenuItem> = new Subject();

/** Whether the menu item is highlighted. */
_highlighted: boolean = false;

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

ngOnDestroy() {
this.hover.complete();
}

/** Used to set the `tabindex`. */
_getTabIndex(): string {
return this.disabled ? '-1' : '0';
Expand All @@ -63,5 +75,13 @@ export class MdMenuItem extends _MdMenuItemMixinBase implements Focusable, CanDi
event.stopPropagation();
}
}

/** Emits to the hover stream. */
_emitHoverEvent() {
if (!this.disabled) {
this.hover.next(this);
}
}

}

6 changes: 4 additions & 2 deletions src/lib/menu/menu-panel.ts
Expand Up @@ -8,14 +8,16 @@

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

export interface MdMenuPanel {
xPosition: MenuPositionX;
yPosition: MenuPositionY;
overlapTrigger: boolean;
templateRef: TemplateRef<any>;
close: EventEmitter<void>;
close: EventEmitter<void | 'click' | 'keydown'>;
isSubmenu?: boolean;
direction?: Direction;
focusFirstItem: () => void;
setPositionClasses: (x: MenuPositionX, y: MenuPositionY) => void;
_emitCloseEvent: () => void;
}

0 comments on commit 1e0c1fc

Please sign in to comment.