Skip to content

Commit 8f23917

Browse files
crisbetommalerba
authored andcommitted
prototype(menu): create prototype menu based on MDC Web (#15857)
* prototype(menu): create prototype menu based on MDC Web Creates a prototype of `mat-menu` that uses MDC web for styling. * refactor: avoid duplicating mat-menu styles and template
1 parent 8b5c0f1 commit 8f23917

File tree

28 files changed

+3329
-105
lines changed

28 files changed

+3329
-105
lines changed

e2e/components/mdc-menu-e2e.spec.ts

Lines changed: 208 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,208 @@
1-
// TODO: copy tests from existing mat-menu, update as necessary to fix.
1+
import {browser, by, element, ExpectedConditions, Key, protractor} from 'protractor';
2+
import {
3+
expectAlignedWith,
4+
expectFocusOn,
5+
expectLocation,
6+
expectToExist,
7+
pressKeys,
8+
} from '../util/index';
9+
10+
const presenceOf = ExpectedConditions.presenceOf;
11+
const not = ExpectedConditions.not;
12+
13+
describe('menu', () => {
14+
const menuSelector = '.mat-mdc-menu-panel';
15+
const page = {
16+
menu: () => element(by.css(menuSelector)),
17+
start: () => element(by.id('start')),
18+
trigger: () => element(by.id('trigger')),
19+
triggerTwo: () => element(by.id('trigger-two')),
20+
backdrop: () => element(by.css('.cdk-overlay-backdrop')),
21+
items: (index: number) => element.all(by.css('[mat-menu-item]')).get(index),
22+
textArea: () => element(by.id('text')),
23+
beforeTrigger: () => element(by.id('before-t')),
24+
aboveTrigger: () => element(by.id('above-t')),
25+
combinedTrigger: () => element(by.id('combined-t')),
26+
beforeMenu: () => element(by.css(`${menuSelector}.before`)),
27+
aboveMenu: () => element(by.css(`${menuSelector}.above`)),
28+
combinedMenu: () => element(by.css(`${menuSelector}.combined`)),
29+
getResultText: () => page.textArea().getText(),
30+
};
31+
32+
beforeEach(async () => await browser.get('/mdc-menu'));
33+
34+
it('should open menu when the trigger is clicked', async () => {
35+
await expectToExist(menuSelector, false);
36+
await page.trigger().click();
37+
38+
await expectToExist(menuSelector);
39+
expect(await page.menu().getText()).toEqual('One\nTwo\nThree\nFour');
40+
});
41+
42+
it('should close menu when menu item is clicked', async () => {
43+
await page.trigger().click();
44+
await page.items(0).click();
45+
await expectToExist(menuSelector, false);
46+
});
47+
48+
it('should run click handlers on regular menu items', async () => {
49+
await page.trigger().click();
50+
await page.items(0).click();
51+
expect(await page.getResultText()).toEqual('one');
52+
53+
await page.trigger().click();
54+
await page.items(1).click();
55+
expect(await page.getResultText()).toEqual('two');
56+
});
57+
58+
it('should run not run click handlers on disabled menu items', async () => {
59+
await page.trigger().click();
60+
await page.items(2).click();
61+
expect(await page.getResultText()).toEqual('');
62+
});
63+
64+
it('should support multiple triggers opening the same menu', async () => {
65+
await page.triggerTwo().click();
66+
67+
expect(await page.menu().getText()).toEqual('One\nTwo\nThree\nFour');
68+
await expectAlignedWith(page.menu(), '#trigger-two');
69+
70+
await page.backdrop().click();
71+
await browser.wait(not(presenceOf(element(by.css(menuSelector)))));
72+
await browser.wait(not(presenceOf(element(by.css('.cdk-overlay-backdrop')))));
73+
74+
await page.trigger().click();
75+
76+
expect(await page.menu().getText()).toEqual('One\nTwo\nThree\nFour');
77+
await expectAlignedWith(page.menu(), '#trigger');
78+
79+
await page.backdrop().click();
80+
81+
await browser.wait(not(presenceOf(element(by.css(menuSelector)))));
82+
await browser.wait(not(presenceOf(element(by.css('.cdk-overlay-backdrop')))));
83+
});
84+
85+
it('should mirror classes on host to menu template in overlay', async () => {
86+
await page.trigger().click();
87+
expect(await page.menu().getAttribute('class')).toContain('mat-mdc-menu-panel');
88+
expect(await page.menu().getAttribute('class')).toContain('custom');
89+
});
90+
91+
describe('keyboard events', () => {
92+
beforeEach(async () => {
93+
// click start button to avoid tabbing past navigation
94+
await page.start().click();
95+
await pressKeys(Key.TAB);
96+
});
97+
98+
it('should auto-focus the first item when opened with ENTER', async () => {
99+
await pressKeys(Key.ENTER);
100+
await expectFocusOn(page.items(0));
101+
});
102+
103+
it('should auto-focus the first item when opened with SPACE', async () => {
104+
await pressKeys(Key.SPACE);
105+
await expectFocusOn(page.items(0));
106+
});
107+
108+
it('should focus the first item when opened by mouse', async () => {
109+
await page.trigger().click();
110+
await expectFocusOn(page.items(0));
111+
});
112+
113+
it('should focus subsequent items when down arrow is pressed', async () => {
114+
await pressKeys(Key.ENTER, Key.DOWN);
115+
await expectFocusOn(page.items(1));
116+
});
117+
118+
it('should focus previous items when up arrow is pressed', async () => {
119+
await pressKeys(Key.ENTER, Key.DOWN, Key.UP);
120+
await expectFocusOn(page.items(0));
121+
});
122+
123+
it('should skip disabled items using arrow keys', async () => {
124+
await pressKeys(Key.ENTER, Key.DOWN, Key.DOWN);
125+
await expectFocusOn(page.items(3));
126+
127+
await pressKeys(Key.UP);
128+
await expectFocusOn(page.items(1));
129+
});
130+
131+
it('should close the menu when tabbing past items', async () => {
132+
await pressKeys(Key.ENTER, Key.TAB);
133+
await expectToExist(menuSelector, false);
134+
135+
await pressKeys(Key.TAB, Key.ENTER);
136+
await expectToExist(menuSelector);
137+
138+
await pressKeys(protractor.Key.chord(Key.SHIFT, Key.TAB));
139+
await expectToExist(menuSelector, false);
140+
});
141+
142+
it('should wrap back to menu when arrow keying past items', async () => {
143+
let down = Key.DOWN;
144+
await pressKeys(Key.ENTER, down, down, down);
145+
await expectFocusOn(page.items(0));
146+
147+
await pressKeys(Key.UP);
148+
await expectFocusOn(page.items(3));
149+
});
150+
151+
it('should focus before and after trigger when tabbing past items', async () => {
152+
let shiftTab = protractor.Key.chord(Key.SHIFT, Key.TAB);
153+
154+
await pressKeys(Key.ENTER, Key.TAB);
155+
await expectFocusOn(page.triggerTwo());
156+
157+
// navigate back to trigger
158+
await pressKeys(shiftTab, Key.ENTER, shiftTab);
159+
await expectFocusOn(page.start());
160+
});
161+
162+
});
163+
164+
describe('position - ', () => {
165+
166+
it('should default menu alignment to "after below" when not set', async () => {
167+
await page.trigger().click();
168+
169+
// menu.x should equal trigger.x, menu.y should equal trigger.y
170+
await expectAlignedWith(page.menu(), '#trigger');
171+
});
172+
173+
it('should align overlay end to origin end when x-position is "before"', async () => {
174+
await page.beforeTrigger().click();
175+
176+
const trigger = await page.beforeTrigger().getLocation();
177+
178+
// the menu's right corner must be attached to the trigger's right corner.
179+
// menu = 112px wide. trigger = 60px wide. 112 - 60 = 52px of menu to the left of trigger.
180+
// trigger.x (left corner) - 52px (menu left of trigger) = expected menu.x (left corner)
181+
// menu.y should equal trigger.y because only x position has changed.
182+
await expectLocation(page.beforeMenu(), {x: trigger.x - 52, y: trigger.y});
183+
});
184+
185+
it('should align overlay bottom to origin bottom when y-position is "above"', async () => {
186+
await page.aboveTrigger().click();
187+
188+
const trigger = await page.aboveTrigger().getLocation();
189+
190+
// the menu's bottom corner must be attached to the trigger's bottom corner.
191+
// menu.x should equal trigger.x because only y position has changed.
192+
// menu = 64px high. trigger = 20px high. 64 - 20 = 44px of menu extending up past trigger.
193+
// trigger.y (top corner) - 44px (menu above trigger) = expected menu.y (top corner)
194+
await expectLocation(page.aboveMenu(), {x: trigger.x, y: trigger.y - 44});
195+
});
196+
197+
it('should align menu to top left of trigger when "below" and "above"', async () => {
198+
await page.combinedTrigger().click();
199+
200+
const trigger = await page.combinedTrigger().getLocation();
201+
202+
// trigger.x (left corner) - 52px (menu left of trigger) = expected menu.x
203+
// trigger.y (top corner) - 44px (menu above trigger) = expected menu.y
204+
await expectLocation(page.combinedMenu(), {x: trigger.x - 52, y: trigger.y - 44});
205+
});
206+
207+
});
208+
});

e2e/components/menu-e2e.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const not = ExpectedConditions.not;
1313
describe('menu', () => {
1414
const menuSelector = '.mat-menu-panel';
1515
const page = {
16-
menu: () => element(by.css('.mat-menu-panel')),
16+
menu: () => element(by.css(menuSelector)),
1717
start: () => element(by.id('start')),
1818
trigger: () => element(by.id('trigger')),
1919
triggerTwo: () => element(by.id('trigger-two')),
@@ -23,9 +23,9 @@ describe('menu', () => {
2323
beforeTrigger: () => element(by.id('before-t')),
2424
aboveTrigger: () => element(by.id('above-t')),
2525
combinedTrigger: () => element(by.id('combined-t')),
26-
beforeMenu: () => element(by.css('.mat-menu-panel.before')),
27-
aboveMenu: () => element(by.css('.mat-menu-panel.above')),
28-
combinedMenu: () => element(by.css('.mat-menu-panel.combined')),
26+
beforeMenu: () => element(by.css(`${menuSelector}.before`)),
27+
aboveMenu: () => element(by.css(`${menuSelector}.above`)),
28+
combinedMenu: () => element(by.css(`${menuSelector}.combined`)),
2929
getResultText: () => page.textArea().getText(),
3030
};
3131

src/dev-app/mdc-menu/mdc-menu-demo-module.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,24 @@
77
*/
88

99
import {NgModule} from '@angular/core';
10+
import {CommonModule} from '@angular/common';
1011
import {MatMenuModule} from '@angular/material-experimental/mdc-menu';
1112
import {RouterModule} from '@angular/router';
13+
import {MatToolbarModule} from '@angular/material/toolbar';
14+
import {MatIconModule} from '@angular/material/icon';
15+
import {MatDividerModule} from '@angular/material/divider';
16+
import {MatButtonModule} from '@angular/material/button';
1217
import {MdcMenuDemo} from './mdc-menu-demo';
1318

1419
@NgModule({
1520
imports: [
21+
CommonModule,
1622
MatMenuModule,
1723
RouterModule.forChild([{path: '', component: MdcMenuDemo}]),
24+
MatButtonModule,
25+
MatToolbarModule,
26+
MatIconModule,
27+
MatDividerModule,
1828
],
1929
declarations: [MdcMenuDemo],
2030
})

0 commit comments

Comments
 (0)