-
Notifications
You must be signed in to change notification settings - Fork 6.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(menu): add menu trigger support (#867)
* feat(menu): add menu trigger support * addressed comments * lazy create menu
- Loading branch information
1 parent
f0965ba
commit 9a32489
Showing
19 changed files
with
432 additions
and
41 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
describe('menu', function () { | ||
beforeEach(function() { | ||
browser.get('/menu'); | ||
}); | ||
|
||
it('should open menu when the trigger is clicked', function () { | ||
expectMenuPresent(false); | ||
element(by.id('trigger')).click(); | ||
|
||
expectMenuPresent(true); | ||
expect(element(by.css('.md-menu')).getText()).toEqual("One\nTwo\nThree"); | ||
}); | ||
|
||
it('should align menu when open', function() { | ||
element(by.id('trigger')).click(); | ||
expectMenuAlignedWith('trigger'); | ||
}); | ||
|
||
it('should close menu when area outside menu is clicked', function () { | ||
element(by.id('trigger')).click(); | ||
element(by.tagName('body')).click(); | ||
expectMenuPresent(false); | ||
}); | ||
|
||
it('should close menu when menu item is clicked', function () { | ||
element(by.id('trigger')).click(); | ||
element(by.id('one')).click(); | ||
expectMenuPresent(false); | ||
}); | ||
|
||
it('should run click handlers on regular menu items', function() { | ||
element(by.id('trigger')).click(); | ||
element(by.id('one')).click(); | ||
expect(element(by.id('text')).getText()).toEqual('one'); | ||
|
||
element(by.id('trigger')).click(); | ||
element(by.id('two')).click(); | ||
expect(element(by.id('text')).getText()).toEqual('two'); | ||
}); | ||
|
||
it('should run not run click handlers on disabled menu items', function() { | ||
element(by.id('trigger')).click(); | ||
element(by.id('three')).click(); | ||
expect(element(by.id('text')).getText()).toEqual(''); | ||
}); | ||
|
||
it('should support multiple triggers opening the same menu', function() { | ||
element(by.id('trigger-two')).click(); | ||
expect(element(by.css('.md-menu')).getText()).toEqual("One\nTwo\nThree"); | ||
expectMenuAlignedWith('trigger-two'); | ||
|
||
element(by.tagName('body')).click(); | ||
expectMenuPresent(false); | ||
|
||
element(by.id('trigger')).click(); | ||
expect(element(by.css('.md-menu')).getText()).toEqual("One\nTwo\nThree"); | ||
expectMenuAlignedWith('trigger'); | ||
|
||
element(by.tagName('body')).click(); | ||
expectMenuPresent(false); | ||
}); | ||
|
||
function expectMenuPresent(bool: boolean) { | ||
return browser.isElementPresent(by.css('.md-menu')).then((isPresent) => { | ||
expect(isPresent).toBe(bool); | ||
}); | ||
} | ||
|
||
function expectMenuAlignedWith(id: string) { | ||
element(by.id(id)).getLocation().then((loc) => { | ||
expectMenuLocation({x: loc.x, y: loc.y}); | ||
}); | ||
} | ||
|
||
function expectMenuLocation({x,y}: {x: number, y: number}) { | ||
element(by.css('.md-menu')).getLocation().then((loc) => { | ||
expect(loc.x).toEqual(x); | ||
expect(loc.y).toEqual(y); | ||
}); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import {MdError} from '@angular2-material/core/errors/error'; | ||
|
||
/** | ||
* Exception thrown when menu trigger doesn't have a valid md-menu instance | ||
*/ | ||
export class MdMenuMissingError extends MdError { | ||
constructor() { | ||
super(`md-menu-trigger: must pass in an md-menu instance. | ||
Example: | ||
<md-menu #menu="mdMenu"></md-menu> | ||
<button [md-menu-trigger-for]="menu"></button> | ||
`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import {Directive, Input, HostBinding} from '@angular/core'; | ||
|
||
/** | ||
* This directive is intended to be used inside an md-menu tag. | ||
* It exists mostly to set the role attribute. | ||
*/ | ||
@Directive({ | ||
selector: 'button[md-menu-item]', | ||
host: {'role': 'menuitem'} | ||
}) | ||
export class MdMenuItem {} | ||
|
||
/** | ||
* This directive is intended to be used inside an md-menu tag. | ||
* It sets the role attribute and adds support for the disabled property to anchors. | ||
*/ | ||
@Directive({ | ||
selector: 'a[md-menu-item]', | ||
host: { | ||
'role': 'menuitem', | ||
'(click)': 'checkDisabled($event)' | ||
} | ||
}) | ||
export class MdMenuAnchor { | ||
_disabled: boolean; | ||
|
||
@HostBinding('attr.disabled') | ||
@Input() | ||
get disabled(): boolean { | ||
return this._disabled; | ||
} | ||
|
||
set disabled(value: boolean) { | ||
this._disabled = (value === false || value === undefined) ? null : true; | ||
} | ||
|
||
@HostBinding('attr.aria-disabled') | ||
get isAriaDisabled(): string { | ||
return String(this.disabled); | ||
} | ||
|
||
@HostBinding('tabIndex') | ||
get tabIndex(): number { | ||
return this.disabled ? -1 : 0; | ||
} | ||
|
||
checkDisabled(event: Event) { | ||
if (this.disabled) { | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import { | ||
Directive, | ||
ElementRef, | ||
Input, | ||
Output, | ||
EventEmitter, | ||
HostListener, | ||
ViewContainerRef, | ||
AfterViewInit, | ||
OnDestroy | ||
} from '@angular/core'; | ||
import {MdMenu} from './menu'; | ||
import {MdMenuItem, MdMenuAnchor} from './menu-item'; | ||
import {MdMenuMissingError} from './menu-errors'; | ||
import { | ||
Overlay, | ||
OverlayState, | ||
OverlayRef, | ||
OVERLAY_PROVIDERS, | ||
TemplatePortal | ||
} from '@angular2-material/core/core'; | ||
import { | ||
ConnectedPositionStrategy | ||
} from '@angular2-material/core/overlay/position/connected-position-strategy'; | ||
|
||
/** | ||
* This directive is intended to be used in conjunction with an md-menu tag. It is | ||
* responsible for toggling the display of the provided menu instance. | ||
*/ | ||
@Directive({ | ||
selector: '[md-menu-trigger-for]', | ||
host: {'aria-haspopup': 'true'}, | ||
providers: [OVERLAY_PROVIDERS], | ||
exportAs: 'mdMenuTrigger' | ||
}) | ||
export class MdMenuTrigger implements AfterViewInit, OnDestroy { | ||
private _portal: TemplatePortal; | ||
private _overlayRef: OverlayRef; | ||
menuOpen: boolean = false; | ||
|
||
@Input('md-menu-trigger-for') menu: MdMenu; | ||
@Output() onMenuOpen = new EventEmitter(); | ||
@Output() onMenuClose = new EventEmitter(); | ||
|
||
constructor(private _overlay: Overlay, private _element: ElementRef, | ||
private _viewContainerRef: ViewContainerRef) {} | ||
|
||
ngAfterViewInit() { | ||
this._checkMenu(); | ||
this.menu.close.subscribe(() => this.closeMenu()); | ||
} | ||
|
||
ngOnDestroy() { this.destroyMenu(); } | ||
|
||
@HostListener('click') | ||
toggleMenu(): Promise<void> { | ||
return this.menuOpen ? this.closeMenu() : this.openMenu(); | ||
} | ||
|
||
openMenu(): Promise<void> { | ||
return this._createOverlay() | ||
.then(() => this._overlayRef.attach(this._portal)) | ||
.then(() => this._setIsMenuOpen(true)); | ||
} | ||
|
||
closeMenu(): Promise<void> { | ||
if (!this._overlayRef) { return Promise.resolve(); } | ||
|
||
return this._overlayRef.detach() | ||
.then(() => this._setIsMenuOpen(false)); | ||
} | ||
|
||
destroyMenu(): void { | ||
this._overlayRef.dispose(); | ||
} | ||
|
||
// set state rather than toggle to support triggers sharing a menu | ||
private _setIsMenuOpen(isOpen: boolean): void { | ||
this.menuOpen = isOpen; | ||
this.menu._setClickCatcher(isOpen); | ||
this.menuOpen ? this.onMenuOpen.emit(null) : this.onMenuClose.emit(null); | ||
} | ||
|
||
/** | ||
* This method checks that a valid instance of MdMenu has been passed into | ||
* md-menu-trigger-for. If not, an exception is thrown. | ||
*/ | ||
private _checkMenu() { | ||
if (!this.menu || !(this.menu instanceof MdMenu)) { | ||
throw new MdMenuMissingError(); | ||
} | ||
} | ||
|
||
/** | ||
* This method creates the overlay from the provided menu's template and saves its | ||
* OverlayRef so that it can be attached to the DOM when openMenu is called. | ||
*/ | ||
private _createOverlay(): Promise<any> { | ||
if (this._overlayRef) { return Promise.resolve(); } | ||
|
||
this._portal = new TemplatePortal(this.menu.templateRef, this._viewContainerRef); | ||
return this._overlay.create(this._getOverlayConfig()) | ||
.then(overlay => this._overlayRef = overlay); | ||
} | ||
|
||
/** | ||
* This method builds the configuration object needed to create the overlay, the OverlayState. | ||
* @returns OverlayState | ||
*/ | ||
private _getOverlayConfig(): OverlayState { | ||
const overlayState = new OverlayState(); | ||
overlayState.positionStrategy = this._getPosition(); | ||
return overlayState; | ||
} | ||
|
||
/** | ||
* This method builds the position strategy for the overlay, so the menu is properly connected | ||
* to the trigger. | ||
* @returns ConnectedPositionStrategy | ||
*/ | ||
private _getPosition(): ConnectedPositionStrategy { | ||
return this._overlay.position().connectedTo( | ||
this._element, | ||
{originX: 'start', originY: 'top'}, | ||
{overlayX: 'start', overlayY: 'top'} | ||
); | ||
} | ||
} | ||
|
||
export const MD_MENU_DIRECTIVES = [MdMenu, MdMenuItem, MdMenuTrigger, MdMenuAnchor]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,6 @@ | ||
<div class="md-menu"> | ||
<ng-content></ng-content> | ||
</div> | ||
|
||
<template> | ||
<div class="md-menu" (click)="_emitCloseEvent()"> | ||
<ng-content></ng-content> | ||
</div> | ||
</template> | ||
<div class="md-menu-click-catcher" *ngIf="_showClickCatcher" (click)="_emitCloseEvent()"></div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.