Skip to content

feat(menu): add animations #1685

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

Merged
merged 1 commit into from
Nov 3, 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
2 changes: 1 addition & 1 deletion e2e/components/menu/menu-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class MenuPage {

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

body() { return element(by.tagName('body')); }
backdrop() { return element(by.css('.md-overlay-backdrop')); }

items(index: number) {
return element.all(by.css('[md-menu-item]')).get(index);
Expand Down
11 changes: 7 additions & 4 deletions e2e/components/menu/menu.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,23 @@ describe('menu', () => {
expect(page.menu().getText()).toEqual("One\nTwo\nThree\nFour");
page.expectMenuAlignedWith(page.menu(), 'trigger-two');

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

// TODO(kara): temporary, remove when #1607 is fixed
browser.sleep(250);
page.trigger().click();
expect(page.menu().getText()).toEqual("One\nTwo\nThree\nFour");
page.expectMenuAlignedWith(page.menu(), 'trigger');

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

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-panel custom');
expect(classes).toContain('md-menu-panel custom');
});
});

Expand Down Expand Up @@ -110,9 +112,10 @@ describe('menu', () => {
page.pressKey(protractor.Key.TAB);
page.expectMenuPresent(false);

page.start().click();
page.pressKey(protractor.Key.TAB);
page.pressKey(protractor.Key.ENTER);
page.expectMenuPresent(true);

page.pressKey(protractor.Key.chord(protractor.Key.SHIFT, protractor.Key.TAB));
page.expectMenuPresent(false);
});
Expand Down
5 changes: 4 additions & 1 deletion src/e2e-app/e2e-app-module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {BrowserModule, AnimationDriver} from '@angular/platform-browser';
import {RouterModule} from '@angular/router';
import {SimpleCheckboxes} from './checkbox/checkbox-e2e';
import {E2EApp, Home} from './e2e-app/e2e-app';
Expand Down Expand Up @@ -29,5 +29,8 @@ import {E2E_APP_ROUTES} from './e2e-app/routes';
Home,
],
bootstrap: [E2EApp],
providers: [
{provide: AnimationDriver, useValue: AnimationDriver.NOOP}
]
})
export class E2eAppModule { }
2 changes: 2 additions & 0 deletions src/e2e-app/system-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ System.config({
'@angular/forms': 'vendor/@angular/forms/bundles/forms.umd.js',
'@angular/router': 'vendor/@angular/router/bundles/router.umd.js',
'@angular/platform-browser': 'vendor/@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser/testing':
'vendor/@angular/platform-browser/bundles/platform-browser-testing.umd.js',
'@angular/platform-browser-dynamic':
'vendor/@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
},
Expand Down
26 changes: 22 additions & 4 deletions src/lib/core/style/_menu-common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ $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;

@mixin md-menu-base() {
@include md-elevation(2);
Expand All @@ -20,9 +19,6 @@ $md-menu-vertical-padding: 8px !default;

overflow: auto;
-webkit-overflow-scrolling: touch; // for momentum scroll on mobile

padding-top: $md-menu-vertical-padding;
padding-bottom: $md-menu-vertical-padding;
}

@mixin md-menu-item-base() {
Expand All @@ -43,3 +39,25 @@ $md-menu-vertical-padding: 8px !default;
cursor: default;
}
}

/**
* This mixin adds the correct panel transform styles based
* on the direction that the menu panel opens.
*/
@mixin md-menu-positions() {
&.md-menu-after.md-menu-below {
transform-origin: left top;
}

&.md-menu-after.md-menu-above {
transform-origin: left bottom;
}

&.md-menu-before.md-menu-below {
transform-origin: right top;
}

&.md-menu-before.md-menu-above {
transform-origin: right bottom;
}
}
2 changes: 1 addition & 1 deletion src/lib/menu/_menu-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
$background: map-get($theme, background);
$foreground: map-get($theme, foreground);

