Skip to content

Commit 16eb6be

Browse files
kararobertmesserle
authored andcommitted
feat(menu): add custom position support to menu (#893)
1 parent 1efbbb9 commit 16eb6be

File tree

14 files changed

+315
-70
lines changed

14 files changed

+315
-70
lines changed

e2e/components/menu/menu-page.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import ElementFinder = protractor.ElementFinder;
2+
3+
export class MenuPage {
4+
5+
constructor() {
6+
browser.get('/menu');
7+
}
8+
9+
menu() { return element(by.css('.md-menu')); }
10+
11+
trigger() { return element(by.id('trigger')); }
12+
13+
triggerTwo() { return element(by.id('trigger-two')); }
14+
15+
body() { return element(by.tagName('body')); }
16+
17+
items(index: number) {
18+
return element.all(by.css('[md-menu-item]')).get(index);
19+
}
20+
21+
textArea() { return element(by.id('text')); }
22+
23+
beforeTrigger() { return element(by.id('before-t')); }
24+
25+
aboveTrigger() { return element(by.id('above-t')); }
26+
27+
combinedTrigger() { return element(by.id('combined-t')); }
28+
29+
beforeMenu() { return element(by.css('.md-menu.before')); }
30+
31+
aboveMenu() { return element(by.css('.md-menu.above')); }
32+
33+
combinedMenu() { return element(by.css('.md-menu.combined')); }
34+
35+
expectMenuPresent(expected: boolean) {
36+
return browser.isElementPresent(by.css('.md-menu')).then((isPresent) => {
37+
expect(isPresent).toBe(expected);
38+
});
39+
}
40+
41+
expectMenuLocation(el: ElementFinder, {x,y}: {x: number, y: number}) {
42+
el.getLocation().then((loc) => {
43+
expect(loc.x).toEqual(x);
44+
expect(loc.y).toEqual(y);
45+
});
46+
}
47+
48+
expectMenuAlignedWith(el: ElementFinder, id: string) {
49+
element(by.id(id)).getLocation().then((loc) => {
50+
this.expectMenuLocation(el, {x: loc.x, y: loc.y});
51+
});
52+
}
53+
54+
getResultText() {
55+
return this.textArea().getText();
56+
}
57+
}

e2e/components/menu/menu.e2e.ts

Lines changed: 86 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,113 @@
1-
describe('menu', function () {
1+
import { MenuPage } from './menu-page';
2+
3+
describe('menu', () => {
4+
let page: MenuPage;
5+
26
beforeEach(function() {
3-
browser.get('/menu');
7+
page = new MenuPage();
48
});
59

6-
it('should open menu when the trigger is clicked', function () {
7-
expectMenuPresent(false);
8-
element(by.id('trigger')).click();
10+
it('should open menu when the trigger is clicked', () => {
11+
page.expectMenuPresent(false);
12+
page.trigger().click();
913

10-
expectMenuPresent(true);
11-
expect(element(by.css('.md-menu')).getText()).toEqual("One\nTwo\nThree");
14+
page.expectMenuPresent(true);
15+
expect(page.menu().getText()).toEqual("One\nTwo\nThree");
1216
});
1317

14-
it('should align menu when open', function() {
15-
element(by.id('trigger')).click();
16-
expectMenuAlignedWith('trigger');
18+
it('should close menu when area outside menu is clicked', () => {
19+
page.trigger().click();
20+
page.body().click();
21+
page.expectMenuPresent(false);
1722
});
1823

19-
it('should close menu when area outside menu is clicked', function () {
20-
element(by.id('trigger')).click();
21-
element(by.tagName('body')).click();
22-
expectMenuPresent(false);
24+
it('should close menu when menu item is clicked', () => {
25+
page.trigger().click();
26+
page.items(0).click();
27+
page.expectMenuPresent(false);
2328
});
2429

25-
it('should close menu when menu item is clicked', function () {
26-
element(by.id('trigger')).click();
27-
element(by.id('one')).click();
28-
expectMenuPresent(false);
30+
it('should run click handlers on regular menu items', () => {
31+
page.trigger().click();
32+
page.items(0).click();
33+
expect(page.getResultText()).toEqual('one');
34+
35+
page.trigger().click();
36+
page.items(1).click();
37+
expect(page.getResultText()).toEqual('two');
2938
});
3039

31-
it('should run click handlers on regular menu items', function() {
32-
element(by.id('trigger')).click();
33-
element(by.id('one')).click();
34-
expect(element(by.id('text')).getText()).toEqual('one');
40+
it('should run not run click handlers on disabled menu items', () => {
41+
page.trigger().click();
42+
page.items(2).click();
43+
expect(page.getResultText()).toEqual('');
44+
});
45+
46+
it('should support multiple triggers opening the same menu', () => {
47+
page.triggerTwo().click();
48+
expect(page.menu().getText()).toEqual("One\nTwo\nThree");
49+
page.expectMenuAlignedWith(page.menu(), 'trigger-two');
50+
51+
page.body().click();
52+
page.expectMenuPresent(false);
53+
54+
page.trigger().click();
55+
expect(page.menu().getText()).toEqual("One\nTwo\nThree");
56+
page.expectMenuAlignedWith(page.menu(), 'trigger');
3557

36-
element(by.id('trigger')).click();
37-
element(by.id('two')).click();
38-
expect(element(by.id('text')).getText()).toEqual('two');
58+
page.body().click();
59+
page.expectMenuPresent(false);
3960
});
4061

41-
it('should run not run click handlers on disabled menu items', function() {
42-
element(by.id('trigger')).click();
43-
element(by.id('three')).click();
44-
expect(element(by.id('text')).getText()).toEqual('');
62+
it('should mirror classes on host to menu template in overlay', () => {
63+
page.trigger().click();
64+
page.menu().getAttribute('class').then((classes) => {
65+
expect(classes).toEqual('md-menu custom');
66+
});
4567
});
4668

47-
it('should support multiple triggers opening the same menu', function() {
48-
element(by.id('trigger-two')).click();
49-
expect(element(by.css('.md-menu')).getText()).toEqual("One\nTwo\nThree");
50-
expectMenuAlignedWith('trigger-two');
69+
describe('position - ', () => {
5170

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

55-
element(by.id('trigger')).click();
56-
expect(element(by.css('.md-menu')).getText()).toEqual("One\nTwo\nThree");
57-
expectMenuAlignedWith('trigger');
74+
// menu.x should equal trigger.x, menu.y should equal trigger.y
75+
page.expectMenuAlignedWith(page.menu(), 'trigger');
76+
});
5877

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

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

69-
function expectMenuAlignedWith(id: string) {
70-
element(by.id(id)).getLocation().then((loc) => {
71-
expectMenuLocation({x: loc.x, y: loc.y});
90+
it('should align overlay bottom to origin bottom when y-position is "above"', () => {
91+
page.aboveTrigger().click();
92+
page.aboveTrigger().getLocation().then((trigger) => {
93+
94+
// the menu's bottom corner must be attached to the trigger's bottom corner.
95+
// menu.x should equal trigger.x because only y position has changed.
96+
// menu = 64px high. trigger = 20px high. 64 - 20 = 44px of menu extending up past trigger.
97+
// trigger.y (top corner) - 44px (menu above trigger) = expected menu.y (top corner)
98+
page.expectMenuLocation(page.aboveMenu(), {x: trigger.x, y: trigger.y - 44});
99+
});
72100
});
73-
}
74101

75-
function expectMenuLocation({x,y}: {x: number, y: number}) {
76-
element(by.css('.md-menu')).getLocation().then((loc) => {
77-
expect(loc.x).toEqual(x);
78-
expect(loc.y).toEqual(y);
102+
it('should align menu to top left of trigger when "below" and "above"', () => {
103+
page.combinedTrigger().click();
104+
page.combinedTrigger().getLocation().then((trigger) => {
105+
106+
// trigger.x (left corner) - 52px (menu left of trigger) = expected menu.x
107+
// trigger.y (top corner) - 44px (menu above trigger) = expected menu.y
108+
page.expectMenuLocation(page.combinedMenu(), {x: trigger.x - 52, y: trigger.y - 44});
109+
});
79110
});
80-
}
111+
112+
});
81113
});

src/components/menu/menu-errors.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,27 @@ export class MdMenuMissingError extends MdError {
1313
`);
1414
}
1515
}
16+
17+
/**
18+
* Exception thrown when menu's x-position value isn't valid.
19+
* In other words, it doesn't match 'before' or 'after'.
20+
*/
21+
export class MdMenuInvalidPositionX extends MdError {
22+
constructor() {
23+
super(`x-position value must be either 'before' or after'.
24+
Example: <md-menu x-position="before" #menu="mdMenu"></md-menu>
25+
`);
26+
}
27+
}
28+
29+
/**
30+
* Exception thrown when menu's y-position value isn't valid.
31+
* In other words, it doesn't match 'above' or 'below'.
32+
*/
33+
export class MdMenuInvalidPositionY extends MdError {
34+
constructor() {
35+
super(`y-position value must be either 'above' or below'.
36+
Example: <md-menu y-position="above" #menu="mdMenu"></md-menu>
37+
`);
38+
}
39+
}

src/components/menu/menu-positions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
export type MenuPositionX = 'before' | 'after';
3+
4+
export type MenuPositionY = 'above' | 'below';

src/components/menu/menu-trigger.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import {
2222
import {
2323
ConnectedPositionStrategy
2424
} from '@angular2-material/core/overlay/position/connected-position-strategy';
25+
import {
26+
HorizontalConnectionPos,
27+
VerticalConnectionPos
28+
} from '@angular2-material/core/overlay/position/connected-position';
2529

2630
/**
2731
* This directive is intended to be used in conjunction with an md-menu tag. It is
@@ -119,10 +123,13 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
119123
* @returns ConnectedPositionStrategy
120124
*/
121125
private _getPosition(): ConnectedPositionStrategy {
126+
const positionX: HorizontalConnectionPos = this.menu.positionX === 'before' ? 'end' : 'start';
127+
const positionY: VerticalConnectionPos = this.menu.positionY === 'above' ? 'bottom' : 'top';
128+
122129
return this._overlay.position().connectedTo(
123130
this._element,
124-
{originX: 'start', originY: 'top'},
125-
{overlayX: 'start', overlayY: 'top'}
131+
{originX: positionX, originY: positionY},
132+
{overlayX: positionX, overlayY: positionY}
126133
);
127134
}
128135
}

src/components/menu/menu.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div class="md-menu" (click)="_emitCloseEvent()">
2+
<div class="md-menu" [ngClass]="_classList" (click)="_emitCloseEvent()">
33
<ng-content></ng-content>
44
</div>
55
</template>

src/components/menu/menu.scss

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ $md-menu-overlay-max-width: 280px !default; // 56 * 5
1414
$md-menu-item-height: 48px !default;
1515
$md-menu-font-size: 16px !default;
1616
$md-menu-side-padding: 16px !default;
17+
$md-menu-vertical-padding: 8px !default;
1718

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

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

2829
background: md-color($md-background, 'card');
30+
padding-top: $md-menu-vertical-padding;
31+
padding-bottom: $md-menu-vertical-padding;
2932
}
3033

3134
[md-menu-item] {
@@ -35,7 +38,6 @@ $md-menu-side-padding: 16px !default;
3538
display: flex;
3639
flex-direction: row;
3740
align-items: center;
38-
width: 100%;
3941
height: $md-menu-item-height;
4042
padding: 0 $md-menu-side-padding;
4143

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

60+
button[md-menu-item] {
61+
width: 100%;
62+
}
63+
5864
.md-menu-click-catcher {
5965
@include md-fullscreen();
6066
}

0 commit comments

Comments
 (0)