Skip to content

Commit 3a5bc2c

Browse files
committed
feat(dialog): add backdrop
1 parent d6f3e77 commit 3a5bc2c

File tree

7 files changed

+134
-7
lines changed

7 files changed

+134
-7
lines changed

src/lib/core/overlay/overlay-ref.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,51 @@
11
import {PortalHost, Portal} from '../portal/portal';
22
import {OverlayState} from './overlay-state';
3+
import {Observable} from 'rxjs/Observable';
4+
import {Subject} from 'rxjs/Subject';
5+
36

47
/**
58
* Reference to an overlay that has been created with the Overlay service.
69
* Used to manipulate or dispose of said overlay.
710
*/
811
export class OverlayRef implements PortalHost {
12+
private _backdropElement: HTMLElement = null;
13+
private _backdropClick: Subject<any> = new Subject();
14+
915
constructor(
1016
private _portalHost: PortalHost,
1117
private _pane: HTMLElement,
1218
private _state: OverlayState) { }
1319

1420
attach(portal: Portal<any>): any {
21+
if (this._state.hasBackdrop) {
22+
this._attachBackdrop();
23+
}
24+
1525
let attachResult = this._portalHost.attach(portal);
1626
this.updatePosition();
1727

1828
return attachResult;
1929
}
2030

2131
detach(): Promise<any> {
32+
this._detatchBackdrop();
2233
return this._portalHost.detach();
2334
}
2435

2536
dispose(): void {
37+
this._detatchBackdrop();
2638
this._portalHost.dispose();
2739
}
2840

2941
hasAttached(): boolean {
3042
return this._portalHost.hasAttached();
3143
}
3244

45+
backdropClick(): Observable<void> {
46+
return this._backdropClick.asObservable();
47+
}
48+
3349
/** Gets the current state config of the overlay. */
3450
getState() {
3551
return this._state;
@@ -42,5 +58,40 @@ export class OverlayRef implements PortalHost {
4258
}
4359
}
4460

45-
// TODO(jelbourn): add additional methods for manipulating the overlay.
61+
/** Attaches a backdrop for this overlay. */
62+
private _attachBackdrop() {
63+
this._backdropElement = document.createElement('div');
64+
this._backdropElement.classList.add('md-overlay-backdrop');
65+
this._pane.parentElement.appendChild(this._backdropElement);
66+
67+
// Forward backdrop clicks that that the consumer of the overlay can perform whatever
68+
// action desired when such a click occurs (usually closing the overlay).
69+
this._backdropElement.addEventListener('click', () => {
70+
this._backdropClick.next(null);
71+
});
72+
73+
// Add class to fade-in the backdrop after one frame.
74+
requestAnimationFrame(() => {
75+
this._backdropElement.classList.add('md-overlay-backdrop-showing');
76+
});
77+
}
78+
79+
/** Detaches the backdrop (if any) associated with the overlay. */
80+
private _detatchBackdrop(): void {
81+
let backdropToDetach = this._backdropElement;
82+
83+
if (backdropToDetach) {
84+
backdropToDetach.classList.remove('md-overlay-backdrop-showing');
85+
backdropToDetach.addEventListener('transitionend', () => {
86+
backdropToDetach.parentNode.removeChild(backdropToDetach);
87+
88+
// It is possible that a new portal has been attached to this overlay since we started
89+
// removing the backdrop. If that is the case, only clear our the backdrop reference if it
90+
// is still the same instance that we started to remove.
91+
if (this._backdropElement == backdropToDetach) {
92+
this._backdropElement = null;
93+
}
94+
});
95+
}
96+
}
4697
}

src/lib/core/overlay/overlay-state.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export class OverlayState {
99
/** Strategy with which to position the overlay. */
1010
positionStrategy: PositionStrategy;
1111

12+
/** Whether the overlay has a backdrop. */
13+
hasBackdrop: boolean = false;
14+
1215
// TODO(jelbourn): configuration still to add
1316
// - overlay size
1417
// - focus trap

src/lib/core/overlay/overlay.scss

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
@import 'variables';
2+
@import 'palette';
3+
4+
$md-backdrop-color: md-color($md-grey, 900);
5+
16
// TODO(jelbourn): change from the `md` prefix to something else for everything in the toolkit.
27

38
@import 'variables';
@@ -14,12 +19,35 @@
1419
left: 0;
1520
height: 100%;
1621
width: 100%;
22+
23+
z-index: 1;
1724
}
1825

1926
/** A single overlay pane. */
2027
.md-overlay-pane {
2128
position: absolute;
2229
pointer-events: auto;
2330
box-sizing: border-box;
24-
z-index: $z-index-overlay;
31+
z-index: $md-z-index-overlay;
32+
}
33+
34+
.md-overlay-backdrop {
35+
position: absolute;
36+
top: 0;
37+
bottom: 0;
38+
left: 0;
39+
right: 0;
40+
41+
z-index: $md-z-index-overlay-backdrop;
42+
pointer-events: auto;
43+
44+
// TODO(jelbourn): figure out if there are actually spec'ed colors for both light and dark
45+
// themes here. Currently using the values from Angular Material 1.
46+
transition: opacity $swift-ease-out-duration $swift-ease-out-timing-function;
47+
background: $md-backdrop-color;
48+
opacity: 0;
49+
}
50+
51+
.md-overlay-backdrop.md-overlay-backdrop-showing {
52+
opacity: .48;
2553
}

