Skip to content

Commit 703932d

Browse files
committed
refactor(overlay): use component to render backdrop
Uses an Angular component to render the backdrop, instead of managing a DOM element manually. This has the advantage of being able to leverage the animations API to transition in/out, as well as not having to worry about the cases where the backdrop animation is disabled. These changes also enable the backdrop transition for the dialog (previously it would be removed immediately on close).
1 parent 9b07712 commit 703932d

File tree

9 files changed

+116
-97
lines changed

9 files changed

+116
-97
lines changed

src/cdk/overlay/_overlay.scss

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
6363
transition: opacity $backdrop-animation-duration $backdrop-animation-timing-function;
6464
opacity: 0;
6565

66-
&.cdk-overlay-backdrop-showing {
67-
opacity: 0.48;
66+
// Prevent the user from interacting while the backdrop is animating.
67+
&.ng-animating {
68+
pointer-events: none;
6869
}
6970
}
7071

src/cdk/overlay/backdrop.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {
10+
Component,
11+
ViewEncapsulation,
12+
ChangeDetectionStrategy,
13+
OnDestroy,
14+
Renderer2,
15+
ElementRef,
16+
} from '@angular/core';
17+
import {animate, AnimationEvent, state, style, transition, trigger} from '@angular/animations';
18+
import {Subject} from 'rxjs/Subject';
19+
20+
/**
21+
* Semi-transparent backdrop that will be rendered behind an overlay.
22+
* @docs-private
23+
*/
24+
@Component({
25+
template: '',
26+
host: {
27+
'class': 'cdk-overlay-backdrop',
28+
'[@state]': '_animationState',
29+
'(@state.done)': '_animationStream.next($event)',
30+
'(click)': '_clickStream.next()',
31+
},
32+
animations: [
33+
trigger('state', [
34+
state('void', style({opacity: '0'})),
35+
state('visible', style({opacity: '0.48'})),
36+
transition('* => *', animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)')),
37+
])
38+
],
39+
changeDetection: ChangeDetectionStrategy.OnPush,
40+
encapsulation: ViewEncapsulation.None,
41+
})
42+
export class MatBackdrop implements OnDestroy {
43+
_animationState = 'visible';
44+
_clickStream = new Subject<void>();
45+
_animationStream = new Subject<AnimationEvent>();
46+
47+
constructor(private _element: ElementRef, private _renderer: Renderer2) {}
48+
49+
_setClass(cssClass: string) {
50+
this._renderer.addClass(this._element.nativeElement, cssClass);
51+
}
52+
53+
ngOnDestroy() {
54+
this._clickStream.complete();
55+
}
56+
}

