diff --git a/src/lib/core/overlay/overlay-ref.ts b/src/lib/core/overlay/overlay-ref.ts index 0485083c52d9..5d3b11717f7a 100644 --- a/src/lib/core/overlay/overlay-ref.ts +++ b/src/lib/core/overlay/overlay-ref.ts @@ -1,17 +1,27 @@ import {PortalHost, Portal} from '../portal/portal'; import {OverlayState} from './overlay-state'; +import {Observable} from 'rxjs/Observable'; +import {Subject} from 'rxjs/Subject'; + /** * Reference to an overlay that has been created with the Overlay service. * Used to manipulate or dispose of said overlay. */ export class OverlayRef implements PortalHost { + private _backdropElement: HTMLElement = null; + private _backdropClick: Subject = new Subject(); + constructor( private _portalHost: PortalHost, private _pane: HTMLElement, private _state: OverlayState) { } attach(portal: Portal): any { + if (this._state.hasBackdrop) { + this._attachBackdrop(); + } + let attachResult = this._portalHost.attach(portal); this.updatePosition(); @@ -19,10 +29,12 @@ export class OverlayRef implements PortalHost { } detach(): Promise { + this._detatchBackdrop(); return this._portalHost.detach(); } dispose(): void { + this._detatchBackdrop(); this._portalHost.dispose(); } @@ -30,6 +42,10 @@ export class OverlayRef implements PortalHost { return this._portalHost.hasAttached(); } + backdropClick(): Observable { + return this._backdropClick.asObservable(); + } + /** Gets the current state config of the overlay. */ getState() { return this._state; @@ -42,5 +58,40 @@ export class OverlayRef implements PortalHost { } } - // TODO(jelbourn): add additional methods for manipulating the overlay. + /** Attaches a backdrop for this overlay. */ + private _attachBackdrop() { + this._backdropElement = document.createElement('div'); + this._backdropElement.classList.add('md-overlay-backdrop'); + this._pane.parentElement.appendChild(this._backdropElement); + + // Forward backdrop clicks such that the consumer of the overlay can perform whatever + // action desired when such a click occurs (usually closing the overlay). + this._backdropElement.addEventListener('click', () => { + this._backdropClick.next(null); + }); + + // Add class to fade-in the backdrop after one frame. + requestAnimationFrame(() => { + this._backdropElement.classList.add('md-overlay-backdrop-showing'); + }); + } + + /** Detaches the backdrop (if any) associated with the overlay. */ + private _detatchBackdrop(): void { + let backdropToDetach = this._backdropElement; + + if (backdropToDetach) { + backdropToDetach.classList.remove('md-overlay-backdrop-showing'); + backdropToDetach.addEventListener('transitionend', () => { + backdropToDetach.parentNode.removeChild(backdropToDetach); + + // It is possible that a new portal has been attached to this overlay since we started + // removing the backdrop. If that is the case, only clear the backdrop reference if it + // is still the same instance that we started to remove. + if (this._backdropElement == backdropToDetach) { + this._backdropElement = null; + } + }); + } + } } diff --git a/src/lib/core/overlay/overlay-state.ts b/src/lib/core/overlay/overlay-state.ts index 573dd034638a..8f9d39de72d0 100644 --- a/src/lib/core/overlay/overlay-state.ts +++ b/src/lib/core/overlay/overlay-state.ts @@ -9,6 +9,9 @@ export class OverlayState { /** Strategy with which to position the overlay. */ positionStrategy: PositionStrategy; + /** Whether the overlay has a backdrop. */ + hasBackdrop: boolean = false; + // TODO(jelbourn): configuration still to add // - overlay size // - focus trap diff --git a/src/lib/core/overlay/overlay.scss b/src/lib/core/overlay/overlay.scss index d04ea0f48f72..473b7111539d 100644 --- a/src/lib/core/overlay/overlay.scss +++ b/src/lib/core/overlay/overlay.scss @@ -1,3 +1,8 @@ +@import 'variables'; +@import 'palette'; + +$md-backdrop-color: md-color($md-grey, 900); + // TODO(jelbourn): change from the `md` prefix to something else for everything in the toolkit. @import 'variables'; @@ -14,6 +19,7 @@ left: 0; height: 100%; width: 100%; + z-index: $md-z-index-overlay-container; } /** A single overlay pane. */ @@ -21,5 +27,27 @@ position: absolute; pointer-events: auto; box-sizing: border-box; - z-index: $z-index-overlay; + z-index: $md-z-index-overlay; +} + +.md-overlay-backdrop { + // TODO(jelbourn): reuse sidenav fullscreen mixin. + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + + z-index: $md-z-index-overlay-backdrop; + pointer-events: auto; + + // TODO(jelbourn): figure out if there are actually spec'ed colors for both light and dark + // themes here. Currently using the values from Angular Material 1. + transition: opacity $swift-ease-out-duration $swift-ease-out-timing-function; + background: $md-backdrop-color; + opacity: 0; +} + +.md-overlay-backdrop.md-overlay-backdrop-showing { + opacity: 0.48; } diff --git a/src/lib/core/overlay/overlay.spec.ts b/src/lib/core/overlay/overlay.spec.ts index facbfac68f1f..7029286f6826 100644 --- a/src/lib/core/overlay/overlay.spec.ts +++ b/src/lib/core/overlay/overlay.spec.ts @@ -1,4 +1,4 @@ -import {inject, TestBed, async} from '@angular/core/testing'; +import {inject, TestBed, async, ComponentFixture} from '@angular/core/testing'; import {NgModule, Component, ViewChild, ViewContainerRef} from '@angular/core'; import {TemplatePortalDirective, PortalModule} from '../portal/portal-directives'; import {TemplatePortal, ComponentPortal} from '../portal/portal'; @@ -14,6 +14,7 @@ describe('Overlay', () => { let componentPortal: ComponentPortal; let templatePortal: TemplatePortal; let overlayContainerElement: HTMLElement; + let viewContainerFixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ @@ -36,6 +37,7 @@ describe('Overlay', () => { fixture.detectChanges(); templatePortal = fixture.componentInstance.templatePortal; componentPortal = new ComponentPortal(PizzaMsg, fixture.componentInstance.viewContainerRef); + viewContainerFixture = fixture; })); it('should load a component into an overlay', () => { @@ -80,7 +82,7 @@ describe('Overlay', () => { expect(overlayContainerElement.textContent).toBe(''); }); - describe('applyState', () => { + describe('positioning', () => { let state: OverlayState; beforeEach(() => { @@ -95,6 +97,27 @@ describe('Overlay', () => { expect(overlayContainerElement.querySelectorAll('.fake-positioned').length).toBe(1); }); }); + + describe('backdrop', () => { + it('should create and destroy an overlay backdrop', () => { + let config = new OverlayState(); + config.hasBackdrop = true; + + let overlayRef = overlay.create(config); + overlayRef.attach(componentPortal); + + viewContainerFixture.detectChanges(); + let backdrop = overlayContainerElement.querySelector('.md-overlay-backdrop'); + expect(backdrop).toBeTruthy(); + expect(backdrop.classList).not.toContain('.md-overlay-backdrop-showing'); + + let backdropClickHandler = jasmine.createSpy('backdropClickHander'); + overlayRef.backdropClick().subscribe(backdropClickHandler); + + backdrop.click(); + expect(backdropClickHandler).toHaveBeenCalled(); + }); + }); }); diff --git a/src/lib/core/style/_variables.scss b/src/lib/core/style/_variables.scss index d69d5ad651cc..818d33755804 100644 --- a/src/lib/core/style/_variables.scss +++ b/src/lib/core/style/_variables.scss @@ -10,9 +10,15 @@ $md-xsmall: 'max-width: 600px'; // TODO: Revisit all z-indices before beta // z-index master list + $z-index-fab: 20 !default; $z-index-drawer: 100 !default; -$z-index-overlay: 1000 !default; + +// Overlay z indices. +$md-z-index-overlay: 1000; +$md-z-index-overlay-container: 1; +$md-z-index-overlay-backdrop: 1; + // Global constants $pi: 3.14159265; diff --git a/src/lib/dialog/dialog.spec.ts b/src/lib/dialog/dialog.spec.ts index 3513421636dc..3a669e8a6e4c 100644 --- a/src/lib/dialog/dialog.spec.ts +++ b/src/lib/dialog/dialog.spec.ts @@ -76,8 +76,6 @@ describe('MdDialog', () => { viewContainerFixture.detectChanges(); - viewContainerFixture.detectChanges(); - let afterCloseResult: string; dialogRef.afterClosed().subscribe(result => { afterCloseResult = result; @@ -88,6 +86,20 @@ describe('MdDialog', () => { expect(afterCloseResult).toBe('Charmander'); expect(overlayContainerElement.querySelector('md-dialog-container')).toBeNull(); }); + + it('should close when clicking on the overlay backdrop', () => { + let config = new MdDialogConfig(); + config.viewContainerRef = testViewContainerRef; + + dialog.open(PizzaMsg, config); + + viewContainerFixture.detectChanges(); + + let backdrop = overlayContainerElement.querySelector('.md-overlay-backdrop'); + backdrop.click(); + + expect(overlayContainerElement.querySelector('md-dialog-container')).toBeFalsy(); + }); }); diff --git a/src/lib/dialog/dialog.ts b/src/lib/dialog/dialog.ts index 775a7cdde2e5..d98e8a8b2ca5 100644 --- a/src/lib/dialog/dialog.ts +++ b/src/lib/dialog/dialog.ts @@ -87,6 +87,9 @@ export class MdDialog { // to modify and close it. let dialogRef = > new MdDialogRef(overlayRef); + // When the dialog backdrop is clicked, we want to close it. + overlayRef.backdropClick().subscribe(() => dialogRef.close()); + // We create an injector specifically for the component we're instantiating so that it can // inject the MdDialogRef. This allows a component loaded inside of a dialog to close itself // and, optionally, to return a value. @@ -108,6 +111,7 @@ export class MdDialog { private _getOverlayState(dialogConfig: MdDialogConfig): OverlayState { let state = new OverlayState(); + state.hasBackdrop = true; state.positionStrategy = this._overlay.position() .global() .centerHorizontally()