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
This commit is contained in:
Kristiyan Kostadinov
2019-04-25 21:08:33 +02:00
committed by mmalerba
parent 8b5c0f12c8
commit 8f2391720d
28 changed files with 3329 additions and 105 deletions
+208 -1
View File
@@ -1 +1,208 @@
// TODO: copy tests from existing mat-menu, update as necessary to fix.
import {browser, by, element, ExpectedConditions, Key, protractor} from 'protractor';
import {
expectAlignedWith,
expectFocusOn,
expectLocation,
expectToExist,
pressKeys,
} from '../util/index';
const presenceOf = ExpectedConditions.presenceOf;
const not = ExpectedConditions.not;
describe('menu', () => {
const menuSelector = '.mat-mdc-menu-panel';
const page = {
menu: () => element(by.css(menuSelector)),
start: () => element(by.id('start')),
trigger: () => element(by.id('trigger')),
triggerTwo: () => element(by.id('trigger-two')),
backdrop: () => element(by.css('.cdk-overlay-backdrop')),
items: (index: number) => element.all(by.css('[mat-menu-item]')).get(index),
textArea: () => element(by.id('text')),
beforeTrigger: () => element(by.id('before-t')),
aboveTrigger: () => element(by.id('above-t')),
combinedTrigger: () => element(by.id('combined-t')),
beforeMenu: () => element(by.css(`${menuSelector}.before`)),
aboveMenu: () => element(by.css(`${menuSelector}.above`)),
combinedMenu: () => element(by.css(`${menuSelector}.combined`)),
getResultText: () => page.textArea().getText(),
};
beforeEach(async () => await browser.get('/mdc-menu'));
it('should open menu when the trigger is clicked', async () => {
await expectToExist(menuSelector, false);
await page.trigger().click();
await expectToExist(menuSelector);
expect(await page.menu().getText()).toEqual('One\nTwo\nThree\nFour');
});
it('should close menu when menu item is clicked', async () => {
await page.trigger().click();
await page.items(0).click();
await expectToExist(menuSelector, false);
});
it('should run click handlers on regular menu items', async () => {
await page.trigger().click();
await page.items(0).click();
expect(await page.getResultText()).toEqual('one');
await page.trigger().click();
await page.items(1).click();
expect(await page.getResultText()).toEqual('two');
});
it('should run not run click handlers on disabled menu items', async () => {
await page.trigger().click();
await page.items(2).click();
expect(await page.getResultText()).toEqual('');
});
it('should support multiple triggers opening the same menu', async () => {
await page.triggerTwo().click();
expect(await page.menu().getText()).toEqual('One\nTwo\nThree\nFour');
await expectAlignedWith(page.menu(), '#trigger-two');
await page.backdrop().click();
await browser.wait(not(presenceOf(element(by.css(menuSelector)))));
await browser.wait(not(presenceOf(element(by.css('.cdk-overlay-backdrop')))));
await page.trigger().click();
expect(await page.menu().getText()).toEqual('One\nTwo\nThree\nFour');
await expectAlignedWith(page.menu(), '#trigger');
await page.backdrop().click();
await browser.wait(not(presenceOf(element(by.css(menuSelector)))));
await browser.wait(not(presenceOf(element(by.css('.cdk-overlay-backdrop')))));
});
it('should mirror classes on host to menu template in overlay', async () => {
await page.trigger().click();
expect(await page.menu().getAttribute('class')).toContain('mat-mdc-menu-panel');
expect(await page.menu().getAttribute('class')).toContain('custom');
});
describe('keyboard events', () => {
beforeEach(async () => {
// click start button to avoid tabbing past navigation
await page.start().click();
await pressKeys(Key.TAB);
});
it('should auto-focus the first item when opened with ENTER', async () => {
await pressKeys(Key.ENTER);
await expectFocusOn(page.items(0));
});
it('should auto-focus the first item when opened with SPACE', async () => {
await pressKeys(Key.SPACE);
await expectFocusOn(page.items(0));
});
it('should focus the first item when opened by mouse', async () => {
await page.trigger().click();
await expectFocusOn(page.items(0));
});
it('should focus subsequent items when down arrow is pressed', async () => {
await pressKeys(Key.ENTER, Key.DOWN);
await expectFocusOn(page.items(1));
});
it('should focus previous items when up arrow is pressed', async () => {
await pressKeys(Key.ENTER, Key.DOWN, Key.UP);
await expectFocusOn(page.items(0));
});
it('should skip disabled items using arrow keys', async () => {
await pressKeys(Key.ENTER, Key.DOWN, Key.DOWN);
await expectFocusOn(page.items(3));
await pressKeys(Key.UP);
await expectFocusOn(page.items(1));
});
it('should close the menu when tabbing past items', async () => {
await pressKeys(Key.ENTER, Key.TAB);
await expectToExist(menuSelector, false);
await pressKeys(Key.TAB, Key.ENTER);
await expectToExist(menuSelector);
await pressKeys(protractor.Key.chord(Key.SHIFT, Key.TAB));
await expectToExist(menuSelector, false);
});
it('should wrap back to menu when arrow keying past items', async () => {
let down = Key.DOWN;
await pressKeys(Key.ENTER, down, down, down);
await expectFocusOn(page.items(0));
await pressKeys(Key.UP);
await expectFocusOn(page.items(3));
});
it('should focus before and after trigger when tabbing past items', async () => {
let shiftTab = protractor.Key.chord(Key.SHIFT, Key.TAB);
await pressKeys(Key.ENTER, Key.TAB);
await expectFocusOn(page.triggerTwo());
// navigate back to trigger
await pressKeys(shiftTab, Key.ENTER, shiftTab);
await expectFocusOn(page.start());
});
});
describe('position - ', () => {
it('should default menu alignment to "after below" when not set', async () => {
await page.trigger().click();
// menu.x should equal trigger.x, menu.y should equal trigger.y
await expectAlignedWith(page.menu(), '#trigger');
});
it('should align overlay end to origin end when x-position is "before"', async () => {
await page.beforeTrigger().click();
const trigger = await page.beforeTrigger().getLocation();
// 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.
await expectLocation(page.beforeMenu(), {x: trigger.x - 52, y: trigger.y});
});
it('should align overlay bottom to origin bottom when y-position is "above"', async () => {
await page.aboveTrigger().click();
const trigger = await page.aboveTrigger().getLocation();
// 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)
await expectLocation(page.aboveMenu(), {x: trigger.x, y: trigger.y - 44});
});
it('should align menu to top left of trigger when "below" and "above"', async () => {
await page.combinedTrigger().click();
const trigger = await page.combinedTrigger().getLocation();
// trigger.x (left corner) - 52px (menu left of trigger) = expected menu.x
// trigger.y (top corner) - 44px (menu above trigger) = expected menu.y
await expectLocation(page.combinedMenu(), {x: trigger.x - 52, y: trigger.y - 44});
});
});
});
+4 -4
View File
@@ -13,7 +13,7 @@ const not = ExpectedConditions.not;
describe('menu', () => {
const menuSelector = '.mat-menu-panel';
const page = {
menu: () => element(by.css('.mat-menu-panel')),
menu: () => element(by.css(menuSelector)),
start: () => element(by.id('start')),
trigger: () => element(by.id('trigger')),
triggerTwo: () => element(by.id('trigger-two')),
@@ -23,9 +23,9 @@ describe('menu', () => {
beforeTrigger: () => element(by.id('before-t')),
aboveTrigger: () => element(by.id('above-t')),
combinedTrigger: () => element(by.id('combined-t')),
beforeMenu: () => element(by.css('.mat-menu-panel.before')),
aboveMenu: () => element(by.css('.mat-menu-panel.above')),
combinedMenu: () => element(by.css('.mat-menu-panel.combined')),
beforeMenu: () => element(by.css(`${menuSelector}.before`)),
aboveMenu: () => element(by.css(`${menuSelector}.above`)),
combinedMenu: () => element(by.css(`${menuSelector}.combined`)),
getResultText: () => page.textArea().getText(),
};
@@ -7,14 +7,24 @@
*/
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {MatMenuModule} from '@angular/material-experimental/mdc-menu';
import {RouterModule} from '@angular/router';
import {MatToolbarModule} from '@angular/material/toolbar';
import {MatIconModule} from '@angular/material/icon';
import {MatDividerModule} from '@angular/material/divider';
import {MatButtonModule} from '@angular/material/button';
import {MdcMenuDemo} from './mdc-menu-demo';
@NgModule({
imports: [
CommonModule,
MatMenuModule,
RouterModule.forChild([{path: '', component: MdcMenuDemo}]),
MatButtonModule,
MatToolbarModule,
MatIconModule,
MatDividerModule,
],
declarations: [MdcMenuDemo],
})
+189 -2
View File
@@ -1,2 +1,189 @@
<!-- TODO: copy in demo template from existing mat-menu demo. -->
Not yet implemented.
<div class="demo-menu">
<div class="demo-menu-section">
<p>You clicked on: {{ selected }}</p>
<mat-toolbar>
<button mat-icon-button [matMenuTriggerFor]="menu" aria-label="Open basic menu">
<mat-icon>more_vert</mat-icon>
</button>
</mat-toolbar>
<mat-menu #menu="matMenu">
<button mat-menu-item *ngFor="let item of items" (click)="select(item.text)" [disabled]="item.disabled">
{{ item.text }}
</button>
</mat-menu>
</div>
<div class="demo-menu-section">
<p>Menu with divider</p>
<mat-toolbar>
<button mat-icon-button [matMenuTriggerFor]="divider" aria-label="Open basic menu">
<mat-icon>more_vert</mat-icon>
</button>
</mat-toolbar>
<mat-menu #divider="matMenu">
<ng-container *ngFor="let item of items; last as last">
<button mat-menu-item [disabled]="item.disabled">
{{ item.text }}
</button>
<mat-divider *ngIf="!last"></mat-divider>
</ng-container>
</mat-menu>
</div>
<div class="demo-menu-section">
<p>Nested menu</p>
<mat-toolbar>
<button mat-icon-button [matMenuTriggerFor]="animals">
<mat-icon>more_vert</mat-icon>
</button>
</mat-toolbar>
<mat-menu #animals="matMenu">
<button mat-menu-item [matMenuTriggerFor]="vertebrates">Vertebrates</button>
<button mat-menu-item [matMenuTriggerFor]="invertebrates">Invertebrates</button>
</mat-menu>
<mat-menu #vertebrates="matMenu">
<button mat-menu-item [matMenuTriggerFor]="fish">Fishes</button>
<button mat-menu-item [matMenuTriggerFor]="amphibians">Amphibians</button>
<button mat-menu-item [matMenuTriggerFor]="reptiles">Reptiles</button>
<button mat-menu-item>Birds</button>
<button mat-menu-item>Mammals</button>
</mat-menu>
<mat-menu #invertebrates="matMenu">
<button mat-menu-item>Insects</button>
<button mat-menu-item>Molluscs</button>
<button mat-menu-item>Crustaceans</button>
<button mat-menu-item>Corals</button>
<button mat-menu-item>Arachnids</button>
<button mat-menu-item>Velvet worms</button>
<button mat-menu-item>Horseshoe crabs</button>
</mat-menu>
<mat-menu #fish="matMenu">
<button mat-menu-item>Baikal oilfish</button>
<button mat-menu-item>Bala shark</button>
<button mat-menu-item>Ballan wrasse</button>
<button mat-menu-item>Bamboo shark</button>
<button mat-menu-item>Banded killifish</button>
</mat-menu>
<mat-menu #amphibians="matMenu">
<button mat-menu-item>Sonoran desert toad</button>
<button mat-menu-item>Western toad</button>
<button mat-menu-item>Arroyo toad</button>
<button mat-menu-item>Yosemite toad</button>
</mat-menu>
<mat-menu #reptiles="matMenu">
<button mat-menu-item>Banded Day Gecko</button>
<button mat-menu-item>Banded Gila Monster</button>
<button mat-menu-item>Black Tree Monitor</button>
<button mat-menu-item>Blue Spiny Lizard</button>
</mat-menu>
</div>
<div class="demo-menu-section">
<p>Clicking these will navigate:</p>
<mat-toolbar>
<button mat-icon-button [matMenuTriggerFor]="anchorMenu" aria-label="Open anchor menu">
<mat-icon>more_vert</mat-icon>
</button>
</mat-toolbar>
<mat-menu #anchorMenu="matMenu">
<a mat-menu-item *ngFor="let item of items" href="http://www.google.com" [disabled]="item.disabled">
{{ item.text }}
</a>
</mat-menu>
</div>
<div class="demo-menu-section">
<p>
Position x: before
</p>
<mat-toolbar class="demo-end-icon">
<button mat-icon-button [matMenuTriggerFor]="posXMenu" aria-label="Open x-positioned menu">
<mat-icon>more_vert</mat-icon>
</button>
</mat-toolbar>
<mat-menu xPosition="before" #posXMenu="matMenu">
<button mat-menu-item *ngFor="let item of iconItems" [disabled]="item.disabled">
<mat-icon>{{ item.icon }}</mat-icon>
{{ item.text }}
</button>
</mat-menu>
</div>
<div class="demo-menu-section">
<p>
Position y: above
</p>
<mat-toolbar>
<button mat-icon-button [matMenuTriggerFor]="posYMenu" aria-label="Open y-positioned menu">
<mat-icon>more_vert</mat-icon>
</button>
</mat-toolbar>
<mat-menu yPosition="above" #posYMenu="matMenu">
<button mat-menu-item *ngFor="let item of items" [disabled]="item.disabled">
{{ item.text }}
</button>
</mat-menu>
</div>
</div>
<div class="demo-menu">
<div class="demo-menu-section">
<p>overlapTrigger: true</p>
<mat-toolbar>
<button mat-icon-button [mat-menu-trigger-for]="menuOverlay">
<mat-icon>more_vert</mat-icon>
</button>
</mat-toolbar>
<mat-menu [overlapTrigger]="true" #menuOverlay="matMenu">
<button mat-menu-item *ngFor="let item of items" [disabled]="item.disabled">
{{ item.text }}
</button>
</mat-menu>
</div>
<div class="demo-menu-section">
<p>
Position x: before, overlapTrigger: true
</p>
<mat-toolbar class="demo-end-icon">
<button mat-icon-button [mat-menu-trigger-for]="posXMenuOverlay">
<mat-icon>more_vert</mat-icon>
</button>
</mat-toolbar>
<mat-menu xPosition="before" [overlapTrigger]="true" #posXMenuOverlay="matMenu">
<button mat-menu-item *ngFor="let item of iconItems" [disabled]="item.disabled">
<mat-icon>{{ item.icon }}</mat-icon>
{{ item.text }}
</button>
</mat-menu>
</div>
<div class="demo-menu-section">
<p>
Position y: above, overlapTrigger: true
</p>
<mat-toolbar>
<button mat-icon-button [mat-menu-trigger-for]="posYMenuOverlay">
<mat-icon>more_vert</mat-icon>
</button>
</mat-toolbar>
<mat-menu yPosition="above" [overlapTrigger]="true" #posYMenuOverlay="matMenu">
<button mat-menu-item *ngFor="let item of items" [disabled]="item.disabled">
{{ item.text }}
</button>
</mat-menu>
</div>
</div>
<div style="height: 500px">This div is for testing scrolled menus.</div>
+13 -1
View File
@@ -1 +1,13 @@
// TODO: copy in demo styles from existing mat-menu demo.
.demo-menu {
display: flex;
flex-flow: row wrap;
.demo-menu-section {
width: 300px;
margin: 20px;
}
.demo-end-icon {
justify-content: flex-end;
}
}
+15
View File
@@ -15,4 +15,19 @@ import {Component} from '@angular/core';
styleUrls: ['mdc-menu-demo.css'],
})
export class MdcMenuDemo {
selected = '';
items = [
{text: 'Refresh'},
{text: 'Settings'},
{text: 'Help', disabled: true},
{text: 'Sign Out'}
];
iconItems = [
{text: 'Redial', icon: 'dialpad'},
{text: 'Check voicemail', icon: 'voicemail', disabled: true},
{text: 'Disable alerts', icon: 'notifications_off'}
];
select(text: string) { this.selected = text; }
}
+48 -1
View File
@@ -1 +1,48 @@
<!-- TODO: copy implementation from existing mat-menu e2e page. -->
<div>
<div style="float:left">
<div id="text">{{ selected }}</div>
<button id="start">START</button>
<button [matMenuTriggerFor]="menu" id="trigger">TRIGGER</button>
<button [matMenuTriggerFor]="menu" id="trigger-two">TRIGGER 2</button>
<mat-menu #menu="matMenu" yPosition="below" class="custom" overlapTrigger>
<button mat-menu-item (click)="selected='one'">One</button>
<button mat-menu-item (click)="selected='two'">Two</button>
<button mat-menu-item (click)="selected='three'" disabled>Three</button>
<button mat-menu-item>Four</button>
</mat-menu>
<button [matMenuTriggerFor]="beforeMenu" id="before-t">
BEFORE
</button>
<mat-menu
xPosition="before"
yPosition="below"
class="before"
#beforeMenu="matMenu"
overlapTrigger>
<button mat-menu-item>Item</button>
</mat-menu>
<div class="bottom-row">
<button [matMenuTriggerFor]="aboveMenu" id="above-t">ABOVE</button>
<mat-menu yPosition="above" class="above" #aboveMenu="matMenu" overlapTrigger>
<button mat-menu-item>Item</button>
</mat-menu>
<button [matMenuTriggerFor]="combined" id="combined-t">
BOTH
</button>
<mat-menu
xPosition="before"
yPosition="above"
class="combined"
#combined="matMenu"
overlapTrigger>
<button mat-menu-item>Item</button>
</mat-menu>
</div>
</div>
</div>
+11 -1
View File
@@ -12,7 +12,17 @@ import {Component} from '@angular/core';
moduleId: module.id,
selector: 'mdc-menu-e2e',
templateUrl: 'mdc-menu-e2e.html',
styles: [`
#before-t, #above-t, #combined-t {
width: 60px;
height: 20px;
}
.bottom-row {
margin-top: 5px;
}
`]
})
export class MdcMenuE2e {
// TODO: copy implementation from existing mat-menu e2e page.
selected: string = '';
}
@@ -10,6 +10,9 @@ ng_module(
module_name = "@angular/material-experimental/mdc-menu",
assets = [":menu_scss"] + glob(["**/*.html"]),
deps = [
"@npm//@angular/animations",
"@npm//@angular/common",
"@npm//@angular/core",
"@npm//material-components-web",
] + CDK_TARGETS + MATERIAL_TARGETS,
)
@@ -20,10 +23,19 @@ sass_library(
deps = [
"//src/material/core:core_scss_lib",
"//src/material-experimental/mdc-helpers:mdc_helpers_scss_lib",
"//src/material-experimental/mdc-helpers:mdc_scss_deps_lib",
],
)
sass_binary(
name = "menu_scss",
src = "menu.scss",
include_paths = [
"external/npm/node_modules",
],
deps = [
":menu_scss_lib",
"//src/material/core:all_themes",
"//src/material-experimental/mdc-helpers:mdc_scss_deps_lib",
]
)
+95 -1
View File
@@ -1 +1,95 @@
This is a placeholder for the MDC-based implementation of menu.
This is prototype of an alternate version of `<mat-menu>` built on top of
[MDC Web](https://github.com/material-components/material-components-web). It demonstrates how
Angular Material could use MDC Web under the hood while still exposing the same API Angular users as
the existing `<mat-menu>`. This component is experimental and should not be used in production.
## How to use
Assuming your application is already up and running using Angular Material, you can add this
component by following these steps:
1. Install Angular Material Experimental & MDC WEB:
```bash
npm i material-components-web @angular/material-experimental
```
2. In your `angular.json`, make sure `node_modules/` is listed as a Sass include path. This is
needed for the Sass compiler to be able to find the MDC Web Sass files.
```json
...
"styles": [
"src/styles.scss"
],
"stylePreprocessorOptions": {
"includePaths": [
"node_modules/"
]
},
...
```
3. Import the experimental `MatMenuModule` and add it to the module that declares your
component:
```ts
import {MatMenuModule} from '@angular/material-experimental/mdc-menu';
@NgModule({
declarations: [MyComponent],
imports: [MatMenuModule],
})
export class MyModule {}
```
4. Add use `<mat-menu>` in your component's template, just like you would the normal
`<mat-menu>`:
```html
<button [matMenuTriggerFor]="menu">Menu</button>
<mat-menu #menu="matMenu">
<button mat-menu-item>Item 1</button>
<button mat-menu-item>Item 2</button>
</mat-menu>
```
5. Add the theme and typography mixins to your Sass. (There is currently no pre-built CSS option for
the experimental `<mat-menu>`):
```scss
@import '~@angular/material/theming';
@import '~@angular/material-experimental/mdc-menu';
$my-primary: mat-palette($mat-indigo);
$my-accent: mat-palette($mat-pink, A200, A100, A400);
$my-theme: mat-light-theme($my-primary, $my-accent);
@include mat-menu-theme-mdc($my-theme);
@include mat-menu-typography-mdc();
```
## API differences
The experimental menu API closely matches the
[API of the standard menu](https://material.angular.io/components/menu/api).
`@angular/material-experimental/mdc-menu` exports symbols with the same name and public interface
as all of the symbols found under `@angular/material/menu`, except for the following
differences:
* The experimental `MatMenu` does not support increasing the elevation of a sub-menu, based on its depth.
## Replacing the standard menu in an existing app
Because the experimental API mirrors the API for the standard menu, it can easily be swapped in
by just changing the import paths. There is currently no schematic for this, but you can run the
following string replace across your TypeScript files:
```bash
grep -lr --include="*.ts" --exclude-dir="node_modules" \
--exclude="*.d.ts" "['\"]@angular/material/menu['\"]" | xargs sed -i \
"s/['\"]@angular\/material\/menu['\"]/'@angular\/material-experimental\/mdc-menu'/g"
```
CSS styles and tests that depend on implementation details of mat-menu (such as getting elements
from the template by class name) will need to be manually updated.
There are some small visual differences between this menu and the standard mat-menu. This
menu has a different font size and elevation `box-shadow`.
@@ -1,18 +1,60 @@
@import '@material/menu-surface/mixins';
@import '@material/menu-surface/variables';
@import '@material/list/mixins';
@import '@material/list/variables';
@import '@material/theme/functions';
@import '../mdc-helpers/mdc-helpers';
@mixin mat-menu-theme-mdc($theme) {
@include mat-using-mdc-theme($theme) {
// TODO: MDC theme styles here.
@include mdc-menu-surface-core-styles($mat-theme-styles-query);
@include mdc-list-without-ripple($mat-theme-styles-query);
// MDC doesn't appear to have disabled styling for menu
// items so we have to grey them out ourselves.
.mat-mdc-menu-item[disabled] {
&, &::after {
@include mdc-theme-prop(color, text-disabled-on-background);
}
}
// Since we're creating `mat-icon` and the submenu trigger
// chevron ourselves, we have to handle the color as well.
.mat-mdc-menu-item .mat-icon-no-color,
.mat-mdc-menu-item-submenu-trigger::after {
@include mdc-theme-prop(color, text-icon-on-background);
}
// MDC's hover and focus styles are tied to their ripples which we aren't using.
.mat-mdc-menu-item:hover,
.mat-mdc-menu-item.cdk-program-focused,
.mat-mdc-menu-item.cdk-keyboard-focused,
.mat-mdc-menu-item-highlighted {
&:not([disabled]) {
$color: $mdc-theme-on-surface;
background: rgba($color, mdc-states-opacity($color, hover));
}
}
}
}
@mixin mat-menu-typography-mdc($config) {
@if config {
@include mat-using-mdc-typography($config) {
// TODO: MDC typography styles here.
@include mdc-menu-surface-core-styles($mat-typography-styles-query);
.mat-mdc-menu-content {
// Note that we include this private mixin, because the public
// one adds a bunch of styles that we aren't using for the menu.
@include mdc-list-base_($mat-typography-styles-query);
}
}
}
@else {
// TODO: MDC typography styles here.
@include mdc-menu-surface-core-styles($mat-typography-styles-query);
.mat-mdc-menu-content {
@include mdc-list-base_($mat-typography-styles-query);
}
}
}
@@ -0,0 +1,5 @@
<ng-content></ng-content>
<div class="mat-mdc-menu-ripple" matRipple
[matRippleDisabled]="disableRipple || disabled"
[matRippleTrigger]="_getHostElement()">
</div>
@@ -0,0 +1,37 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core';
import {MatMenuItem as BaseMatMenuItem} from '@angular/material/menu';
/**
* This directive is intended to be used inside an mat-menu tag.
* It exists mostly to set the role attribute.
*/
@Component({
moduleId: module.id,
selector: '[mat-menu-item]',
exportAs: 'matMenuItem',
inputs: ['disabled', 'disableRipple'],
host: {
'[attr.role]': 'role',
'class': 'mat-mdc-menu-item',
'[class.mat-mdc-menu-item-highlighted]': '_highlighted',
'[class.mat-mdc-menu-item-submenu-trigger]': '_triggersSubmenu',
'[attr.tabindex]': '_getTabIndex()',
'[attr.aria-disabled]': 'disabled',
'[attr.disabled]': 'disabled || null',
},
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
templateUrl: 'menu-item.html',
providers: [
{provide: BaseMatMenuItem, useExisting: MatMenuItem},
]
})
export class MatMenuItem extends BaseMatMenuItem {}
+16 -1
View File
@@ -1 +1,16 @@
<!-- TODO: MDC template here. -->
<ng-template>
<div
class="mat-mdc-menu-panel mdc-menu-surface mdc-menu-surface--open"
[ngClass]="_classList"
(keydown)="_handleKeydown($event)"
(click)="closed.emit('click')"
[@transformMenu]="_panelAnimationState"
(@transformMenu.start)="_onAnimationStart($event)"
(@transformMenu.done)="_onAnimationDone($event)"
tabindex="-1"
role="menu">
<div class="mat-mdc-menu-content mdc-list">
<ng-content></ng-content>
</div>
</div>
</ng-template>
+68 -1
View File
@@ -1 +1,68 @@
// TODO: MDC core styles here.
@import '@material/menu-surface/mixins';
@import '@material/list/mixins';
@import '@material/list/variables';
@import '../../material/core/style/menu-common';
@import '../../material/core/style/button-common';
@include mdc-menu-surface-core-styles($query: structure);
.mat-mdc-menu-content {
// Note that we include this private mixin, because the public
// one adds a bunch of styles that we aren't using for the menu.
@include mdc-list-base_($query: structure);
}
.mat-mdc-menu-panel {
// Include Material's base menu panel styling which contain the `min-width`, `max-width` and some
// styling to make scrolling smoother. MDC doesn't include the `min-width` and `max-width`, even
// though they're explicitly defined in spec.
@include mat-menu-base;
// The CDK positioning uses flexbox to anchor the element, whereas MDC uses `position: absolute`.
position: static;
}
.mat-mdc-menu-item {
// Note that we include this private mixin, because the public
// one adds a bunch of styles that we aren't using for the menu.
@include mdc-list-item-base_;
// MDC's menu items are `<li>` nodes which don't need resets, however ours
// can be anything, including buttons, so we need to do the reset ourselves.
@include mat-button-reset;
cursor: pointer;
width: 100%;
text-align: left;
box-sizing: border-box;
color: inherit;
font-size: inherit;
background: none;
&[disabled] {
cursor: default;
}
.mat-icon {
margin-right: $mdc-list-side-padding;
}
[dir='rtl'] & {
text-align: right;
.mat-icon {
margin-right: 0;
margin-left: $mdc-list-side-padding;
}
}
}
// Renders out a chevron on menu items that trigger a sub-menu.
.mat-mdc-menu-item-submenu-trigger {
@include mat-menu-item-submenu-trigger($mdc-list-side-padding);
}
// Increase specificity because ripple styles are part of the `mat-core` mixin and can
// potentially overwrite the absolute position of the container.
.mat-mdc-menu-item .mat-mdc-menu-ripple {
@include mat-menu-item-ripple;
}
File diff suppressed because it is too large Load Diff
+40 -8
View File
@@ -6,20 +6,52 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core';
import {ChangeDetectionStrategy, Component, ViewEncapsulation, Provider} from '@angular/core';
import {Overlay, ScrollStrategy} from '@angular/cdk/overlay';
import {
MatMenu as BaseMatMenu,
MAT_MENU_PANEL,
matMenuAnimations,
MAT_MENU_SCROLL_STRATEGY,
} from '@angular/material/menu';
/** @docs-private */
export function MAT_MENU_SCROLL_STRATEGY_FACTORY(overlay: Overlay): () => ScrollStrategy {
return () => overlay.scrollStrategies.reposition();
}
/** @docs-private */
export const MAT_MENU_SCROLL_STRATEGY_FACTORY_PROVIDER: Provider = {
provide: MAT_MENU_SCROLL_STRATEGY,
deps: [Overlay],
useFactory: MAT_MENU_SCROLL_STRATEGY_FACTORY,
};
@Component({
moduleId: module.id,
selector: 'mat-menu',
templateUrl: 'menu.html',
styleUrls: ['menu.css'],
host: {
'class': 'mat-mdc-menu',
},
exportAs: 'matMenu',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
exportAs: 'matMenu',
animations: [
matMenuAnimations.transformMenu,
matMenuAnimations.fadeInItems
],
providers: [
{provide: MAT_MENU_PANEL, useExisting: MatMenu},
{provide: BaseMatMenu, useExisting: MatMenu},
]
})
export class MatMenu {
// TODO: set up MDC foundation class.
export class MatMenu extends BaseMatMenu {
setElevation(_depth: number) {
// TODO(crisbeto): MDC's styles come with elevation already and we haven't mapped our mixins
// to theirs. Disable the elevation stacking for now until everything has been mapped.
// The following unit tests should be re-enabled:
// - should not remove mat-elevation class from overlay when panelClass is changed
// - should increase the sub-menu elevation based on its depth
// - should update the elevation when the same menu is opened at a different depth
// - should not increase the elevation if the user specified a custom one
}
}
+16 -7
View File
@@ -8,13 +8,22 @@
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {MatCommonModule} from '@angular/material/core';
import {MatMenu} from './menu';
import {MatCommonModule, MatRippleModule} from '@angular/material/core';
import {OverlayModule} from '@angular/cdk/overlay';
import {_MatMenuDirectivesModule} from '@angular/material/menu';
import {MatMenu, MAT_MENU_SCROLL_STRATEGY_FACTORY_PROVIDER} from './menu';
import {MatMenuItem} from './menu-item';
@NgModule({
imports: [MatCommonModule, CommonModule],
exports: [MatMenu, MatCommonModule],
declarations: [MatMenu],
imports: [
CommonModule,
MatRippleModule,
MatCommonModule,
OverlayModule,
_MatMenuDirectivesModule
],
exports: [MatMenu, MatMenuItem, _MatMenuDirectivesModule],
declarations: [MatMenu, MatMenuItem],
providers: [MAT_MENU_SCROLL_STRATEGY_FACTORY_PROVIDER]
})
export class MatMenuModule {
}
export class MatMenuModule {}
@@ -6,5 +6,22 @@
* found in the LICENSE file at https://angular.io/license
*/
export * from './menu';
export {MatMenu} from './menu';
export {MatMenuItem} from './menu-item';
export * from './module';
export {
_MatMenuDirectivesModule,
fadeInItems,
MAT_MENU_DEFAULT_OPTIONS,
MAT_MENU_PANEL,
MAT_MENU_SCROLL_STRATEGY,
matMenuAnimations,
MatMenuContent,
MatMenuDefaultOptions,
MatMenuPanel,
MatMenuTrigger,
MenuPositionX,
MenuPositionY,
transformMenu,
} from '@angular/material/menu';
+44
View File
@@ -1,5 +1,6 @@
@import './variables';
@import './list-common';
@import './layout-common';
@import './vendor-prefixes';
/** The mixins below are shared between mat-menu and mat-select */
@@ -57,3 +58,46 @@ $mat-menu-icon-margin: 16px !default;
}
}
}
@mixin mat-menu-item-submenu-trigger($side-padding, $triangle-height: 10px) {
// Increase the side padding to prevent the indicator from overlapping the text.
padding-right: $side-padding * 2;
// Renders a triangle to indicate that the menu item will open a sub-menu.
&::after {
$size: $triangle-height / 2;
width: 0;
height: 0;
border-style: solid;
border-width: $size 0 $size $size;
border-color: transparent transparent transparent currentColor;
content: '';
display: inline-block;
position: absolute;
top: 50%;
right: $side-padding;
transform: translateY(-50%);
}
[dir='rtl'] & {
padding-right: $side-padding;
padding-left: $side-padding * 2;
&::after {
right: auto;
left: $side-padding;
transform: rotateY(180deg) translateY(-50%);
}
}
}
@mixin mat-menu-item-ripple() {
@include mat-fill;
// Prevent any pointer events on the ripple container for a menu item. The ripple container is
// not the trigger element for the ripples and can be therefore disabled like that. Disabling
// the pointer events ensures that there is no `click` event that can bubble up and cause
// the menu panel to close.
pointer-events: none;
}
+3 -1
View File
@@ -41,7 +41,9 @@ export const matMenuAnimations: {
transform: 'scale(0.8)'
})),
transition('void => enter', group([
query('.mat-menu-content', animate('100ms linear', style({opacity: 1}))),
query('.mat-menu-content, .mat-mdc-menu-content', animate('100ms linear', style({
opacity: 1
}))),
animate('120ms cubic-bezier(0, 0, 0.2, 1)', style({transform: 'scale(1)'})),
])),
transition('* => void', animate('100ms 25ms linear', style({opacity: 0})))
+15 -3
View File
@@ -11,13 +11,24 @@ import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {MatCommonModule, MatRippleModule} from '@angular/material/core';
import {MatMenuContent} from './menu-content';
import {MatMenu} from './menu';
import {_MatMenu} from './menu';
import {MatMenuItem} from './menu-item';
import {
MatMenuTrigger,
MAT_MENU_SCROLL_STRATEGY_FACTORY_PROVIDER,
} from './menu-trigger';
/**
* Used by both the current `MatMenuModule` and the MDC `MatMenuModule`
* to declare the menu-related directives.
*/
@NgModule({
exports: [MatMenuTrigger, MatMenuContent, MatCommonModule],
declarations: [MatMenuTrigger, MatMenuContent],
providers: [MAT_MENU_SCROLL_STRATEGY_FACTORY_PROVIDER]
})
// tslint:disable-next-line:class-name
export class _MatMenuDirectivesModule {}
@NgModule({
imports: [
@@ -25,9 +36,10 @@ import {
MatCommonModule,
MatRippleModule,
OverlayModule,
_MatMenuDirectivesModule,
],
exports: [MatMenu, MatMenuItem, MatMenuTrigger, MatMenuContent, MatCommonModule],
declarations: [MatMenu, MatMenuItem, MatMenuTrigger, MatMenuContent],
exports: [_MatMenu, MatMenuItem, _MatMenuDirectivesModule],
declarations: [_MatMenu, MatMenuItem],
providers: [MAT_MENU_SCROLL_STRATEGY_FACTORY_PROVIDER]
})
export class MatMenuModule {}
+1 -1
View File
@@ -407,7 +407,7 @@ export class MatMenuTrigger implements AfterContentInit, OnDestroy {
positionStrategy: this._overlay.position()
.flexibleConnectedTo(this._element)
.withLockedPosition()
.withTransformOriginOn('.mat-menu-panel'),
.withTransformOriginOn('.mat-menu-panel, .mat-mdc-menu-panel'),
backdropClass: this.menu.backdropClass || 'cdk-overlay-transparent-backdrop',
scrollStrategy: this._scrollStrategy(),
direction: this._dir
+2 -38
View File
@@ -1,6 +1,5 @@
// TODO(kara): update vars for desktop when MD team responds
@import '../core/style/button-common';
@import '../core/style/layout-common';
@import '../core/style/menu-common';
@import '../../cdk/a11y/a11y';
@@ -51,36 +50,7 @@ $mat-menu-submenu-indicator-size: 10px !default;
}
.mat-menu-item-submenu-trigger {
// Increase the side padding to prevent the indicator from overlapping the text.
padding-right: $mat-menu-side-padding * 2;
// Renders a triangle to indicate that the menu item will open a sub-menu.
&::after {
$size: $mat-menu-submenu-indicator-size / 2;
width: 0;
height: 0;
border-style: solid;
border-width: $size 0 $size $size;
border-color: transparent transparent transparent currentColor;
content: '';
display: inline-block;
position: absolute;
top: 50%;
right: $mat-menu-side-padding;
transform: translateY(-50%);
}
[dir='rtl'] & {
padding-right: $mat-menu-side-padding;
padding-left: $mat-menu-side-padding * 2;
&::after {
right: auto;
left: $mat-menu-side-padding;
transform: rotateY(180deg) translateY(-50%);
}
}
@include mat-menu-item-submenu-trigger($mat-menu-side-padding);
}
button.mat-menu-item {
@@ -90,11 +60,5 @@ button.mat-menu-item {
// Increase specificity because ripple styles are part of the `mat-core` mixin and can
// potentially overwrite the absolute position of the container.
.mat-menu-item .mat-menu-ripple {
@include mat-fill;
// Prevent any pointer events on the ripple container for a menu item. The ripple container is
// not the trigger element for the ripples and can be therefore disabled like that. Disabling
// the pointer events ensures that there is no `click` event that can bubble up and cause
// the menu panel to close.
pointer-events: none;
@include mat-menu-item-ripple;
}
+38 -18
View File
@@ -89,24 +89,10 @@ export function MAT_MENU_DEFAULT_OPTIONS_FACTORY(): MatMenuDefaultOptions {
*/
const MAT_MENU_BASE_ELEVATION = 4;
@Component({
moduleId: module.id,
selector: 'mat-menu',
templateUrl: 'menu.html',
styleUrls: ['menu.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
exportAs: 'matMenu',
animations: [
matMenuAnimations.transformMenu,
matMenuAnimations.fadeInItems
],
providers: [
{provide: MAT_MENU_PANEL, useExisting: MatMenu}
]
})
export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnInit, OnDestroy {
/** Base class with all of the `MatMenu` functionality. */
// tslint:disable-next-line:class-name
export class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnInit,
OnDestroy {
private _keyManager: FocusKeyManager<MatMenuItem>;
private _xPosition: MenuPositionX = this._defaultOptions.xPosition;
private _yPosition: MenuPositionY = this._defaultOptions.yPosition;
@@ -426,3 +412,37 @@ export class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnI
}
}
}
export class MatMenu extends _MatMenuBase {}
// Note on the weird inheritance setup: we need three classes, because the MDC-based menu has to
// extend `MatMenu`, however keeping a reference to it will cause the inlined template and styles
// to be retained as well. The MDC menu also has to provide itself as a `MatMenu` in order for
// queries and DI to work correctly, while still not referencing the actual menu class.
// Class responsibility is split up as follows:
// * _MatMenuBase - provides all the functionality without any of the Angular metadata.
// * MatMenu - keeps the same name symbol name as the current menu and
// is used as a provider for DI and query purposes.
// * _MatMenu - the actual menu component implementation with the Angular metadata that should
// be tree shaken away for MDC.
@Component({
moduleId: module.id,
selector: 'mat-menu',
templateUrl: 'menu.html',
styleUrls: ['menu.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
exportAs: 'matMenu',
animations: [
matMenuAnimations.transformMenu,
matMenuAnimations.fadeInItems
],
providers: [
{provide: MAT_MENU_PANEL, useExisting: MatMenu},
{provide: MatMenu, useExisting: _MatMenu}
]
})
// tslint:disable-next-line:class-name
export class _MatMenu extends MatMenu {
}
+8 -2
View File
@@ -6,10 +6,16 @@
* found in the LICENSE file at https://angular.io/license
*/
export {MatMenu, MatMenuDefaultOptions, MAT_MENU_DEFAULT_OPTIONS} from './menu';
export {
MatMenu,
MatMenuDefaultOptions,
MAT_MENU_DEFAULT_OPTIONS,
_MatMenu,
_MatMenuBase,
} from './menu';
export {MatMenuItem} from './menu-item';
export {MatMenuTrigger, MAT_MENU_SCROLL_STRATEGY} from './menu-trigger';
export {MatMenuPanel} from './menu-panel';
export {MatMenuPanel, MAT_MENU_PANEL} from './menu-panel';
export * from './menu-module';
export * from './menu-animations';
export * from './menu-content';
@@ -17,9 +17,12 @@ Not yet implemented.
<mat-checkbox>with a label</mat-checkbox>
<h2>MDC menu</h2>
<!-- TODO: Copy kitchen sink examples for mat-menu here -->
Not yet implemented.
<button mat-button [matMenuTriggerFor]="menu">Open</button>
<mat-menu #menu="matMenu">
<button mat-menu-item>Mercy</button>
<button mat-menu-item>Lucio</button>
<button mat-menu-item disabled>Sombra</button>
</mat-menu>
<h2>MDC radio</h2>
+17 -6
View File
@@ -1,10 +1,7 @@
export declare const fadeInItems: AnimationTriggerMetadata;
export declare class _MatMenu extends MatMenu {
}
export declare const MAT_MENU_DEFAULT_OPTIONS: InjectionToken<MatMenuDefaultOptions>;
export declare const MAT_MENU_SCROLL_STRATEGY: InjectionToken<() => ScrollStrategy>;
export declare class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnInit, OnDestroy {
export declare class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnInit, OnDestroy {
_animationDone: Subject<AnimationEvent>;
_classList: {
[key: string]: boolean;
@@ -43,6 +40,20 @@ export declare class MatMenu implements AfterContentInit, MatMenuPanel<MatMenuIt
setPositionClasses(posX?: MenuPositionX, posY?: MenuPositionY): void;
}
export declare class _MatMenuDirectivesModule {
}
export declare const fadeInItems: AnimationTriggerMetadata;
export declare const MAT_MENU_DEFAULT_OPTIONS: InjectionToken<MatMenuDefaultOptions>;
export declare const MAT_MENU_PANEL: InjectionToken<MatMenuPanel<any>>;
export declare const MAT_MENU_SCROLL_STRATEGY: InjectionToken<() => ScrollStrategy>;
export declare class MatMenu extends _MatMenuBase {
}
export declare const matMenuAnimations: {
readonly transformMenu: AnimationTriggerMetadata;
readonly fadeInItems: AnimationTriggerMetadata;