.md-menu-panel {
.md-menu-content {
background: md-color($background, 'card');
}

Expand Down
53 changes: 53 additions & 0 deletions src/lib/menu/menu-animations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import{
AnimationEntryMetadata,
trigger,
state,
style,
animate,
transition
} from '@angular/core';

/**
* Below are all the animations for the md-menu component.
* Animation duration and timing values are based on Material 1.
*/


/**
* This animation controls the menu panel's entry and exit from the page.
*
* When the menu panel is added to the DOM, it scales in and fades in its border.
*
* When the menu panel is removed from the DOM, it simply fades out after a brief
* delay to display the ripple.
*
* TODO(kara): switch to :enter and :leave once Mobile Safari is sorted out.
*/
export const transformMenu: AnimationEntryMetadata = trigger('transformMenu', [
state('showing', style({
opacity: 1,
transform: `scale(1)`
})),
transition('void => *', [
style({
opacity: 0,
transform: `scale(0)`
}),
animate(`200ms cubic-bezier(0.25, 0.8, 0.25, 1)`)
]),
transition('* => void', [
animate('50ms 100ms linear', style({opacity: 0}))
])
]);

/**
* This animation fades in the background color and content of the menu panel
* after its containing element is scaled in.
*/
export const fadeInItems: AnimationEntryMetadata = trigger('fadeInItems', [
state('showing', style({opacity: 1})),
transition('void => *', [
style({opacity: 0}),
animate(`200ms 100ms cubic-bezier(0.55, 0, 0.55, 0.2)`)
])
]);
26 changes: 23 additions & 3 deletions src/lib/menu/menu-directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import {
QueryList,
TemplateRef,
ViewChild,
ViewEncapsulation
ViewEncapsulation,
} from '@angular/core';
import {MenuPositionX, MenuPositionY} from './menu-positions';
import {MdMenuInvalidPositionX, MdMenuInvalidPositionY} from './menu-errors';
import {MdMenuItem} from './menu-item';
import {ListKeyManager} from '../core/a11y/list-key-manager';
import {MdMenuPanel} from './menu-panel';
import {Subscription} from 'rxjs/Subscription';
import {transformMenu, fadeInItems} from './menu-animations';

@Component({
moduleId: module.id,
Expand All @@ -28,6 +29,10 @@ import {Subscription} from 'rxjs/Subscription';
templateUrl: 'menu.html',
styleUrls: ['menu.css'],
encapsulation: ViewEncapsulation.None,
animations: [
transformMenu,
fadeInItems
],
exportAs: 'mdMenu'
})
export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
Expand All @@ -37,7 +42,7 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
private _tabSubscription: Subscription;

/** Config object to be passed into the menu's ngClass */
_classList: Object;
_classList: any = {};

positionX: MenuPositionX = 'after';
positionY: MenuPositionY = 'below';
Expand All @@ -49,6 +54,7 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
@Attribute('y-position') posY: MenuPositionY) {
if (posX) { this._setPositionX(posX); }
if (posY) { this._setPositionY(posY); }
this._setPositionClasses();
}

// TODO: internal
Expand Down Expand Up @@ -77,6 +83,7 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
obj[className] = true;
return obj;
}, {});
this._setPositionClasses();
}

@Output() close = new EventEmitter<void>();
Expand All @@ -91,11 +98,12 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
this.items.first.focus();
this._keyManager.focusedItemIndex = 0;
}

/**
* This emits a close event to which the trigger is subscribed. When emitted, the
* trigger will close the menu.
*/
private _emitCloseEvent(): void {
_emitCloseEvent(): void {
this.close.emit();
}

Expand All @@ -112,4 +120,16 @@ export class MdMenu implements AfterContentInit, MdMenuPanel, OnDestroy {
}
this.positionY = pos;
}

