diff --git a/src/cdk/a11y/tsconfig-build.json b/src/cdk/a11y/tsconfig-build.json index c9480c9ecef8..8bde0bb3f5db 100644 --- a/src/cdk/a11y/tsconfig-build.json +++ b/src/cdk/a11y/tsconfig-build.json @@ -1,7 +1,8 @@ { "extends": "../tsconfig-build", "files": [ - "public-api.ts" + "public-api.ts", + "../typings.d.ts" ], "angularCompilerOptions": { "annotateForClosureCompiler": true, diff --git a/src/cdk/overlay/_overlay.scss b/src/cdk/overlay/_overlay.scss index 7090525badc9..3b06b238e02c 100644 --- a/src/cdk/overlay/_overlay.scss +++ b/src/cdk/overlay/_overlay.scss @@ -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; } } diff --git a/src/cdk/overlay/backdrop.ts b/src/cdk/overlay/backdrop.ts new file mode 100644 index 000000000000..cab32cbb3c78 --- /dev/null +++ b/src/cdk/overlay/backdrop.ts @@ -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(); + _animationStream = new Subject(); + + constructor(public _element: ElementRef) {} + + _setClass(cssClass: string) { + this._element.nativeElement.classList.add(cssClass); + } + + ngOnDestroy() { + this._clickStream.complete(); + } +} diff --git a/src/cdk/overlay/overlay-directives.spec.ts b/src/cdk/overlay/overlay-directives.spec.ts index 135e98e4965e..16837d7a74ef 100644 --- a/src/cdk/overlay/overlay-directives.spec.ts +++ b/src/cdk/overlay/overlay-directives.spec.ts @@ -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'; @@ -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'}}], }); diff --git a/src/cdk/overlay/overlay-module.ts b/src/cdk/overlay/overlay-module.ts index 513692578603..862c73fceb8c 100644 --- a/src/cdk/overlay/overlay-module.ts +++ b/src/cdk/overlay/overlay-module.ts @@ -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 {} diff --git a/src/cdk/overlay/overlay-ref.ts b/src/cdk/overlay/overlay-ref.ts index 83eb45f9c665..6a47440670a3 100644 --- a/src/cdk/overlay/overlay-ref.ts +++ b/src/cdk/overlay/overlay-ref.ts @@ -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. */ @@ -26,10 +27,10 @@ export type ImmutableObject = { * Used to manipulate or dispose of said overlay. */ export class OverlayRef implements PortalOutlet { - private _backdropElement: HTMLElement | null = null; - private _backdropClick: Subject = new Subject(); + private _backdropClick = new Subject(); private _attachments = new Subject(); private _detachments = new Subject(); + private _backdropInstance: CdkOverlayBackdrop | null; /** Stream of keydown events dispatched to this overlay. */ _keydownEvents = new Subject(); @@ -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, private _ngZone: NgZone, - private _keyboardDispatcher: OverlayKeyboardDispatcher, - private _document: Document) { + private _keyboardDispatcher: OverlayKeyboardDispatcher) { if (_config.scrollStrategy) { _config.scrollStrategy.attach(this); @@ -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; } /** @@ -79,7 +80,7 @@ export class OverlayRef implements PortalOutlet { * @returns The portal attachment result. */ attach(portal: Portal): any { - let attachResult = this._portalOutlet.attach(portal); + const attachResult = this._portalOutlet.attach(portal); if (this._config.positionStrategy) { this._config.positionStrategy.attach(this); @@ -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) { @@ -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 @@ -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(); @@ -205,7 +210,7 @@ export class OverlayRef implements PortalOutlet { /** Gets an observable that emits when the backdrop has been clicked. */ backdropClick(): Observable { - return this._backdropClick.asObservable(); + return this._backdropInstance ? this._backdropInstance._clickStream : empty(); } /** Gets an observable that emits when the overlay has been attached. */ @@ -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, @@ -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(); + } } diff --git a/src/cdk/overlay/overlay.spec.ts b/src/cdk/overlay/overlay.spec.ts index a2121b758a21..ce3e5f2fb248 100644 --- a/src/cdk/overlay/overlay.spec.ts +++ b/src/cdk/overlay/overlay.spec.ts @@ -1,6 +1,7 @@ import {async, fakeAsync, tick, ComponentFixture, inject, TestBed} from '@angular/core/testing'; import {Component, NgModule, ViewChild, ViewContainerRef} from '@angular/core'; import {Direction, Directionality} from '@angular/cdk/bidi'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import { ComponentPortal, PortalModule, @@ -30,7 +31,7 @@ describe('Overlay', () => { beforeEach(async(() => { dir = 'ltr'; TestBed.configureTestingModule({ - imports: [OverlayModule, PortalModule, OverlayTestModule], + imports: [OverlayModule, PortalModule, OverlayTestModule, NoopAnimationsModule], providers: [{ provide: Directionality, useFactory: () => { @@ -92,6 +93,7 @@ describe('Overlay', () => { .toBe('auto', 'Expected the overlay pane to enable pointerEvents when attached.'); overlayRef.detach(); + viewContainerFixture.detectChanges(); expect(paneElement.childNodes.length).toBe(0); expect(paneElement.style.pointerEvents) @@ -220,6 +222,8 @@ describe('Overlay', () => { let overlayRef = overlay.create(); overlayRef.detachments().subscribe(() => { + viewContainerFixture.detectChanges(); + expect(overlayContainerElement.querySelector('pizza')) .toBeFalsy('Expected the overlay to have been detached.'); }); @@ -410,7 +414,6 @@ describe('Overlay', () => { viewContainerFixture.detectChanges(); let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement; expect(backdrop).toBeTruthy(); - expect(backdrop.classList).not.toContain('cdk-overlay-backdrop-showing'); let backdropClickHandler = jasmine.createSpy('backdropClickHander'); overlayRef.backdropClick().subscribe(backdropClickHandler); @@ -453,29 +456,15 @@ describe('Overlay', () => { expect(backdrop.classList).toContain('cdk-overlay-transparent-backdrop'); }); - it('should disable the pointer events of a backdrop that is being removed', () => { + it('should insert the backdrop before the overlay pane in the DOM order', () => { let overlayRef = overlay.create(config); - overlayRef.attach(componentPortal); - - viewContainerFixture.detectChanges(); - let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop') as HTMLElement; - - expect(backdrop.style.pointerEvents).toBeFalsy(); - - overlayRef.detach(); - - expect(backdrop.style.pointerEvents).toBe('none'); - }); - - it('should insert the backdrop before the overlay host in the DOM order', () => { - const overlayRef = overlay.create(config); overlayRef.attach(componentPortal); viewContainerFixture.detectChanges(); - const backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop'); - const host = overlayContainerElement.querySelector('.cdk-overlay-pane')!.parentElement!; - const children = Array.prototype.slice.call(overlayContainerElement.children); + let backdrop = overlayContainerElement.querySelector('.cdk-overlay-backdrop')!.parentNode; + let children = Array.prototype.slice.call(overlayContainerElement.children); + let host = overlayRef.hostElement; expect(children.indexOf(backdrop)).toBeGreaterThan(-1); expect(children.indexOf(host)).toBeGreaterThan(-1); diff --git a/src/cdk/overlay/overlay.ts b/src/cdk/overlay/overlay.ts index 7c6511aa2f4d..ed35be51947b 100644 --- a/src/cdk/overlay/overlay.ts +++ b/src/cdk/overlay/overlay.ts @@ -60,15 +60,17 @@ export class Overlay { * @returns Reference to the created overlay. */ create(config?: OverlayConfig): OverlayRef { - const host = this._createHostElement(); + const overlayConfig = new OverlayConfig(config); + const backdrop = overlayConfig.hasBackdrop ? this._createOverlayElement() : null; + const backdropHost = backdrop ? this._createPortalOutlet(backdrop) : null; + const host = this._createOverlayElement(); const pane = this._createPaneElement(host); const portalOutlet = this._createPortalOutlet(pane); - const overlayConfig = new OverlayConfig(config); overlayConfig.direction = overlayConfig.direction || this._directionality.value; - return new OverlayRef(portalOutlet, host, pane, overlayConfig, this._ngZone, - this._keyboardDispatcher, this._document); + return new OverlayRef(portalOutlet, host, pane, backdropHost, overlayConfig, this._ngZone, + this._keyboardDispatcher); } /** @@ -94,21 +96,17 @@ export class Overlay { return pane; } - /** - * Creates the host element that wraps around an overlay - * and can be used for advanced positioning. - * @returns Newly-create host element. - */ - private _createHostElement(): HTMLElement { - const host = this._document.createElement('div'); - this._overlayContainer.getContainerElement().appendChild(host); - return host; + /** Creates an element and appends it to the overlay container. */ + private _createOverlayElement(): HTMLElement { + const element = this._document.createElement('div'); + this._overlayContainer.getContainerElement().appendChild(element); + return element; } /** - * Create a DomPortalOutlet into which the overlay content can be loaded. - * @param pane The DOM element to turn into a portal outlet. - * @returns A portal outlet for the given DOM element. + * Create a DomPortalHost into which the overlay content can be loaded. + * @param pane The DOM element to turn into a portal host. + * @returns A portal host for the given DOM element. */ private _createPortalOutlet(pane: HTMLElement): DomPortalOutlet { return new DomPortalOutlet(pane, this._componentFactoryResolver, this._appRef, this._injector); diff --git a/src/cdk/overlay/public-api.ts b/src/cdk/overlay/public-api.ts index 9b95f7ef073b..ead93e595e58 100644 --- a/src/cdk/overlay/public-api.ts +++ b/src/cdk/overlay/public-api.ts @@ -10,6 +10,8 @@ export * from './overlay-config'; export * from './position/connected-position'; export * from './scroll/index'; export * from './overlay-module'; +export * from './backdrop'; + export {Overlay} from './overlay'; export {OverlayContainer} from './overlay-container'; export {CdkOverlayOrigin, CdkConnectedOverlay} from './overlay-directives'; diff --git a/src/cdk/overlay/tsconfig-build.json b/src/cdk/overlay/tsconfig-build.json index 0a06cd3fa8c5..758030030236 100644 --- a/src/cdk/overlay/tsconfig-build.json +++ b/src/cdk/overlay/tsconfig-build.json @@ -1,7 +1,8 @@ { "extends": "../tsconfig-build", "files": [ - "public-api.ts" + "public-api.ts", + "../typings.d.ts" ], "angularCompilerOptions": { "annotateForClosureCompiler": true, diff --git a/src/lib/dialog/dialog-ref.ts b/src/lib/dialog/dialog-ref.ts index 2857ec8f731b..a8ef4581b5a6 100644 --- a/src/lib/dialog/dialog-ref.ts +++ b/src/lib/dialog/dialog-ref.ts @@ -104,11 +104,10 @@ export class MatDialogRef { this._containerInstance._animationStateChanged.pipe( filter(event => event.phaseName === 'start'), take(1) - ) - .subscribe(() => { + ).subscribe(() => { this._beforeClose.next(dialogResult); this._beforeClose.complete(); - this._overlayRef.detachBackdrop(); + this._overlayRef.disposeBackdrop(); }); this._containerInstance._startExitAnimation();