Skip to content

Commit f2c1fa0

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 dbae360 commit f2c1fa0

File tree

9 files changed

+121
-109
lines changed

9 files changed

+121
-109
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 MdBackdrop 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-ref.ts

Lines changed: 30 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,30 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {NgZone} from '@angular/core';
109
import {PortalHost, Portal} from '@angular/cdk/portal';
1110
import {OverlayConfig} from './overlay-config';
1211
import {Observable} from 'rxjs/Observable';
1312
import {Subject} from 'rxjs/Subject';
13+
import {MdBackdrop} from './backdrop';
14+
import {ComponentPortal} from '@angular/cdk/portal';
15+
import {first} from '@angular/cdk/rxjs';
16+
import {empty} from 'rxjs/observable/empty';
1417

1518

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

2628
constructor(
2729
private _portalHost: PortalHost,
2830
private _pane: HTMLElement,
29-
private _state: OverlayConfig,
30-
private _ngZone: NgZone) {
31+
private _backdropHost: PortalHost | null,
32+
private _state: OverlayConfig) {
3133

3234
if (_state.scrollStrategy) {
3335
_state.scrollStrategy.attach(this);
@@ -45,7 +47,7 @@ export class OverlayRef implements PortalHost {
4547
* @returns The portal attachment result.
4648
*/
4749
attach(portal: Portal<any>): any {
48-
let attachResult = this._portalHost.attach(portal);
50+
const attachResult = this._portalHost.attach(portal);
4951

5052
if (this._state.positionStrategy) {
5153
this._state.positionStrategy.attach(this);
@@ -64,14 +66,15 @@ export class OverlayRef implements PortalHost {
6466
// Enable pointer events for the overlay pane element.
6567
this._togglePointerEvents(true);
6668

67-
if (this._state.hasBackdrop) {
68-
this._attachBackdrop();
69+
if (this._backdropHost) {
70+
this._backdropInstance = this._backdropHost.attach(new ComponentPortal(MdBackdrop)).instance;
71+
this._backdropInstance!._setClass(this._state.backdropClass!);
6972
}
7073

7174
if (this._state.panelClass) {
7275
// We can't do a spread here, because IE doesn't support setting multiple classes.
7376
if (Array.isArray(this._state.panelClass)) {
74-
this._state.panelClass.forEach(cls => this._pane.classList.add(cls));
77+
this._state.panelClass.forEach(cssClass => this._pane.classList.add(cssClass));
7578
} else {
7679
this._pane.classList.add(this._state.panelClass);
7780
}
@@ -88,7 +91,9 @@ export class OverlayRef implements PortalHost {
8891
* @returns Resolves when the overlay has been detached.
8992
*/
9093
detach(): Promise<any> {
91-
this.detachBackdrop();
94+
if (this._backdropHost && this._backdropHost.hasAttached()) {
95+
this._backdropHost.detach();
96+
}
9297

9398
// When the overlay is detached, the pane element should disable pointer events.
9499
// This is necessary because otherwise the pane element will cover the page and disable
@@ -99,7 +104,7 @@ export class OverlayRef implements PortalHost {
99104
this._state.scrollStrategy.disable();
100105
}
101106

102-
let detachmentResult = this._portalHost.detach();
107+
const detachmentResult = this._portalHost.detach();
103108

104109
// Only emit after everything is detached.
105110
this._detachments.next();
@@ -119,10 +124,9 @@ export class OverlayRef implements PortalHost {
119124
this._state.scrollStrategy.disable();
120125
}
121126

122-
this.detachBackdrop();
127+
this.disposeBackdrop();
123128
this._portalHost.dispose();
124129
this._attachments.complete();
125-
this._backdropClick.complete();
126130
this._detachments.next();
127131
this._detachments.complete();
128132
}
@@ -138,7 +142,7 @@ export class OverlayRef implements PortalHost {
138142
* Returns an observable that emits when the backdrop has been clicked.
139143
*/
140144
backdropClick(): Observable<void> {
141-
return this._backdropClick.asObservable();
145+
return this._backdropInstance ? this._backdropInstance._clickStream : empty<void>();
142146
}
143147

144148
/** Returns an observable that emits when the overlay has been attached. */
@@ -202,31 +206,6 @@ export class OverlayRef implements PortalHost {
202206
this._pane.style.pointerEvents = enablePointer ? 'auto' : 'none';
203207
}
204208

205-
/** Attaches a backdrop for this overlay. */
206-
private _attachBackdrop() {
207-
this._backdropElement = document.createElement('div');
208-
this._backdropElement.classList.add('cdk-overlay-backdrop');
209-
210-
if (this._state.backdropClass) {
211-
this._backdropElement.classList.add(this._state.backdropClass);
212-
}
213-
214-
// Insert the backdrop before the pane in the DOM order,
215-
// in order to handle stacked overlays properly.
216-
this._pane.parentElement!.insertBefore(this._backdropElement, this._pane);
217-
218-
// Forward backdrop clicks such that the consumer of the overlay can perform whatever
219-
// action desired when such a click occurs (usually closing the overlay).
220-
this._backdropElement.addEventListener('click', () => this._backdropClick.next(null));
221-
222-
// Add class to fade-in the backdrop after one frame.
223-
requestAnimationFrame(() => {
224-
if (this._backdropElement) {
225-
this._backdropElement.classList.add('cdk-overlay-backdrop-showing');
226-
}
227-
});
228-
}
229-
230209
/**
231210
* Updates the stacking order of the element, moving it to the top if necessary.
232211
* This is required in cases where one overlay was detached, while another one,
@@ -240,43 +219,19 @@ export class OverlayRef implements PortalHost {
240219
}
241220
}
242221

243-
/** Detaches the backdrop (if any) associated with the overlay. */
244-
detachBackdrop(): void {
245-
let backdropToDetach = this._backdropElement;
246-
247-
if (backdropToDetach) {
248-
let finishDetach = () => {
249-
// It may not be attached to anything in certain cases (e.g. unit tests).
250-
if (backdropToDetach && backdropToDetach.parentNode) {
251-
backdropToDetach.parentNode.removeChild(backdropToDetach);
252-
}
253-
254-
// It is possible that a new portal has been attached to this overlay since we started
255-
// removing the backdrop. If that is the case, only clear the backdrop reference if it
256-
// is still the same instance that we started to remove.
257-
if (this._backdropElement == backdropToDetach) {
258-
this._backdropElement = null;
259-
}
260-
};
261-
262-
backdropToDetach.classList.remove('cdk-overlay-backdrop-showing');
263-
264-
if (this._state.backdropClass) {
265-
backdropToDetach.classList.remove(this._state.backdropClass);
266-
}
267-
268-
backdropToDetach.addEventListener('transitionend', finishDetach);
222+
/** Animates out and disposes of the backdrop. */
223+
disposeBackdrop(): void {
224+
if (this._backdropHost) {
225+
if (this._backdropHost.hasAttached()) {
226+
this._backdropHost.detach();
269227

270-
// If the backdrop doesn't have a transition, the `transitionend` event won't fire.
271-
// In this case we make it unclickable and we try to remove it after a delay.
272-
backdropToDetach.style.pointerEvents = 'none';
273-
274-
// Run this outside the Angular zone because there's nothing that Angular cares about.
275-
// If it were to run inside the Angular zone, every test that used Overlay would have to be
276-
// either async or fakeAsync.
277-
this._ngZone.runOutsideAngular(() => {
278-
setTimeout(finishDetach, 500);
279-
});
228+
first.call(this._backdropInstance!._animationStream).subscribe(() => {
229+
this._backdropHost!.dispose();
230+
this._backdropHost = this._backdropInstance = null;
231+
});
232+
} else {
233+
this._backdropHost.dispose();
234+
}
280235
}
281236
}
282237
}

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, 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
});
@@ -334,7 +338,6 @@ describe('Overlay', () => {
334338
viewContainerFixture.detectChanges();
335339
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement;
336340
expect(backdrop).toBeTruthy();
337-
expect(backdrop.classList).not.toContain('cdk-overlay-backdrop-showing');
338341

339342
let backdropClickHandler = jasmine.createSpy('backdropClickHander');
340343
overlayRef.backdropClick().subscribe(backdropClickHandler);
@@ -377,27 +380,13 @@ describe('Overlay', () => {
377380
expect(backdrop.classList).toContain('cdk-overlay-transparent-backdrop');
378381
});
379382

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

398387
viewContainerFixture.detectChanges();
399388

400-
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop');
389+
let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop')!.parentNode;
401390
let pane = overlayContainerElement.querySelector('.cdk-overlay-pane');
402391
let children = Array.prototype.slice.call(overlayContainerElement.children);
403392

0 commit comments

Comments
 (0)