Skip to content

Commit

Permalink
feat(menu): add custom position support to menu (#893)
Browse files Browse the repository at this point in the history
  • Loading branch information
kara authored and robertmesserle committed Jul 22, 2016
1 parent 1efbbb9 commit 16eb6be
Show file tree
Hide file tree
Showing 14 changed files with 315 additions and 70 deletions.
57 changes: 57 additions & 0 deletions e2e/components/menu/menu-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import ElementFinder = protractor.ElementFinder;

export class MenuPage {

constructor() {
browser.get('/menu');
}

menu() { return element(by.css('.md-menu')); }

trigger() { return element(by.id('trigger')); }

triggerTwo() { return element(by.id('trigger-two')); }

body() { return element(by.tagName('body')); }

items(index: number) {
return element.all(by.css('[md-menu-item]')).get(index);
}

textArea() { return element(by.id('text')); }

beforeTrigger() { return element(by.id('before-t')); }

aboveTrigger() { return element(by.id('above-t')); }

combinedTrigger() { return element(by.id('combined-t')); }

beforeMenu() { return element(by.css('.md-menu.before')); }

aboveMenu() { return element(by.css('.md-menu.above')); }

combinedMenu() { return element(by.css('.md-menu.combined')); }

expectMenuPresent(expected: boolean) {
return browser.isElementPresent(by.css('.md-menu')).then((isPresent) => {
expect(isPresent).toBe(expected);
});
}

expectMenuLocation(el: ElementFinder, {x,y}: {x: number, y: number}) {
el.getLocation().then((loc) => {
expect(loc.x).toEqual(x);
expect(loc.y).toEqual(y);
});
}

expectMenuAlignedWith(el: ElementFinder, id: string) {
element(by.id(id)).getLocation().then((loc) => {
this.expectMenuLocation(el, {x: loc.x, y: loc.y});
});
}

getResultText() {
return this.textArea().getText();
}
}
140 changes: 86 additions & 54 deletions e2e/components/menu/menu.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,113 @@
describe('menu', function () {
import { MenuPage } from './menu-page';

describe('menu', () => {
let page: MenuPage;

beforeEach(function() {
browser.get('/menu');
page = new MenuPage();
});

it('should open menu when the trigger is clicked', function () {
expectMenuPresent(false);
element(by.id('trigger')).click();
it('should open menu when the trigger is clicked', () => {
page.expectMenuPresent(false);
page.trigger().click();

expectMenuPresent(true);
expect(element(by.css('.md-menu')).getText()).toEqual("One\nTwo\nThree");
page.expectMenuPresent(true);
expect(page.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', () => {
page.trigger().click();
page.body().click();
page.expectMenuPresent(false);
});

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', () => {
page.trigger().click();
page.items(0).click();
page.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', () => {
page.trigger().click();
page.items(0).click();
expect(page.getResultText()).toEqual('one');

page.trigger().click();
page.items(1).click();
expect(page.getResultText()).toEqual('two');
});

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');
it('should run not run click handlers on disabled menu items', () => {
page.trigger().click();
page.items(2).click();
expect(page.getResultText()).toEqual('');
});

it('should support multiple triggers opening the same menu', () => {
page.triggerTwo().click();
expect(page.menu().getText()).toEqual("One\nTwo\nThree");
page.expectMenuAlignedWith(page.menu(), 'trigger-two');

page.body().click();
page.expectMenuPresent(false);

page.trigger().click();
expect(page.menu().getText()).toEqual("One\nTwo\nThree");
page.expectMenuAlignedWith(page.menu(), 'trigger');

element(by.id('trigger')).click();
element(by.id('two')).click();
expect(element(by.id('text')).getText()).toEqual('two');
page.body().click();
page.expectMenuPresent(false);
});

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 mirror classes on host to menu template in overlay', () => {
page.trigger().click();
page.menu().getAttribute('class').then((classes) => {
expect(classes).toEqual('md-menu custom');
});
});

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');
describe('position - ', () => {

element(by.tagName('body')).click();
expectMenuPresent(false);
it('should default menu alignment to "after below" when not set', () => {
page.trigger().click();

element(by.id('trigger')).click();
expect(element(by.css('.md-menu')).getText()).toEqual("One\nTwo\nThree");
expectMenuAlignedWith('trigger');
// menu.x should equal trigger.x, menu.y should equal trigger.y
page.expectMenuAlignedWith(page.menu(), 'trigger');
});

element(by.tagName('body')).click();
expectMenuPresent(false);
});
it('should align overlay end to origin end when x-position is "before"', () => {
page.beforeTrigger().click();
page.beforeTrigger().getLocation().then((trigger) => {

function expectMenuPresent(bool: boolean) {
return browser.isElementPresent(by.css('.md-menu')).then((isPresent) => {
expect(isPresent).toBe(bool);
// the menu's right corner must be attached to the trigger's right corner.
// menu = 112px wide. trigger = 60px wide. 112 - 60 = 52px of menu to the left of trigger.
// trigger.x (left corner) - 52px (menu left of trigger) = expected menu.x (left corner)
// menu.y should equal trigger.y because only x position has changed.
page.expectMenuLocation(page.beforeMenu(), {x: trigger.x - 52, y: trigger.y});
});
});
}

function expectMenuAlignedWith(id: string) {
element(by.id(id)).getLocation().then((loc) => {
expectMenuLocation({x: loc.x, y: loc.y});
it('should align overlay bottom to origin bottom when y-position is "above"', () => {
page.aboveTrigger().click();
page.aboveTrigger().getLocation().then((trigger) => {

// the menu's bottom corner must be attached to the trigger's bottom corner.
// menu.x should equal trigger.x because only y position has changed.
// menu = 64px high. trigger = 20px high. 64 - 20 = 44px of menu extending up past trigger.
// trigger.y (top corner) - 44px (menu above trigger) = expected menu.y (top corner)
page.expectMenuLocation(page.aboveMenu(), {x: trigger.x, y: trigger.y - 44});
});
});
}

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);
it('should align menu to top left of trigger when "below" and "above"', () => {
page.combinedTrigger().click();
page.combinedTrigger().getLocation().then((trigger) => {

// trigger.x (left corner) - 52px (menu left of trigger) = expected menu.x
// trigger.y (top corner) - 44px (menu above trigger) = expected menu.y
page.expectMenuLocation(page.combinedMenu(), {x: trigger.x - 52, y: trigger.y - 44});
});
});
}

});
});
24 changes: 24 additions & 0 deletions src/components/menu/menu-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,27 @@ export class MdMenuMissingError extends MdError {
`);
}
}

/**
* Exception thrown when menu's x-position value isn't valid.
* In other words, it doesn't match 'before' or 'after'.
*/
export class MdMenuInvalidPositionX extends MdError {
constructor() {
super(`x-position value must be either 'before' or after'.
Example: <md-menu x-position="before" #menu="mdMenu"></md-menu>
`);
}
}

/**
* Exception thrown when menu's y-position value isn't valid.
* In other words, it doesn't match 'above' or 'below'.
*/
export class MdMenuInvalidPositionY extends MdError {
constructor() {
super(`y-position value must be either 'above' or below'.
Example: <md-menu y-position="above" #menu="mdMenu"></md-menu>
`);
}
}
4 changes: 4 additions & 0 deletions src/components/menu/menu-positions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

export type MenuPositionX = 'before' | 'after';

export type MenuPositionY = 'above' | 'below';
11 changes: 9 additions & 2 deletions src/components/menu/menu-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import {
import {
ConnectedPositionStrategy
} from '@angular2-material/core/overlay/position/connected-position-strategy';
import {
HorizontalConnectionPos,
VerticalConnectionPos
} from '@angular2-material/core/overlay/position/connected-position';

/**
* This directive is intended to be used in conjunction with an md-menu tag. It is
Expand Down Expand Up @@ -119,10 +123,13 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
* @returns ConnectedPositionStrategy
*/
private _getPosition(): ConnectedPositionStrategy {
const positionX: HorizontalConnectionPos = this.menu.positionX === 'before' ? 'end' : 'start';
const positionY: VerticalConnectionPos = this.menu.positionY === 'above' ? 'bottom' : 'top';

return this._overlay.position().connectedTo(
this._element,
{originX: 'start', originY: 'top'},
{overlayX: 'start', overlayY: 'top'}
{originX: positionX, originY: positionY},
{overlayX: positionX, overlayY: positionY}
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/menu/menu.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="md-menu" (click)="_emitCloseEvent()">
<div class="md-menu" [ngClass]="_classList" (click)="_emitCloseEvent()">
<ng-content></ng-content>
</div>
</template>
Expand Down
10 changes: 8 additions & 2 deletions src/components/menu/menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ $md-menu-overlay-max-width: 280px !default; // 56 * 5
$md-menu-item-height: 48px !default;
$md-menu-font-size: 16px !default;
$md-menu-side-padding: 16px !default;
$md-menu-vertical-padding: 8px !default;

.md-menu {
@include md-elevation(2);
Expand All @@ -22,10 +23,12 @@ $md-menu-side-padding: 16px !default;

// max height must be 100% of the viewport height + one row height
max-height: calc(100vh + 48px);
overflow: scroll;
overflow: auto;
-webkit-overflow-scrolling: touch; // for momentum scroll on mobile

background: md-color($md-background, 'card');
padding-top: $md-menu-vertical-padding;
padding-bottom: $md-menu-vertical-padding;
}

[md-menu-item] {
Expand All @@ -35,7 +38,6 @@ $md-menu-side-padding: 16px !default;
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
height: $md-menu-item-height;
padding: 0 $md-menu-side-padding;

Expand All @@ -55,6 +57,10 @@ $md-menu-side-padding: 16px !default;
}
}

button[md-menu-item] {
width: 100%;
}

.md-menu-click-catcher {
@include md-fullscreen();
}
Loading

0 comments on commit 16eb6be

Please sign in to comment.