fix(menu): update to use overlay backdrop (#1534)

This commit is contained in:
Kara
2016-10-21 15:25:30 -07:00
committed by Jeremy Elbourn
parent 99396a47f6
commit add0d23b12
6 changed files with 49 additions and 30 deletions
-6
View File
@@ -15,12 +15,6 @@ describe('menu', () => {
expect(page.menu().getText()).toEqual("One\nTwo\nThree\nFour");
});
it('should close menu when area outside menu is clicked', () => {
page.trigger().click();
page.body().click();
page.expectMenuPresent(false);
});
it('should close menu when menu item is clicked', () => {
page.trigger().click();
page.items(0).click();
-10
View File
@@ -27,7 +27,6 @@ import {UP_ARROW, DOWN_ARROW, TAB} from '../core';
exportAs: 'mdMenu'
})
export class MdMenu {
_showClickCatcher: boolean = false;
private _focusedItemIndex: number = 0;
// config object to be passed into the menu's ngClass
@@ -61,15 +60,6 @@ export class MdMenu {
@Output() close = new EventEmitter;
/**
* This function toggles the display of the menu's click catcher element.
* This element covers the viewport when the menu is open to detect clicks outside the menu.
* TODO: internal
*/
_setClickCatcher(bool: boolean): void {
this._showClickCatcher = bool;
}
/**
* Focus the first item in the menu. This method is used by the menu trigger
* to focus the first item when the menu is opened by the ENTER key.
+22 -1
View File
@@ -23,6 +23,7 @@ import {
HorizontalConnectionPos,
VerticalConnectionPos
} from '../core';
import { Subscription } from 'rxjs/Subscription';
/**
* This directive is intended to be used in conjunction with an md-menu tag. It is
@@ -40,6 +41,7 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
private _portal: TemplatePortal;
private _overlayRef: OverlayRef;
private _menuOpen: boolean = false;
private _backdropSubscription: Subscription;
// tracking input type is necessary so it's possible to only auto-focus
// the first item of the list when the menu is opened via the keyboard
@@ -70,6 +72,7 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
if (!this._menuOpen) {
this._createOverlay();
this._overlayRef.attach(this._portal);
this._subscribeToBackdrop();
this._initMenu();
}
}
@@ -77,6 +80,7 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
closeMenu(): void {
if (this._overlayRef) {
this._overlayRef.detach();
this._backdropSubscription.unsubscribe();
this._resetMenu();
}
}
@@ -85,6 +89,10 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
if (this._overlayRef) {
this._overlayRef.dispose();
this._overlayRef = null;
if (this._backdropSubscription) {
this._backdropSubscription.unsubscribe();
}
}
}
@@ -92,6 +100,18 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
this._renderer.invokeElementMethod(this._element.nativeElement, 'focus');
}
/**
* This method ensures that the menu closes when the overlay backdrop is clicked.
* We do not use first() here because doing so would not catch clicks from within
* the menu, and it would fail to unsubscribe properly. Instead, we unsubscribe
* explicitly when the menu is closed or destroyed.
*/
private _subscribeToBackdrop(): void {
this._backdropSubscription = this._overlayRef.backdropClick().subscribe(() => {
this.closeMenu();
});
}
/**
* This method sets the menu state to open and focuses the first item if
* the menu was opened via the keyboard.
@@ -120,7 +140,6 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
// set state rather than toggle to support triggers sharing a menu
private _setIsMenuOpen(isOpen: boolean): void {
this._menuOpen = isOpen;
this.menu._setClickCatcher(isOpen);
this._menuOpen ? this.onMenuOpen.emit(null) : this.onMenuClose.emit(null);
}
@@ -152,6 +171,8 @@ export class MdMenuTrigger implements AfterViewInit, OnDestroy {
private _getOverlayConfig(): OverlayState {
const overlayState = new OverlayState();
overlayState.positionStrategy = this._getPosition();
overlayState.hasBackdrop = true;
overlayState.backdropClass = 'md-overlay-transparent-backdrop';
return overlayState;
}
+1 -1
View File
@@ -4,4 +4,4 @@
<ng-content></ng-content>
</div>
</template>
<div class="md-menu-click-catcher" *ngIf="_showClickCatcher" (click)="_emitCloseEvent()"></div>
+1 -5
View File
@@ -52,8 +52,4 @@ $md-menu-vertical-padding: 8px !default;
button[md-menu-item] {
width: 100%;
}
.md-menu-click-catcher {
@include md-fullscreen();
}
}
+25 -7
View File
@@ -1,40 +1,58 @@
import {TestBed, async} from '@angular/core/testing';
import {Component, ViewChild} from '@angular/core';
import {By} from '@angular/platform-browser';
import {MdMenuModule, MdMenuTrigger} from './menu';
import {OverlayContainer} from '../core/overlay/overlay-container';
describe('MdMenu', () => {
let overlayContainerElement: HTMLElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdMenuModule.forRoot()],
declarations: [SimpleMenu],
providers: [
{provide: OverlayContainer, useFactory: () => {
overlayContainerElement = document.createElement('div');
return {getContainerElement: () => overlayContainerElement};
}}
]
});
TestBed.compileComponents();
}));
it('should open the menu as an idempotent operation', () => {
let fixture = TestBed.createComponent(SimpleMenu);
const fixture = TestBed.createComponent(SimpleMenu);
fixture.detectChanges();
let menu = fixture.debugElement.query(By.css('.md-menu-panel'));
expect(menu).toBe(null);
expect(overlayContainerElement.textContent).toBe('');
expect(() => {
fixture.componentInstance.trigger.openMenu();
fixture.componentInstance.trigger.openMenu();
menu = fixture.debugElement.query(By.css('.md-menu-panel'));
expect(menu.nativeElement.innerHTML.trim()).toEqual('Content');
expect(overlayContainerElement.textContent.trim()).toBe('Content');
}).not.toThrowError();
});
it('should close the menu when a click occurs outside the menu', () => {
const fixture = TestBed.createComponent(SimpleMenu);
fixture.detectChanges();
fixture.componentInstance.trigger.openMenu();
const backdrop = <HTMLElement>overlayContainerElement.querySelector('.md-overlay-backdrop');
backdrop.click();
fixture.detectChanges();
expect(overlayContainerElement.textContent).toBe('');
});
});
@Component({
template: `
<button [md-menu-trigger-for]="menu">Toggle menu</button>
<md-menu #menu="mdMenu">
Content
<button md-menu-item> Content </button>
</md-menu>
`
})