/**
* It's necessary to set position-based classes to ensure the menu panel animation
* folds out from the correct direction.
*/
private _setPositionClasses() {
this._classList['md-menu-before'] = this.positionX == 'before';
this._classList['md-menu-after'] = this.positionX == 'after';
this._classList['md-menu-above'] = this.positionY == 'above';
this._classList['md-menu-below'] = this.positionY == 'below';
}

}
4 changes: 4 additions & 0 deletions src/lib/menu/menu-item.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<ng-content></ng-content>
<div class="md-menu-ripple" *ngIf="!disabled" md-ripple md-ripple-background-color="rgba(0,0,0,0)"
[md-ripple-trigger]="_getHostElement()">
</div>
16 changes: 10 additions & 6 deletions src/lib/menu/menu-item.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import {Directive, ElementRef, Input, HostBinding, Renderer} from '@angular/core';
import {Component, ElementRef, Input, HostBinding, Renderer} from '@angular/core';
import {MdFocusable} from '../core/a11y/list-key-manager';

/**
* This directive is intended to be used inside an md-menu tag.
* It exists mostly to set the role attribute.
*/
@Directive({
@Component({
moduleId: module.id,
selector: '[md-menu-item]',
host: {
'role': 'menuitem',
'(click)': '_checkDisabled($event)',
'tabindex': '-1'
},
templateUrl: 'menu-item.html',
exportAs: 'mdMenuItem'
})
export class MdMenuItem implements MdFocusable {
Expand All @@ -36,12 +38,14 @@ export class MdMenuItem implements MdFocusable {

@HostBinding('attr.aria-disabled')
get isAriaDisabled(): string {
return String(this.disabled);
return String(!!this.disabled);
}


_getHostElement(): HTMLElement {
return this._elementRef.nativeElement;
}

/**
* TODO: internal
*/
_checkDisabled(event: Event) {
if (this.disabled) {
event.preventDefault();
Expand Down
5 changes: 2 additions & 3 deletions src/lib/menu/menu-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
Input,
Output,
EventEmitter,
HostListener,
ViewContainerRef,
AfterViewInit,
OnDestroy,
Expand Down Expand Up @@ -33,7 +32,8 @@ import { Subscription } from 'rxjs/Subscription';
selector: '[md-menu-trigger-for]',
host: {
'aria-haspopup': 'true',
'(keydown)': '_handleKeydown($event)'
'(keydown)': '_handleKeydown($event)',
'(click)': 'toggleMenu()'
},
exportAs: 'mdMenuTrigger'
})
Expand Down Expand Up @@ -63,7 +63,6 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {

get menuOpen(): boolean { return this._menuOpen; }

@HostListener('click')
toggleMenu(): void {
return this._menuOpen ? this.closeMenu() : this.openMenu();
}
Expand Down
8 changes: 5 additions & 3 deletions src/lib/menu/menu.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<template>
<div class="md-menu-panel" [ngClass]="_classList"
(click)="_emitCloseEvent()" (keydown)="_keyManager.onKeydown($event)">
<ng-content></ng-content>
<div class="md-menu-panel" [ngClass]="_classList" (keydown)="_keyManager.onKeydown($event)"
(click)="_emitCloseEvent()" [@transformMenu]="'showing'">
<div class="md-menu-content" [@fadeInItems]="'showing'">
<ng-content></ng-content>
</div>
</div>
</template>

17 changes: 17 additions & 0 deletions src/lib/menu/menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,35 @@
@import '../core/style/sidenav-common';
@import '../core/style/menu-common';

$md-menu-vertical-padding: 8px !default;

.md-menu-panel {
@include md-menu-base();
@include md-menu-positions();

// max height must be 100% of the viewport height + one row height
max-height: calc(100vh + 48px);
}

.md-menu-content {
padding-top: $md-menu-vertical-padding;
padding-bottom: $md-menu-vertical-padding;
}

[md-menu-item] {
@include md-button-reset();
@include md-menu-item-base();
position: relative;
}

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

.md-menu-ripple {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
Loading