Skip to content

Commit 3ac4e7a

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 5210b3e commit 3ac4e7a

File tree

9 files changed

+128
-97
lines changed

9 files changed

+128
-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: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC 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+
ElementRef,
15+
} from '@angular/core';
16+
import {animate, AnimationEvent, state, style, transition, trigger} from '@angular/animations';
17+
import {Subject} from 'rxjs/Subject';
18+
19+
/**
20+
* Semi-transparent backdrop that will be rendered behind an overlay.
21+
* @docs-private
22+
*/
23+
@Component({
24+
moduleId: module.id,
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+
preserveWhitespaces: false,
42+
})
43+
export class MatBackdrop implements OnDestroy {
44+
_animationState = 'visible';
45+
_clickStream = new Subject<void>();
46+
_animationStream = new Subject<AnimationEvent>();
47+
48+
constructor(private _element: ElementRef) {}
49+
50+
_setClass(cssClass: string) {
51+
this._element.nativeElement.classList.add(cssClass);
52+
}
53+
54+
ngOnDestroy() {
55+
this._clickStream.complete();
56+
}
57+
}

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, inject} 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';
@@ -18,7 +19,7 @@ describe('Overlay directives', () => {
1819

1920
beforeEach(() => {
2021
TestBed.configureTestingModule({
21-
imports: [OverlayModule],
22+
imports: [OverlayModule, NoopAnimationsModule],
2223
declarations: [ConnectedOverlayDirectiveTest, ConnectedOverlayPropertyInitOrder],
2324
providers: [
2425
{provide: Directionality, useFactory: () => {

src/cdk/overlay/overlay-module.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import {OverlayPositionBuilder} from './position/overlay-position-builder';
2121
import {OVERLAY_KEYBOARD_DISPATCHER_PROVIDER} from './keyboard/overlay-keyboard-dispatcher';
2222
import {ScrollStrategyOptions} from './scroll/scroll-strategy-options';
23+
import {MatBackdrop} from './backdrop';
2324

2425
export const OVERLAY_PROVIDERS: Provider[] = [
2526
Overlay,
@@ -32,8 +33,9 @@ export const OVERLAY_PROVIDERS: Provider[] = [
3233

3334
@NgModule({
3435
imports: [BidiModule, PortalModule, ScrollDispatchModule],
35-
exports: [CdkConnectedOverlay, CdkOverlayOrigin, ScrollDispatchModule],
36-
declarations: [CdkConnectedOverlay, CdkOverlayOrigin],
36+
exports: [CdkConnectedOverlay, CdkOverlayOrigin, MatBackdrop, ScrollDispatchModule],
37+
declarations: [CdkConnectedOverlay, CdkOverlayOrigin, MatBackdrop],
3738
providers: [OVERLAY_PROVIDERS, ScrollStrategyOptions],
39+
entryComponents: [MatBackdrop],
3840
})
3941
export class OverlayModule {}

src/cdk/overlay/overlay-ref.ts

Lines changed: 34 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {take} from 'rxjs/operators/take';
1414
import {Subject} from 'rxjs/Subject';
1515
import {OverlayKeyboardDispatcher} from './keyboard/overlay-keyboard-dispatcher';
1616
import {OverlayConfig} from './overlay-config';
17+
import {MatBackdrop} from './backdrop';
18+
import {empty} from 'rxjs/observable/empty';
1719

1820

1921
/** An object where all of its properties cannot be written. */
@@ -26,17 +28,18 @@ export type ImmutableObject<T> = {
2628
* Used to manipulate or dispose of said overlay.
2729
*/
2830
export class OverlayRef implements PortalOutlet {
29-
private _backdropElement: HTMLElement | null = null;
3031
private _backdropClick: Subject<any> = new Subject();
3132
private _attachments = new Subject<void>();
3233
private _detachments = new Subject<void>();
34+
private _backdropInstance: MatBackdrop | null;
3335

3436
/** Stream of keydown events dispatched to this overlay. */
3537
_keydownEvents = new Subject<KeyboardEvent>();
3638

3739
constructor(
3840
private _portalOutlet: PortalOutlet,
3941
private _pane: HTMLElement,
42+
private _backdropHost: PortalOutlet | null,
4043
private _config: ImmutableObject<OverlayConfig>,
4144
private _ngZone: NgZone,
4245
private _keyboardDispatcher: OverlayKeyboardDispatcher) {
@@ -63,7 +66,7 @@ export class OverlayRef implements PortalOutlet {
6366
* @returns The portal attachment result.
6467
*/
6568
attach(portal: Portal<any>): any {
66-
let attachResult = this._portalOutlet.attach(portal);
69+
const attachResult = this._portalOutlet.attach(portal);
6770

6871
if (this._config.positionStrategy) {
6972
this._config.positionStrategy.attach(this);
@@ -88,14 +91,15 @@ export class OverlayRef implements PortalOutlet {
8891
// Enable pointer events for the overlay pane element.
8992
this._togglePointerEvents(true);
9093

91-
if (this._config.hasBackdrop) {
92-
this._attachBackdrop();
94+
if (this._backdropHost) {
95+
this._backdropInstance = this._backdropHost.attach(new ComponentPortal(MatBackdrop)).instance;
96+
this._backdropInstance!._setClass(this._config.backdropClass!);
9397
}
9498

9599
if (this._config.panelClass) {
96100
// We can't do a spread here, because IE doesn't support setting multiple classes.
97101
if (Array.isArray(this._config.panelClass)) {
98-
this._config.panelClass.forEach(cls => this._pane.classList.add(cls));
102+
this._config.panelClass.forEach(cssClass => this._pane.classList.add(cssClass));
99103
} else {
100104
this._pane.classList.add(this._config.panelClass);
101105
}
@@ -119,7 +123,9 @@ export class OverlayRef implements PortalOutlet {
119123
return;
120124
}
121125

122-
this.detachBackdrop();
126+
if (this._backdropHost && this._backdropHost.hasAttached()) {
127+
this._backdropHost.detach();
128+
}
123129

124130
// When the overlay is detached, the pane element should disable pointer events.
125131
// This is necessary because otherwise the pane element will cover the page and disable
@@ -157,7 +163,7 @@ export class OverlayRef implements PortalOutlet {
157163
this._config.scrollStrategy.disable();
158164
}
159165

160-
this.detachBackdrop();
166+
this.disposeBackdrop();
161167
this._keyboardDispatcher.remove(this);
162168
this._portalOutlet.dispose();
163169
this._attachments.complete();
@@ -178,7 +184,7 @@ export class OverlayRef implements PortalOutlet {
178184

179185
/** Gets an observable that emits when the backdrop has been clicked. */
180186
backdropClick(): Observable<void> {
181-
return this._backdropClick.asObservable();
187+
return this._backdropInstance ? this._backdropInstance._clickStream : empty<void>();
182188
}
183189

184190
/** Gets an observable that emits when the overlay has been attached. */
@@ -257,33 +263,6 @@ export class OverlayRef implements PortalOutlet {
257263
this._pane.style.pointerEvents = enablePointer ? 'auto' : 'none';
258264
}
259265

260-
/** Attaches a backdrop for this overlay. */
261-
private _attachBackdrop() {
262-
this._backdropElement = document.createElement('div');
263-
this._backdropElement.classList.add('cdk-overlay-backdrop');
264-
265-
if (this._config.backdropClass) {
266-
this._backdropElement.classList.add(this._config.backdropClass);
267-
}
268-
269-
// Insert the backdrop before the pane in the DOM order,
270-
// in order to handle stacked overlays properly.
271-
this._pane.parentElement!.insertBefore(this._backdropElement, this._pane);
272-
273-
// Forward backdrop clicks such that the consumer of the overlay can perform whatever
274-
// action desired when such a click occurs (usually closing the overlay).
275-
this._backdropElement.addEventListener('click', () => this._backdropClick.next(null));
276-
277-
// Add class to fade-in the backdrop after one frame.
278-
this._ngZone.runOutsideAngular(() => {
279-
requestAnimationFrame(() => {
280-
if (this._backdropElement) {
281-
this._backdropElement.classList.add('cdk-overlay-backdrop-showing');
282-
}
283-
});
284-
});
285-
}
286-
287266
/**
288267
* Updates the stacking order of the element, moving it to the top if necessary.
289268
* This is required in cases where one overlay was detached, while another one,
@@ -297,45 +276,29 @@ export class OverlayRef implements PortalOutlet {
297276
}
298277
}
299278

300-
/** Detaches the backdrop (if any) associated with the overlay. */
301-
detachBackdrop(): void {
302-
let backdropToDetach = this._backdropElement;
303-
304-
if (backdropToDetach) {
305-
let finishDetach = () => {
306-
// It may not be attached to anything in certain cases (e.g. unit tests).
307-
if (backdropToDetach && backdropToDetach.parentNode) {
308-
backdropToDetach.parentNode.removeChild(backdropToDetach);
309-
}
310-
311-
// It is possible that a new portal has been attached to this overlay since we started
312-
// removing the backdrop. If that is the case, only clear the backdrop reference if it
313-
// is still the same instance that we started to remove.
314-
if (this._backdropElement == backdropToDetach) {
315-
this._backdropElement = null;
316-
}
317-
};
318-
319-
backdropToDetach.classList.remove('cdk-overlay-backdrop-showing');
320-
321-
if (this._config.backdropClass) {
322-
backdropToDetach.classList.remove(this._config.backdropClass);
323-
}
324-
325-
backdropToDetach.addEventListener('transitionend', finishDetach);
326-
327-
// If the backdrop doesn't have a transition, the `transitionend` event won't fire.
328-
// In this case we make it unclickable and we try to remove it after a delay.
329-
backdropToDetach.style.pointerEvents = 'none';
279+
/** Animates out and disposes of the backdrop. */
280+
disposeBackdrop(): void {
281+
if (this._backdropHost) {
282+
if (this._backdropHost.hasAttached()) {
283+
this._backdropHost.detach();
330284

331-
// Run this outside the Angular zone because there's nothing that Angular cares about.
332-
// If it were to run inside the Angular zone, every test that used Overlay would have to be
333-
// either async or fakeAsync.
334-
this._ngZone.runOutsideAngular(() => {
335-
setTimeout(finishDetach, 500);
336-
});
285+
this._backdropInstance!._animationStream.pipe(take(1)).subscribe(() => {
286+
this._backdropHost!.dispose();
287+
this._backdropHost = this._backdropInstance = null;
288+
});
289+
} else {
290+
this._backdropHost.dispose();
291+
}
337292
}
338293
}
294+
295+
/**
296+
* Detaches the backdrop (if any) associated with the overlay.
297+
* @deprecated Use `disposeBackdrop` instead.
298+
*/
299+
detachBackdrop(): void {
300+
this.disposeBackdrop();
301+
}
339302
}
340303

341304
function formatCssUnit(value: number | string) {

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,
@@ -27,7 +28,7 @@ describe('Overlay', () => {
2728

2829
beforeEach(async(() => {
2930
TestBed.configureTestingModule({
30-
imports: [OverlayModule, PortalModule, OverlayTestModule]
31+
imports: [OverlayModule, PortalModule, OverlayTestModule, NoopAnimationsModule]
3132
}).compileComponents();
3233
}));
3334

@@ -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)
@@ -201,6 +203,8 @@ describe('Overlay', () => {
201203
let overlayRef = overlay.create();
202204

203205
overlayRef.detachments().subscribe(() => {
206+
viewContainerFixture.detectChanges();
207+
204208
expect(overlayContainerElement.querySelector('pizza'))
205209
.toBeFalsy('Expected the overlay to have been detached.');
206210
});
@@ -356,7 +360,6 @@ describe('Overlay', () => {
356360
viewContainerFixture.detectChanges();
357361
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
358362
expect(backdrop).toBeTruthy();
359-
expect(backdrop.classList).not.toContain('cdk-overlay-backdrop-showing');
360363

361364
let backdropClickHandler = jasmine.createSpy('backdropClickHander');
362365
overlayRef.backdropClick().subscribe(backdropClickHandler);
@@ -399,27 +402,13 @@ describe('Overlay', () => {
399402
expect(backdrop.classList).toContain('cdk-overlay-transparent-backdrop');
400403
});
401404

402-
it('should disable the pointer events of a backdrop that is being removed', () => {
403-
let overlayRef = overlay.create(config);
404-
overlayRef.attach(componentPortal);
405-
406-
viewContainerFixture.detectChanges();
407-
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
408-
409-
expect(backdrop.style.pointerEvents).toBeFalsy();
410-
411-
overlayRef.detach();
412-
413-
expect(backdrop.style.pointerEvents).toBe('none');
414-
});
415-
416405
it('should insert the backdrop before the overlay pane in the DOM order', () => {
417406
let overlayRef = overlay.create(config);
418407
overlayRef.attach(componentPortal);
419408

420409
viewContainerFixture.detectChanges();
421410

422-
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop');
411+
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop')!.parentNode;
423412
let pane = overlayContainerElement.querySelector('.cdk-overlay-pane');
424413
let children = Array.prototype.slice.call(overlayContainerElement.children);
425414

src/cdk/overlay/overlay.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,13 @@ export class Overlay {
5959
* @returns Reference to the created overlay.
6060
*/
6161
create(config: OverlayConfig = defaultConfig): OverlayRef {
62+
const backdrop = config.hasBackdrop ? this._createBackdropElement() : null;
63+
const backdropHost = backdrop ? this._createPortalOutlet(backdrop) : null;
6264
const pane = this._createPaneElement();
6365
const portalOutlet = this._createPortalOutlet(pane);
64-
return new OverlayRef(portalOutlet, pane, config, this._ngZone, this._keyboardDispatcher);
66+
67+
return new OverlayRef(portalOutlet, pane, backdropHost,
68+
config, this._ngZone, this._keyboardDispatcher);
6569
}
6670

6771
/**
@@ -91,6 +95,19 @@ export class Overlay {
9195
* Create a DomPortalOutlet into which the overlay content can be loaded.
9296
* @param pane The DOM element to turn into a portal outlet.
9397
* @returns A portal outlet for the given DOM element.
98+
* Creates the DOM element that will wrap the backdrop and adds it to the overlay container.
99+
* @returns Newly-created backdrop host.
100+
*/
101+
private _createBackdropElement(): HTMLElement {
102+
const pane = document.createElement('div');
103+
this._overlayContainer.getContainerElement().appendChild(pane);
104+
return pane;
105+
}
106+
107+
/**
108+
* Create a DomPortalHost into which the overlay content can be loaded.
109+
* @param pane The DOM element to turn into a portal host.
110+
* @returns A portal host for the given DOM element.
94111
*/
95112
private _createPortalOutlet(pane: HTMLElement): DomPortalOutlet {
96113
return new DomPortalOutlet(pane, this._componentFactoryResolver, this._appRef, this._injector);

0 commit comments

Comments
 (0)