Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(menu): add custom position support to menu #893

Merged
merged 2 commits into from
Jul 22, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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