diff --git a/src/cdk/overlay/_index.scss b/src/cdk/overlay/_index.scss index 07571dfef8fe..fcf02f134f8a 100644 --- a/src/cdk/overlay/_index.scss +++ b/src/cdk/overlay/_index.scss @@ -96,12 +96,18 @@ $backdrop-animation-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default; } .cdk-overlay-transparent-backdrop { + // Define a transition on the visibility so that the `transitionend` event can fire immediately. + transition: visibility 1ms linear, opacity 1ms linear; + visibility: hidden; + opacity: 1; + // Note: as of Firefox 57, having the backdrop be `background: none` will prevent it from // capturing the user's mouse scroll events. Since we also can't use something like // `rgba(0, 0, 0, 0)`, we work around the inconsistency by not setting the background at // all and using `opacity` to make the element transparent. - &, &.cdk-overlay-backdrop-showing { + &.cdk-overlay-backdrop-showing { opacity: 0; + visibility: visible; } } diff --git a/src/cdk/overlay/overlay-ref.ts b/src/cdk/overlay/overlay-ref.ts index 0dc05592cd0c..b9de4897791e 100644 --- a/src/cdk/overlay/overlay-ref.ts +++ b/src/cdk/overlay/overlay-ref.ts @@ -31,6 +31,7 @@ export type ImmutableObject = { */ export class OverlayRef implements PortalOutlet, OverlayReference { private _backdropElement: HTMLElement | null = null; + private _backdropTimeout: number | undefined; private readonly _backdropClick = new Subject(); private readonly _attachments = new Subject(); private readonly _detachments = new Subject(); @@ -38,6 +39,9 @@ export class OverlayRef implements PortalOutlet, OverlayReference { private _scrollStrategy: ScrollStrategy | undefined; private _locationChanges: SubscriptionLike = Subscription.EMPTY; private _backdropClickHandler = (event: MouseEvent) => this._backdropClick.next(event); + private _backdropTransitionendHandler = (event: TransitionEvent) => { + this._disposeBackdrop(event.target as HTMLElement | null); + }; /** * Reference to the parent of the `_host` at the time it was detached. Used to restore @@ -418,26 +422,10 @@ export class OverlayRef implements PortalOutlet, OverlayReference { return; } - let timeoutId: number; - const finishDetach = () => { - // It may not be attached to anything in certain cases (e.g. unit tests). - if (backdropToDetach) { - backdropToDetach.removeEventListener('click', this._backdropClickHandler); - backdropToDetach.removeEventListener('transitionend', finishDetach); - this._disposeBackdrop(backdropToDetach); - } - - if (this._config.backdropClass) { - this._toggleClasses(backdropToDetach!, this._config.backdropClass, false); - } - - clearTimeout(timeoutId); - }; - backdropToDetach.classList.remove('cdk-overlay-backdrop-showing'); this._ngZone.runOutsideAngular(() => { - backdropToDetach!.addEventListener('transitionend', finishDetach); + backdropToDetach!.addEventListener('transitionend', this._backdropTransitionendHandler); }); // If the backdrop doesn't have a transition, the `transitionend` event won't fire. @@ -447,7 +435,12 @@ export class OverlayRef implements PortalOutlet, OverlayReference { // 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. - timeoutId = this._ngZone.runOutsideAngular(() => setTimeout(finishDetach, 500)); + this._backdropTimeout = this._ngZone.runOutsideAngular(() => + setTimeout(() => { + console.log('fallback'); + this._disposeBackdrop(backdropToDetach); + }, 500), + ); } /** Toggles a single CSS class or an array of classes on an element. */ @@ -505,6 +498,8 @@ export class OverlayRef implements PortalOutlet, OverlayReference { /** Removes a backdrop element from the DOM. */ private _disposeBackdrop(backdrop: HTMLElement | null) { if (backdrop) { + backdrop.removeEventListener('click', this._backdropClickHandler); + backdrop.removeEventListener('transitionend', this._backdropTransitionendHandler); backdrop.remove(); // It is possible that a new portal has been attached to this overlay since we started @@ -514,6 +509,11 @@ export class OverlayRef implements PortalOutlet, OverlayReference { this._backdropElement = null; } } + + if (this._backdropTimeout) { + clearTimeout(this._backdropTimeout); + this._backdropTimeout = undefined; + } } } diff --git a/src/cdk/overlay/overlay.spec.ts b/src/cdk/overlay/overlay.spec.ts index 180c6cb0ebce..cd357c260b39 100644 --- a/src/cdk/overlay/overlay.spec.ts +++ b/src/cdk/overlay/overlay.spec.ts @@ -348,6 +348,16 @@ describe('Overlay', () => { // `fakeAsync` will throw if we have an unflushed timer. })); + it('should clear the backdrop timeout if the overlay is disposed', fakeAsync(() => { + const overlayRef = overlay.create({hasBackdrop: true}); + overlayRef.attach(componentPortal); + overlayRef.detach(); + overlayRef.dispose(); + + // Note: we don't `tick` or `flush` here. The assertion is that + // `fakeAsync` will throw if we have an unflushed timer. + })); + it('should be able to use the `Overlay` provider during app initialization', () => { /** Dummy provider that depends on `Overlay`. */ @Injectable()