fix(dialog): DOM nodes not cleaned up if view container is destroyed mid-animation (#16309)

Currently the dialog's cleanup logic is tied to its exit animation completing. This usually isn't a problem since by default the dialog is attached to the application ref, however if the consumer has set a `viewContainerRef` and that ref is destroyed mid-animation, the exit animation event will never fire. These changes add a timeout as a fallback in case the animation doesn't finish in the specified time.

Fixes #16284.
This commit is contained in:
Kristiyan Kostadinov
2019-06-26 17:47:49 +02:00
committed by Andrew Seguin
parent 3fb87d799c
commit 2ab867332f
2 changed files with 29 additions and 2 deletions
+17 -2
View File
@@ -42,6 +42,9 @@ export class MatDialogRef<T, R = any> {
/** 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<T, R = any> {
_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<T, R = any> {
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();
+12
View File
@@ -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',