src/cdk/overlay/overlay-directives.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {Component, ViewChild} from '@angular/core';
22
import {By} from '@angular/platform-browser';
33
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
4+
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
45
import {Directionality} from '@angular/cdk/bidi';
56
import {dispatchKeyboardEvent} from '@angular/cdk/testing';
67
import {ESCAPE} from '@angular/cdk/keycodes';
@@ -17,7 +18,7 @@ describe('Overlay directives', () => {
1718

1819
beforeEach(() => {
1920
TestBed.configureTestingModule({
20-
imports: [OverlayModule],
21+
imports: [OverlayModule, NoopAnimationsModule],
2122
declarations: [ConnectedOverlayDirectiveTest, ConnectedOverlayPropertyInitOrder],
2223
providers: [
2324
{provide: OverlayContainer, useFactory: () => {

src/cdk/overlay/overlay-module.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from './overlay-directives';
2020
import {OverlayPositionBuilder} from './position/overlay-position-builder';
2121
import {ScrollStrategyOptions} from './scroll/scroll-strategy-options';
22+
import {MatBackdrop} from './backdrop';
2223

2324
export const OVERLAY_PROVIDERS: Provider[] = [
2425
Overlay,
@@ -30,8 +31,9 @@ export const OVERLAY_PROVIDERS: Provider[] = [
3031

3132
@NgModule({
3233
imports: [BidiModule, PortalModule, ScrollDispatchModule],
33-
exports: [ConnectedOverlayDirective, OverlayOrigin, ScrollDispatchModule],
34-
declarations: [ConnectedOverlayDirective, OverlayOrigin],
34+
exports: [ConnectedOverlayDirective, MatBackdrop, OverlayOrigin, ScrollDispatchModule],
35+
declarations: [ConnectedOverlayDirective, MatBackdrop, OverlayOrigin],
3536
providers: [OVERLAY_PROVIDERS, ScrollStrategyOptions],
37+
entryComponents: [MatBackdrop],
3638
})
3739
export class OverlayModule {}

src/cdk/overlay/overlay-ref.ts

Lines changed: 28 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,25 @@ import {PortalHost, Portal} from '@angular/cdk/portal';
1111
import {OverlayConfig} from './overlay-config';
1212
import {Observable} from 'rxjs/Observable';
1313
import {Subject} from 'rxjs/Subject';
14-
import {first} from 'rxjs/operator/first';
14+
import {MatBackdrop} from './backdrop';
15+
import {ComponentPortal} from '@angular/cdk/portal';
16+
import {first} from '@angular/cdk/rxjs';
17+
import {empty} from 'rxjs/observable/empty';
1518

1619

1720
/**
1821
* Reference to an overlay that has been created with the Overlay service.
1922
* Used to manipulate or dispose of said overlay.
2023
*/
2124
export class OverlayRef implements PortalHost {
22-
private _backdropElement: HTMLElement | null = null;
23-
private _backdropClick: Subject<any> = new Subject();
2425
private _attachments = new Subject<void>();
2526
private _detachments = new Subject<void>();
27+
private _backdropInstance: MatBackdrop | null;
2628

2729
constructor(
2830
private _portalHost: PortalHost,
2931
private _pane: HTMLElement,
32+
private _backdropHost: PortalHost | null,
3033
private _config: OverlayConfig,
3134
private _ngZone: NgZone) {
3235

@@ -46,7 +49,7 @@ export class OverlayRef implements PortalHost {
4649
* @returns The portal attachment result.
4750
*/
4851
attach(portal: Portal<any>): any {
49-
let attachResult = this._portalHost.attach(portal);
52+
const attachResult = this._portalHost.attach(portal);
5053

5154
if (this._config.positionStrategy) {
5255
this._config.positionStrategy.attach(this);
@@ -71,14 +74,15 @@ export class OverlayRef implements PortalHost {
7174
// Enable pointer events for the overlay pane element.
7275
this._togglePointerEvents(true);
7376

74-
if (this._config.hasBackdrop) {
75-
this._attachBackdrop();
77+
if (this._backdropHost) {
78+
this._backdropInstance = this._backdropHost.attach(new ComponentPortal(MatBackdrop)).instance;
79+
this._backdropInstance!._setClass(this._config.backdropClass!);
7680
}
7781

7882
if (this._config.panelClass) {
7983
// We can't do a spread here, because IE doesn't support setting multiple classes.
8084
if (Array.isArray(this._config.panelClass)) {
81-
this._config.panelClass.forEach(cls => this._pane.classList.add(cls));
85+
this._config.panelClass.forEach(cssClass => this._pane.classList.add(cssClass));
8286
} else {
8387
this._pane.classList.add(this._config.panelClass);
8488
}
@@ -95,7 +99,9 @@ export class OverlayRef implements PortalHost {
9599
* @returns The portal detachment result.
96100
*/
97101
detach(): any {
98-
this.detachBackdrop();
102+
if (this._backdropHost && this._backdropHost.hasAttached()) {
103+
this._backdropHost.detach();
104+
}
99105

100106
// When the overlay is detached, the pane element should disable pointer events.
101107
// This is necessary because otherwise the pane element will cover the page and disable
@@ -130,10 +136,9 @@ export class OverlayRef implements PortalHost {
130136
this._config.scrollStrategy.disable();
131137
}
132138

133-
this.detachBackdrop();
139+
this.disposeBackdrop();
134140
this._portalHost.dispose();
135141
this._attachments.complete();
136-
this._backdropClick.complete();
137142
this._detachments.next();
138143
this._detachments.complete();
139144
}
@@ -149,7 +154,7 @@ export class OverlayRef implements PortalHost {
149154
* Returns an observable that emits when the backdrop has been clicked.
150155
*/
151156
backdropClick(): Observable<void> {
152-
return this._backdropClick.asObservable();
157+
return this._backdropInstance ? this._backdropInstance._clickStream : empty<void>();
153158
}
154159

155160
/** Returns an observable that emits when the overlay has been attached. */
@@ -213,31 +218,6 @@ export class OverlayRef implements PortalHost {
213218
this._pane.style.pointerEvents = enablePointer ? 'auto' : 'none';
214219
}
215220

216-
/** Attaches a backdrop for this overlay. */
217-
private _attachBackdrop() {
218-
this._backdropElement = document.createElement('div');
219-
this._backdropElement.classList.add('cdk-overlay-backdrop');
220-
221-
if (this._config.backdropClass) {
222-
this._backdropElement.classList.add(this._config.backdropClass);
223-
}
224-
225-
// Insert the backdrop before the pane in the DOM order,
226-
// in order to handle stacked overlays properly.
227-
this._pane.parentElement!.insertBefore(this._backdropElement, this._pane);
228-
229-
// Forward backdrop clicks such that the consumer of the overlay can perform whatever
230-
// action desired when such a click occurs (usually closing the overlay).
231-
this._backdropElement.addEventListener('click', () => this._backdropClick.next(null));
232-
233-
// Add class to fade-in the backdrop after one frame.
234-
requestAnimationFrame(() => {
235-
if (this._backdropElement) {
236-
this._backdropElement.classList.add('cdk-overlay-backdrop-showing');
237-
}
238-
});
239-
}
240-
241221
/**
242222
* Updates the stacking order of the element, moving it to the top if necessary.
243223
* This is required in cases where one overlay was detached, while another one,
@@ -251,43 +231,19 @@ export class OverlayRef implements PortalHost {
251231
}
252232
}
253233

254-
/** Detaches the backdrop (if any) associated with the overlay. */
255-
detachBackdrop(): void {
256-
let backdropToDetach = this._backdropElement;
257-
258-
if (backdropToDetach) {
259-
let finishDetach = () => {
260-
// It may not be attached to anything in certain cases (e.g. unit tests).
261-
if (backdropToDetach && backdropToDetach.parentNode) {
262-
backdropToDetach.parentNode.removeChild(backdropToDetach);
263-
}
264-
265-
// It is possible that a new portal has been attached to this overlay since we started
266-
// removing the backdrop. If that is the case, only clear the backdrop reference if it
267-
// is still the same instance that we started to remove.
268-
if (this._backdropElement == backdropToDetach) {
269-
this._backdropElement = null;
270-
}
271-
};
272-
273-
backdropToDetach.classList.remove('cdk-overlay-backdrop-showing');
274-
275-
if (this._config.backdropClass) {
276-
backdropToDetach.classList.remove(this._config.backdropClass);
277-
}
278-
279-
backdropToDetach.addEventListener('transitionend', finishDetach);
234+
/** Animates out and disposes of the backdrop. */
235+
disposeBackdrop(): void {
236+
if (this._backdropHost) {
237+
if (this._backdropHost.hasAttached()) {
238+
this._backdropHost.detach();
280239

281-
// If the backdrop doesn't have a transition, the `transitionend` event won't fire.
282-
// In this case we make it unclickable and we try to remove it after a delay.
283-
backdropToDetach.style.pointerEvents = 'none';
284-
285-
// Run this outside the Angular zone because there's nothing that Angular cares about.
286-
// If it were to run inside the Angular zone, every test that used Overlay would have to be
287-
// either async or fakeAsync.
288-
this._ngZone.runOutsideAngular(() => {
289-
setTimeout(finishDetach, 500);
290-
});
240+
first.call(this._backdropInstance!._animationStream).subscribe(() => {
241+
this._backdropHost!.dispose();
242+
this._backdropHost = this._backdropInstance = null;
243+
});
244+
} else {
245+
this._backdropHost.dispose();
246+
}
291247
}
292248
}
293249
}

src/cdk/overlay/overlay.spec.ts

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {async, fakeAsync, tick, ComponentFixture, inject, TestBed} from '@angular/core/testing';
22
import {Component, NgModule, ViewChild, ViewContainerRef} from '@angular/core';
3+
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
34
import {
45
ComponentPortal,
56
PortalModule,
@@ -26,7 +27,7 @@ describe('Overlay', () => {
2627

2728
beforeEach(async(() => {
2829
TestBed.configureTestingModule({
29-
imports: [OverlayModule, PortalModule, OverlayTestModule],
30+
imports: [OverlayModule, PortalModule, OverlayTestModule, NoopAnimationsModule],
3031
providers: [{
3132
provide: OverlayContainer,
3233
useFactory: () => {
@@ -81,6 +82,7 @@ describe('Overlay', () => {
8182
.toBe('auto', 'Expected the overlay pane to enable pointerEvents when attached.');
8283

8384
overlayRef.detach();
85+
viewContainerFixture.detectChanges();
8486

8587
expect(paneElement.childNodes.length).toBe(0);
8688
expect(paneElement.style.pointerEvents)
@@ -181,6 +183,8 @@ describe('Overlay', () => {
181183
let overlayRef = overlay.create();
182184

183185
overlayRef.detachments().subscribe(() => {
186+
viewContainerFixture.detectChanges();
187+
184188
expect(overlayContainerElement.querySelector('pizza'))
185189
.toBeFalsy('Expected the overlay to have been detached.');
186190
});
@@ -336,7 +340,6 @@ describe('Overlay', () => {
336340
viewContainerFixture.detectChanges();
337341
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
338342
expect(backdrop).toBeTruthy();
339-
expect(backdrop.classList).not.toContain('cdk-overlay-backdrop-showing');
340343

341344
let backdropClickHandler = jasmine.createSpy('backdropClickHander');
342345
overlayRef.backdropClick().subscribe(backdropClickHandler);
@@ -379,27 +382,13 @@ describe('Overlay', () => {
379382
expect(backdrop.classList).toContain('cdk-overlay-transparent-backdrop');
380383
});
381384

382-
it('should disable the pointer events of a backdrop that is being removed', () => {
383-
let overlayRef = overlay.create(config);
384-
overlayRef.attach(componentPortal);
385-
386-
viewContainerFixture.detectChanges();
387-
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
388-
389-
expect(backdrop.style.pointerEvents).toBeFalsy();
390-
391-
overlayRef.detach();
392-
393-
expect(backdrop.style.pointerEvents).toBe('none');
394-
});
395-
396385
it('should insert the backdrop before the overlay pane in the DOM order', () => {
397386
let overlayRef = overlay.create(config);
398387
overlayRef.attach(componentPortal);
399388

400389
viewContainerFixture.detectChanges();
401390

402-
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop');
391+
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop')!.parentNode;
403392
let pane = overlayContainerElement.querySelector('.cdk-overlay-pane');
404393
let children = Array.prototype.slice.call(overlayContainerElement.children);
405394

src/cdk/overlay/overlay.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,12 @@ export class Overlay {
5252
* @returns Reference to the created overlay.
5353
*/
5454
create(config: OverlayConfig = defaultConfig): OverlayRef {
55+
const backdrop = config.hasBackdrop ? this._createBackdropElement() : null;
56+
const backdropHost = backdrop ? this._createPortalHost(backdrop) : null;
5557
const pane = this._createPaneElement();
5658
const portalHost = this._createPortalHost(pane);
57-
return new OverlayRef(portalHost, pane, config, this._ngZone);
59+
60+
return new OverlayRef(portalHost, pane, backdropHost, config, this._ngZone);
5861
}
5962

6063
/**
@@ -70,7 +73,7 @@ export class Overlay {
7073
* @returns Newly-created pane element
7174
*/
7275
private _createPaneElement(): HTMLElement {
73-
let pane = document.createElement('div');
76+
const pane = document.createElement('div');
7477

7578
pane.id = `cdk-overlay-${nextUniqueId++}`;
7679
pane.classList.add('cdk-overlay-pane');
@@ -79,6 +82,16 @@ export class Overlay {
7982
return pane;
8083
}
8184

85+
/**
86+
* Creates the DOM element that will wrap the backdrop and adds it to the overlay container.
87+
* @returns Newly-created backdrop host.
88+
*/
89+
private _createBackdropElement(): HTMLElement {
90+
const pane = document.createElement('div');
91+
this._overlayContainer.getContainerElement().appendChild(pane);
92+
return pane;
93+
}
94+
8295
/**
8396
* Create a DomPortalHost into which the overlay content can be loaded.
8497
* @param pane The DOM element to turn into a portal host.

0 commit comments

Comments
 (0)