Skip to content

Commit a4e7e80

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 7857b92 commit a4e7e80

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
@@ -78,14 +78,15 @@ $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
7878
transition: opacity $backdrop-animation-duration $backdrop-animation-timing-function;
7979
opacity: 0;
8080

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

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,10 +9,11 @@
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';
16+
import {CdkOverlayBackdrop} from './backdrop';
1617

1718

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

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

4546
if (_config.scrollStrategy) {
4647
_config.scrollStrategy.attach(this);
@@ -54,7 +55,7 @@ export class OverlayRef implements PortalOutlet {
5455

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

6061
/**
@@ -78,7 +79,7 @@ export class OverlayRef implements PortalOutlet {
7879
* @returns The portal attachment result.
7980
*/
8081
attach(portal: Portal<any>): any {
81-
let attachResult = this._portalOutlet.attach(portal);
82+
const attachResult = this._portalOutlet.attach(portal);
8283

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

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

116119
if (this._config.panelClass) {
@@ -140,7 +143,9 @@ export class OverlayRef implements PortalOutlet {
140143
return;
141144
}
142145

143-
this.detachBackdrop();
146+
if (this._backdropHost && this._backdropHost.hasAttached()) {
147+
this._backdropHost.detach();
148+
}
144149

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

181-
this.detachBackdrop();
186+
this.disposeBackdrop();
182187
this._keyboardDispatcher.remove(this);
183188
this._portalOutlet.dispose();
184189
this._attachments.complete();
@@ -204,7 +209,7 @@ export class OverlayRef implements PortalOutlet {
204209

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

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

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

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

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

372330
function formatCssUnit(value: number | string) {

0 commit comments

Comments
 (0)