Skip to content

Commit

Permalink
feat(dialog): add backdrop (#1041)
Browse files Browse the repository at this point in the history
* feat(dialog): add backdrop

* rebase on sync overlay

* fix lint errors

* fix typos
  • Loading branch information
jelbourn authored and hansl committed Sep 9, 2016
1 parent 3d5ceab commit 7ecda22
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 7 deletions.
53 changes: 52 additions & 1 deletion src/lib/core/overlay/overlay-ref.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,51 @@
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<any> = new Subject();

constructor(
private _portalHost: PortalHost,
private _pane: HTMLElement,
private _state: OverlayState) { }

attach(portal: Portal<any>): any {
if (this._state.hasBackdrop) {
this._attachBackdrop();
}

let attachResult = this._portalHost.attach(portal);
this.updatePosition();

return attachResult;
}

detach(): Promise<any> {
this._detatchBackdrop();
return this._portalHost.detach();
}

dispose(): void {
this._detatchBackdrop();
this._portalHost.dispose();
}

hasAttached(): boolean {
return this._portalHost.hasAttached();
}

backdropClick(): Observable<void> {
return this._backdropClick.asObservable();
}

/** Gets the current state config of the overlay. */
getState() {
return this._state;
Expand All @@ -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;
}
});
}
}
}
3 changes: 3 additions & 0 deletions src/lib/core/overlay/overlay-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 29 additions & 1 deletion src/lib/core/overlay/overlay.scss
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,12 +19,35 @@
left: 0;
height: 100%;
width: 100%;
z-index: $md-z-index-overlay-container;
}

/** A single overlay pane. */
.md-overlay-pane {
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;
}
27 changes: 25 additions & 2 deletions src/lib/core/overlay/overlay.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,6 +14,7 @@ describe('Overlay', () => {
let componentPortal: ComponentPortal<PizzaMsg>;
let templatePortal: TemplatePortal;
let overlayContainerElement: HTMLElement;
let viewContainerFixture: ComponentFixture<TestComponentWithTemplatePortals>;

beforeEach(async(() => {
TestBed.configureTestingModule({
Expand All @@ -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', () => {
Expand Down Expand Up @@ -80,7 +82,7 @@ describe('Overlay', () => {
expect(overlayContainerElement.textContent).toBe('');
});

describe('applyState', () => {
describe('positioning', () => {
let state: OverlayState;

beforeEach(() => {
Expand All @@ -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 = <HTMLElement> 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();
});
});
});


Expand Down
8 changes: 7 additions & 1 deletion src/lib/core/style/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 14 additions & 2 deletions src/lib/dialog/dialog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,6 @@ describe('MdDialog', () => {

viewContainerFixture.detectChanges();

viewContainerFixture.detectChanges();

let afterCloseResult: string;
dialogRef.afterClosed().subscribe(result => {
afterCloseResult = result;
Expand All @@ -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 = <HTMLElement> overlayContainerElement.querySelector('.md-overlay-backdrop');
backdrop.click();

expect(overlayContainerElement.querySelector('md-dialog-container')).toBeFalsy();
});
});


Expand Down
4 changes: 4 additions & 0 deletions src/lib/dialog/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ export class MdDialog {
// to modify and close it.
let dialogRef = <MdDialogRef<T>> 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.
Expand All @@ -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()
Expand Down

0 comments on commit 7ecda22

Please sign in to comment.