diff --git a/src/material/dialog/dialog-ref.ts b/src/material/dialog/dialog-ref.ts index 02b1ec017a2..8a54579dbbf 100644 --- a/src/material/dialog/dialog-ref.ts +++ b/src/material/dialog/dialog-ref.ts @@ -42,6 +42,9 @@ export class MatDialogRef { /** Result to be passed to afterClosed. */ private _result: R | undefined; + /** Handle to the timeout that's running as a fallback in case the exit animation doesn't fire. */ + private _closeFallbackTimeout: number; + constructor( private _overlayRef: OverlayRef, public _containerInstance: MatDialogContainer, @@ -66,7 +69,10 @@ export class MatDialogRef { _containerInstance._animationStateChanged.pipe( filter(event => event.phaseName === 'done' && event.toState === 'exit'), take(1) - ).subscribe(() => this._overlayRef.dispose()); + ).subscribe(() => { + clearTimeout(this._closeFallbackTimeout); + this._overlayRef.dispose(); + }); _overlayRef.detachments().subscribe(() => { this._beforeClosed.next(this._result); @@ -99,10 +105,19 @@ export class MatDialogRef { filter(event => event.phaseName === 'start'), take(1) ) - .subscribe(() => { + .subscribe(event => { this._beforeClosed.next(dialogResult); this._beforeClosed.complete(); this._overlayRef.detachBackdrop(); + + // The logic that disposes of the overlay depends on the exit animation completing, however + // it isn't guaranteed if the parent view is destroyed while it's running. Add a fallback + // timeout which will clean everything up if the animation hasn't fired within the specified + // amount of time plus 100ms. We don't need to run this outside the NgZone, because for the + // vast majority of cases the timeout will have been cleared before it has the chance to fire. + this._closeFallbackTimeout = setTimeout(() => { + this._overlayRef.dispose(); + }, event.totalTime + 100); }); this._containerInstance._startExitAnimation(); diff --git a/src/material/dialog/dialog.spec.ts b/src/material/dialog/dialog.spec.ts index 38ba018d3f8..5093fc868e0 100644 --- a/src/material/dialog/dialog.spec.ts +++ b/src/material/dialog/dialog.spec.ts @@ -194,6 +194,17 @@ describe('MatDialog', () => { expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeNull(); })); + it('should dispose of dialog if view container is destroyed while animating', fakeAsync(() => { + const dialogRef = dialog.open(PizzaMsg, {viewContainerRef: testViewContainerRef}); + + dialogRef.close(); + viewContainerFixture.detectChanges(); + viewContainerFixture.destroy(); + flush(); + + expect(overlayContainerElement.querySelector('mat-dialog-container')).toBeNull(); + })); + it('should dispatch the beforeClose and afterClose events when the ' + 'overlay is detached externally', fakeAsync(inject([Overlay], (overlay: Overlay) => { const dialogRef = dialog.open(PizzaMsg, { @@ -1064,6 +1075,7 @@ describe('MatDialog', () => { document.body.removeChild(button); document.body.removeChild(input); + flush(); })); it('should move focus to the container if there are no focusable elements in the dialog',