Skip to content

Commit fe28d4b

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 6a82c65 commit fe28d4b

File tree

11 files changed

+139
-131
lines changed

11 files changed

+139
-131
lines changed

src/cdk/a11y/tsconfig-build.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"extends": "../tsconfig-build",
33
"files": [
4-
"public-api.ts"
4+
"public-api.ts",
5+
"../typings.d.ts"
56
],
67
"angularCompilerOptions": {
78
"annotateForClosureCompiler": true,

src/cdk/overlay/_overlay.scss

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,15 @@ $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
7979
transition: opacity $backdrop-animation-duration $backdrop-animation-timing-function;
8080
opacity: 0;
8181

82-
&.cdk-overlay-backdrop-showing {
83-
opacity: 1;
84-
85-
// In high contrast mode the rgba background will become solid
86-
// so we need to fall back to making it opaque using `opacity`.
87-
@include cdk-high-contrast {
88-
opacity: 0.6;
89-
}
82+
// In high contrast mode the rgba background will become solid
83+
// so we need to fall back to making it opaque using `opacity`.
84+
@include cdk-high-contrast {
85+
opacity: 0.6;
86+
}
87+
88+
// Prevent the user from interacting while the backdrop is animating.
89+
&.ng-animating {
90+
pointer-events: none;
9091
}
9192
}
9293

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 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';
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($event)',
31+
},
32+
animations: [
33+
trigger('state', [
34+
state('void', style({opacity: '0'})),
35+
state('visible', style({opacity: '1'})),
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 CdkOverlayBackdrop implements OnDestroy {
43+
_animationState = 'visible';
44+
_clickStream = new Subject<MouseEvent>();
45+
_animationStream = new Subject<AnimationEvent>();
46+
47+
constructor(public _element: ElementRef) {}
48+
49+
_setClass(cssClass: string) {
50+
this._element.nativeElement.classList.add(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, 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';
@@ -21,7 +22,7 @@ describe('Overlay directives', () => {
2122

2223
beforeEach(() => {
2324
TestBed.configureTestingModule({
24-
imports: [OverlayModule],
25+
imports: [OverlayModule, NoopAnimationsModule],
2526
declarations: [ConnectedOverlayDirectiveTest, ConnectedOverlayPropertyInitOrder],
2627
providers: [{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}],
2728
});

src/cdk/overlay/overlay-module.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ import {
1919
CdkOverlayOrigin,
2020
} from './overlay-directives';
2121
import {OverlayPositionBuilder} from './position/overlay-position-builder';
22+
import {CdkOverlayBackdrop} from './backdrop';
2223

2324

2425
@NgModule({
2526
imports: [BidiModule, PortalModule, ScrollDispatchModule],
26-
exports: [CdkConnectedOverlay, CdkOverlayOrigin, ScrollDispatchModule],
27-
declarations: [CdkConnectedOverlay, CdkOverlayOrigin],
27+
exports: [CdkConnectedOverlay, CdkOverlayOrigin, CdkOverlayBackdrop, ScrollDispatchModule],
28+
declarations: [CdkConnectedOverlay, CdkOverlayOrigin, CdkOverlayBackdrop],
2829
providers: [Overlay],
30+
entryComponents: [CdkOverlayBackdrop],
2931
})
3032
export class OverlayModule {}
3133

src/cdk/overlay/overlay-ref.ts

Lines changed: 37 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
import {Direction} from '@angular/cdk/bidi';
1010
import {ComponentPortal, Portal, PortalOutlet, TemplatePortal} from '@angular/cdk/portal';
1111
import {ComponentRef, EmbeddedViewRef, NgZone} from '@angular/core';
12-
import {Observable, Subject} from 'rxjs';
12+
import {Observable, Subject, empty} from 'rxjs';
1313
import {take} from 'rxjs/operators';
1414
import {OverlayKeyboardDispatcher} from './keyboard/overlay-keyboard-dispatcher';
1515
import {OverlayConfig} from './overlay-config';
1616
import {coerceCssPixelValue} from '@angular/cdk/coercion';
17+
import {CdkOverlayBackdrop} from './backdrop';
1718

1819

1920
/** An object where all of its properties cannot be written. */
@@ -26,10 +27,10 @@ export type ImmutableObject<T> = {
2627
* Used to manipulate or dispose of said overlay.
2728
*/
2829
export class OverlayRef implements PortalOutlet {
29-
private _backdropElement: HTMLElement | null = null;
30-
private _backdropClick: Subject<MouseEvent> = new Subject();
30+
private _backdropClick = new Subject<MouseEvent>();
3131
private _attachments = new Subject<void>();
3232
private _detachments = new Subject<void>();
33+
private _backdropInstance: CdkOverlayBackdrop | null;
3334

3435
/** Stream of keydown events dispatched to this overlay. */
3536
_keydownEvents = new Subject<KeyboardEvent>();
@@ -38,10 +39,10 @@ export class OverlayRef implements PortalOutlet {
3839
private _portalOutlet: PortalOutlet,
3940
private _host: HTMLElement,
4041
private _pane: HTMLElement,
42+
private _backdropHost: PortalOutlet | null,
4143
private _config: ImmutableObject<OverlayConfig>,
4244
private _ngZone: NgZone,
43-
private _keyboardDispatcher: OverlayKeyboardDispatcher,
44-
private _document: Document) {
45+
private _keyboardDispatcher: OverlayKeyboardDispatcher) {
4546

4647
if (_config.scrollStrategy) {
4748
_config.scrollStrategy.attach(this);
@@ -55,7 +56,7 @@ export class OverlayRef implements PortalOutlet {
5556

5657
/** The overlay's backdrop HTML element. */
5758
get backdropElement(): HTMLElement | null {
58-
return this._backdropElement;
59+
return this._backdropInstance ? this._backdropInstance._element.nativeElement : null;
5960
}
6061

6162
/**
@@ -79,7 +80,7 @@ export class OverlayRef implements PortalOutlet {
7980
* @returns The portal attachment result.
8081
*/
8182
attach(portal: Portal<any>): any {
82-
let attachResult = this._portalOutlet.attach(portal);
83+
const attachResult = this._portalOutlet.attach(portal);
8384

8485
if (this._config.positionStrategy) {
8586
this._config.positionStrategy.attach(this);
@@ -110,8 +111,10 @@ export class OverlayRef implements PortalOutlet {
110111
// Enable pointer events for the overlay pane element.
111112
this._togglePointerEvents(true);
112113

113-
if (this._config.hasBackdrop) {
114-
this._attachBackdrop();
114+
if (this._backdropHost) {
115+
this._backdropInstance =
116+
this._backdropHost.attach(new ComponentPortal(CdkOverlayBackdrop)).instance;
117+
this._backdropInstance!._setClass(this._config.backdropClass!);
115118
}
116119

117120
if (this._config.panelClass) {
@@ -141,7 +144,9 @@ export class OverlayRef implements PortalOutlet {
141144
return;
142145
}
143146

144-
this.detachBackdrop();
147+
if (this._backdropHost && this._backdropHost.hasAttached()) {
148+
this._backdropHost.detach();
149+
}
145150

146151
// When the overlay is detached, the pane element should disable pointer events.
147152
// This is necessary because otherwise the pane element will cover the page and disable
@@ -179,7 +184,7 @@ export class OverlayRef implements PortalOutlet {
179184
this._config.scrollStrategy.disable();
180185
}
181186

182-
this.detachBackdrop();
187+
this.disposeBackdrop();
183188
this._keyboardDispatcher.remove(this);
184189
this._portalOutlet.dispose();
185190
this._attachments.complete();
@@ -205,7 +210,7 @@ export class OverlayRef implements PortalOutlet {
205210

206211
/** Gets an observable that emits when the backdrop has been clicked. */
207212
backdropClick(): Observable<MouseEvent> {
208-
return this._backdropClick.asObservable();
213+
return this._backdropInstance ? this._backdropInstance._clickStream : empty();
209214
}
210215

211216
/** Gets an observable that emits when the overlay has been attached. */
@@ -284,40 +289,6 @@ export class OverlayRef implements PortalOutlet {
284289
this._pane.style.pointerEvents = enablePointer ? 'auto' : 'none';
285290
}
286291

287-
/** Attaches a backdrop for this overlay. */
288-
private _attachBackdrop() {
289-
const showingClass = 'cdk-overlay-backdrop-showing';
290-
291-
this._backdropElement = this._document.createElement('div');
292-
this._backdropElement.classList.add('cdk-overlay-backdrop');
293-
294-
if (this._config.backdropClass) {
295-
this._backdropElement.classList.add(this._config.backdropClass);
296-
}
297-
298-
// Insert the backdrop before the pane in the DOM order,
299-
// in order to handle stacked overlays properly.
300-
this._host.parentElement!.insertBefore(this._backdropElement, this._host);
301-
302-
// Forward backdrop clicks such that the consumer of the overlay can perform whatever
303-
// action desired when such a click occurs (usually closing the overlay).
304-
this._backdropElement.addEventListener('click',
305-
(event: MouseEvent) => this._backdropClick.next(event));
306-
307-
// Add class to fade-in the backdrop after one frame.
308-
if (typeof requestAnimationFrame !== 'undefined') {
309-
this._ngZone.runOutsideAngular(() => {
310-
requestAnimationFrame(() => {
311-
if (this._backdropElement) {
312-
this._backdropElement.classList.add(showingClass);
313-
}
314-
});
315-
});
316-
} else {
317-
this._backdropElement.classList.add(showingClass);
318-
}
319-
}
320-
321292
/**
322293
* Updates the stacking order of the element, moving it to the top if necessary.
323294
* This is required in cases where one overlay was detached, while another one,
@@ -331,43 +302,30 @@ export class OverlayRef implements PortalOutlet {
331302
}
332303
}
333304

334-
/** Detaches the backdrop (if any) associated with the overlay. */
335-
detachBackdrop(): void {
336-
let backdropToDetach = this._backdropElement;
337-
338-
if (backdropToDetach) {
339-
let finishDetach = () => {
340-
// It may not be attached to anything in certain cases (e.g. unit tests).
341-
if (backdropToDetach && backdropToDetach.parentNode) {
342-
backdropToDetach.parentNode.removeChild(backdropToDetach);
343-
}
344-
345-
// It is possible that a new portal has been attached to this overlay since we started
346-
// removing the backdrop. If that is the case, only clear the backdrop reference if it
347-
// is still the same instance that we started to remove.
348-
if (this._backdropElement == backdropToDetach) {
349-
this._backdropElement = null;
350-
}
351-
};
352-
353-
backdropToDetach.classList.remove('cdk-overlay-backdrop-showing');
305+
/** Animates out and disposes of the backdrop. */
306+
disposeBackdrop(): void {
307+
if (this._backdropHost) {
308+
if (this._backdropHost.hasAttached()) {
309+
this._backdropHost.detach();
354310

355-
if (this._config.backdropClass) {
356-
backdropToDetach.classList.remove(this._config.backdropClass);
311+
this._backdropInstance!._animationStream.pipe(take(1)).subscribe(() => {
312+
this._backdropHost!.dispose();
313+
this._backdropHost = this._backdropInstance = null;
314+
});
315+
} else {
316+
this._backdropHost.dispose();
357317
}
358-
359-
backdropToDetach.addEventListener('transitionend', finishDetach);
360-
361-
// If the backdrop doesn't have a transition, the `transitionend` event won't fire.
362-
// In this case we make it unclickable and we try to remove it after a delay.
363-
backdropToDetach.style.pointerEvents = 'none';
364-
365-
// Run this outside the Angular zone because there's nothing that Angular cares about.
366-
// If it were to run inside the Angular zone, every test that used Overlay would have to be
367-
// either async or fakeAsync.
368-
this._ngZone.runOutsideAngular(() => setTimeout(finishDetach, 500));
369318
}
370319
}
320+
321+
/**
322+
* Detaches the backdrop (if any) associated with the overlay.
323+
* @deprecated Use `disposeBackdrop` instead.
324+
* @deletion-target 7.0.0
325+
*/
326+
detachBackdrop(): void {
327+
this.disposeBackdrop();
328+
}
371329
}
372330

373331

0 commit comments

Comments
 (0)