src/lib/core/overlay/overlay.spec.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import {inject, TestBed, async} from '@angular/core/testing';
1+
import {inject, TestBed, async, ComponentFixture} from '@angular/core/testing';
22
import {NgModule, Component, ViewChild, ViewContainerRef} from '@angular/core';
33
import {TemplatePortalDirective, PortalModule} from '../portal/portal-directives';
44
import {TemplatePortal, ComponentPortal} from '../portal/portal';
55
import {Overlay} from './overlay';
6+
import {OverlayRef} from './overlay-ref';
67
import {OverlayContainer} from './overlay-container';
78
import {OverlayState} from './overlay-state';
89
import {PositionStrategy} from './position/position-strategy';
@@ -14,6 +15,7 @@ describe('Overlay', () => {
1415
let componentPortal: ComponentPortal<PizzaMsg>;
1516
let templatePortal: TemplatePortal;
1617
let overlayContainerElement: HTMLElement;
18+
let viewContainerFixture: ComponentFixture<TestComponentWithTemplatePortals>;
1719

1820
beforeEach(async(() => {
1921
TestBed.configureTestingModule({
@@ -80,7 +82,7 @@ describe('Overlay', () => {
8082
expect(overlayContainerElement.textContent).toBe('');
8183
});
8284

83-
describe('applyState', () => {
85+
describe('positioning', () => {
8486
let state: OverlayState;
8587

8688
beforeEach(() => {
@@ -95,6 +97,28 @@ describe('Overlay', () => {
9597
expect(overlayContainerElement.querySelectorAll('.fake-positioned').length).toBe(1);
9698
});
9799
});
100+
101+
describe('backdrop', () => {
102+
it('should create and destroy an overlay backdrop', () => {
103+
let config = new OverlayState();
104+
config.hasBackdrop = true;
105+
106+
let overlayRef = overlay.create(config).attach(componentPortal);
107+
108+
viewContainerFixture.whenStable().then(() => {
109+
viewContainerFixture.detectChanges();
110+
let backdrop = <HTMLElement> overlayContainerElement.querySelector('.md-overlay-backdrop');
111+
expect(backdrop).toBeTruthy();
112+
expect(backdrop.classList).not.toContain('.md-overlay-backdrop-showing');
113+
114+
let backdropClickHandler = jasmine.createSpy('backdropClickHander');
115+
overlayRef.backdropClick().subscribe(backdropClickHandler);
116+
117+
backdrop.click();
118+
expect(backdropClickHandler).toHaveBeenCalled();
119+
});
120+
});
121+
});
98122
});
99123

100124

src/lib/core/style/_variables.scss

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ $md-xsmall: 'max-width: 600px';
1010

1111
// TODO: Revisit all z-indices before beta
1212
// z-index master list
13+
1314
$z-index-fab: 20 !default;
1415
$z-index-drawer: 100 !default;
15-
$z-index-overlay: 1000 !default;
16+
17+
// Overlay z indices.
18+
$md-z-index-overlay: 1000;
19+
$md-z-index-overlay-backdrop: 1;
20+
1621

1722
// Global constants
1823
$pi: 3.14159265;

src/lib/dialog/dialog.spec.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,6 @@ describe('MdDialog', () => {
7676

7777
viewContainerFixture.detectChanges();
7878

79-
viewContainerFixture.detectChanges();
80-
8179
let afterCloseResult: string;
8280
dialogRef.afterClosed().subscribe(result => {
8381
afterCloseResult = result;
@@ -88,6 +86,20 @@ describe('MdDialog', () => {
8886
expect(afterCloseResult).toBe('Charmander');
8987
expect(overlayContainerElement.querySelector('md-dialog-container')).toBeNull();
9088
});
89+
90+
it('should close when clicking on the overlay backdrop', () => {
91+
let config = new MdDialogConfig();
92+
config.viewContainerRef = testViewContainerRef;
93+
94+
let dialogRef = dialog.open(PizzaMsg, config);
95+
96+
viewContainerFixture.detectChanges();
97+
98+
let backdrop = <HTMLElement> overlayContainerElement.querySelector('.md-overlay-backdrop');
99+
backdrop.click();
100+
101+
expect(overlayContainerElement.querySelector('md-dialog-container')).toBeFalsy();
102+
});
91103
});
92104

93105

src/lib/dialog/dialog.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ export class MdDialog {
8686
// to modify and close it.
8787
let dialogRef = <MdDialogRef<T>> new MdDialogRef(overlayRef);
8888

89+
// When the dialog backdrop is clicked, we want to close it.
90+
overlayRef.backdropClick().subscribe(() => dialogRef.close());
91+
8992
// We create an injector specifically for the component we're instantiating so that it can
9093
// inject the MdDialogRef. This allows a component loaded inside of a dialog to close itself
9194
// and, optionally, to return a value.
@@ -107,6 +110,7 @@ export class MdDialog {
107110
private _getOverlayState(dialogConfig: MdDialogConfig): OverlayState {
108111
let state = new OverlayState();
109112

113+
state.hasBackdrop = true;
110114
state.positionStrategy = this._overlay.position()
111115
.global()
112116
.centerHorizontally()

0 commit comments

Comments
 (0)