Skip to content

refactor(overlay): use component to render backdrop #6627

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/cdk/a11y/tsconfig-build.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"extends": "../tsconfig-build",
"files": [
"public-api.ts"
"public-api.ts",
"../typings.d.ts"
],
"angularCompilerOptions": {
"annotateForClosureCompiler": true,
Expand Down
17 changes: 9 additions & 8 deletions src/cdk/overlay/_overlay.scss
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,15 @@ $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
transition: opacity $backdrop-animation-duration $backdrop-animation-timing-function;
opacity: 0;

&.cdk-overlay-backdrop-showing {
opacity: 1;

// In high contrast mode the rgba background will become solid
// so we need to fall back to making it opaque using `opacity`.
@include cdk-high-contrast {
opacity: 0.6;
}
// In high contrast mode the rgba background will become solid
// so we need to fall back to making it opaque using `opacity`.
@include cdk-high-contrast {
opacity: 0.6;
}

// Prevent the user from interacting while the backdrop is animating.
&.ng-animating {
pointer-events: none;
}
}

Expand Down
56 changes: 56 additions & 0 deletions src/cdk/overlay/backdrop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {
Component,
ViewEncapsulation,
ChangeDetectionStrategy,
OnDestroy,
ElementRef,
} from '@angular/core';
import {animate, AnimationEvent, state, style, transition, trigger} from '@angular/animations';
import {Subject} from 'rxjs';

/**
* Semi-transparent backdrop that will be rendered behind an overlay.
* @docs-private
*/
@Component({
moduleId: module.id,
template: '',
host: {
'class': 'cdk-overlay-backdrop',
'[@state]': '_animationState',
'(@state.done)': '_animationStream.next($event)',
'(click)': '_clickStream.next($event)',
},
animations: [
trigger('state', [
state('void', style({opacity: '0'})),
state('visible', style({opacity: '1'})),
transition('* => *', animate('400ms cubic-bezier(0.25, 0.8, 0.25, 1)')),
])
],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class CdkOverlayBackdrop implements OnDestroy {
_animationState = 'visible';
_clickStream = new Subject<MouseEvent>();
_animationStream = new Subject<AnimationEvent>();

constructor(public _element: ElementRef) {}

_setClass(cssClass: string) {
this._element.nativeElement.classList.add(cssClass);
}

ngOnDestroy() {
this._clickStream.complete();
}
}
3 changes: 2 additions & 1 deletion src/cdk/overlay/overlay-directives.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Component, ViewChild} from '@angular/core';
import {By} from '@angular/platform-browser';
import {ComponentFixture, TestBed, async, inject} from '@angular/core/testing';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {Directionality} from '@angular/cdk/bidi';
import {dispatchKeyboardEvent} from '@angular/cdk/testing';
import {ESCAPE} from '@angular/cdk/keycodes';
Expand All @@ -21,7 +22,7 @@ describe('Overlay directives', () => {

beforeEach(() => {
TestBed.configureTestingModule({
imports: [OverlayModule],
imports: [OverlayModule, NoopAnimationsModule],
declarations: [ConnectedOverlayDirectiveTest, ConnectedOverlayPropertyInitOrder],
providers: [{provide: Directionality, useFactory: () => dir = {value: 'ltr'}}],
});
Expand Down
6 changes: 4 additions & 2 deletions src/cdk/overlay/overlay-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ import {
CdkOverlayOrigin,
} from './overlay-directives';
import {OverlayPositionBuilder} from './position/overlay-position-builder';
import {CdkOverlayBackdrop} from './backdrop';


@NgModule({
imports: [BidiModule, PortalModule, ScrollDispatchModule],
exports: [CdkConnectedOverlay, CdkOverlayOrigin, ScrollDispatchModule],
declarations: [CdkConnectedOverlay, CdkOverlayOrigin],
exports: [CdkConnectedOverlay, CdkOverlayOrigin, CdkOverlayBackdrop, ScrollDispatchModule],
declarations: [CdkConnectedOverlay, CdkOverlayOrigin, CdkOverlayBackdrop],
providers: [Overlay],
entryComponents: [CdkOverlayBackdrop],
})
export class OverlayModule {}

Expand Down
116 changes: 37 additions & 79 deletions src/cdk/overlay/overlay-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
import {Direction} from '@angular/cdk/bidi';
import {ComponentPortal, Portal, PortalOutlet, TemplatePortal} from '@angular/cdk/portal';
import {ComponentRef, EmbeddedViewRef, NgZone} from '@angular/core';
import {Observable, Subject} from 'rxjs';
import {Observable, Subject, empty} from 'rxjs';
import {take} from 'rxjs/operators';
import {OverlayKeyboardDispatcher} from './keyboard/overlay-keyboard-dispatcher';
import {OverlayConfig} from './overlay-config';
import {coerceCssPixelValue} from '@angular/cdk/coercion';
import {CdkOverlayBackdrop} from './backdrop';


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

/** Stream of keydown events dispatched to this overlay. */
_keydownEvents = new Subject<KeyboardEvent>();
Expand All @@ -38,10 +39,10 @@ export class OverlayRef implements PortalOutlet {
private _portalOutlet: PortalOutlet,
private _host: HTMLElement,
private _pane: HTMLElement,
private _backdropHost: PortalOutlet | null,
private _config: ImmutableObject<OverlayConfig>,
private _ngZone: NgZone,
private _keyboardDispatcher: OverlayKeyboardDispatcher,
private _document: Document) {
private _keyboardDispatcher: OverlayKeyboardDispatcher) {

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

/** The overlay's backdrop HTML element. */
get backdropElement(): HTMLElement | null {
return this._backdropElement;
return this._backdropInstance ? this._backdropInstance._element.nativeElement : null;
}

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

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

if (this._config.hasBackdrop) {
this._attachBackdrop();
if (this._backdropHost) {
this._backdropInstance =
this._backdropHost.attach(new ComponentPortal(CdkOverlayBackdrop)).instance;
this._backdropInstance!._setClass(this._config.backdropClass!);
}

if (this._config.panelClass) {
Expand Down Expand Up @@ -141,7 +144,9 @@ export class OverlayRef implements PortalOutlet {
return;
}

this.detachBackdrop();
if (this._backdropHost && this._backdropHost.hasAttached()) {
this._backdropHost.detach();
}

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

this.detachBackdrop();
this.disposeBackdrop();
this._keyboardDispatcher.remove(this);
this._portalOutlet.dispose();
this._attachments.complete();
Expand All @@ -205,7 +210,7 @@ export class OverlayRef implements PortalOutlet {

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

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

/** Attaches a backdrop for this overlay. */
private _attachBackdrop() {
const showingClass = 'cdk-overlay-backdrop-showing';

this._backdropElement = this._document.createElement('div');
this._backdropElement.classList.add('cdk-overlay-backdrop');

if (this._config.backdropClass) {
this._backdropElement.classList.add(this._config.backdropClass);
}

// Insert the backdrop before the pane in the DOM order,
// in order to handle stacked overlays properly.
this._host.parentElement!.insertBefore(this._backdropElement, this._host);

// Forward backdrop clicks such that the consumer of the overlay can perform whatever
// action desired when such a click occurs (usually closing the overlay).
this._backdropElement.addEventListener('click',
(event: MouseEvent) => this._backdropClick.next(event));

// Add class to fade-in the backdrop after one frame.
if (typeof requestAnimationFrame !== 'undefined') {
this._ngZone.runOutsideAngular(() => {
requestAnimationFrame(() => {
if (this._backdropElement) {
this._backdropElement.classList.add(showingClass);
}
});
});
} else {
this._backdropElement.classList.add(showingClass);
}
}

/**
* Updates the stacking order of the element, moving it to the top if necessary.
* This is required in cases where one overlay was detached, while another one,
Expand All @@ -331,43 +302,30 @@ export class OverlayRef implements PortalOutlet {
}
}

/** Detaches the backdrop (if any) associated with the overlay. */
detachBackdrop(): void {
let backdropToDetach = this._backdropElement;

if (backdropToDetach) {
let finishDetach = () => {
// It may not be attached to anything in certain cases (e.g. unit tests).
if (backdropToDetach && backdropToDetach.parentNode) {
backdropToDetach.parentNode.removeChild(backdropToDetach);
}

// It is possible that a new portal has been attached to this overlay since we started
// removing the backdrop. If that is the case, only clear the backdrop reference if it
// is still the same instance that we started to remove.
if (this._backdropElement == backdropToDetach) {
this._backdropElement = null;
}
};

backdropToDetach.classList.remove('cdk-overlay-backdrop-showing');
/** Animates out and disposes of the backdrop. */
disposeBackdrop(): void {
if (this._backdropHost) {
if (this._backdropHost.hasAttached()) {
this._backdropHost.detach();

if (this._config.backdropClass) {
backdropToDetach.classList.remove(this._config.backdropClass);
this._backdropInstance!._animationStream.pipe(take(1)).subscribe(() => {
this._backdropHost!.dispose();
this._backdropHost = this._backdropInstance = null;
});
} else {
this._backdropHost.dispose();
}

backdropToDetach.addEventListener('transitionend', finishDetach);

// If the backdrop doesn't have a transition, the `transitionend` event won't fire.
// In this case we make it unclickable and we try to remove it after a delay.
backdropToDetach.style.pointerEvents = 'none';

// Run this outside the Angular zone because there's nothing that Angular cares about.
// If it were to run inside the Angular zone, every test that used Overlay would have to be
// either async or fakeAsync.
this._ngZone.runOutsideAngular(() => setTimeout(finishDetach, 500));
}
}

/**
* Detaches the backdrop (if any) associated with the overlay.
* @deprecated Use `disposeBackdrop` instead.
* @deletion-target 7.0.0
*/
detachBackdrop(): void {
this.disposeBackdrop();
}
}


Expand Down
Loading