From 44eea0ef30f776e43777bb206a0095b825d0faa8 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Fri, 12 Oct 2018 14:19:23 +0300 Subject: [PATCH] fix(experimental/dialog): emitting events twice on some browsers Fixes the `Dialog` emitting some of its events twice on certain browsers due to an issue in `@angular/animations` which invokes the animation `done` callback twice. Fixes #13585. --- .../dialog/dialog-container.ts | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/cdk-experimental/dialog/dialog-container.ts b/src/cdk-experimental/dialog/dialog-container.ts index ffe9ae17dffe..88e5ffa5094c 100644 --- a/src/cdk-experimental/dialog/dialog-container.ts +++ b/src/cdk-experimental/dialog/dialog-container.ts @@ -30,6 +30,7 @@ import { ViewEncapsulation, } from '@angular/core'; import {Subject} from 'rxjs'; +import {distinctUntilChanged} from 'rxjs/operators'; import {DialogConfig} from './dialog-config'; @@ -61,7 +62,7 @@ export function throwDialogContentAlreadyAttachedError() { host: { '[@dialog]': '_state', '(@dialog.start)': '_onAnimationStart($event)', - '(@dialog.done)': '_onAnimationDone($event)', + '(@dialog.done)': '_animationDone.next($event)', }, }) export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { @@ -103,6 +104,9 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { /** A subject emitting after the dialog exits the view. */ _afterExit: Subject = new Subject(); + /** Stream of animation `done` events. */ + _animationDone = new Subject(); + constructor( private _elementRef: ElementRef, private _focusTrapFactory: FocusTrapFactory, @@ -111,11 +115,30 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { /** The dialog configuration. */ public _config: DialogConfig) { super(); + + // We use a Subject with a distinctUntilChanged, rather than a callback attached to .done, + // because some browsers fire the done event twice and we don't want to emit duplicate events. + // See: https://github.com/angular/angular/issues/24084 + this._animationDone.pipe(distinctUntilChanged((x, y) => { + return x.fromState === y.fromState && x.toState === y.toState; + })).subscribe(event => { + // Emit lifecycle events based on animation `done` callback. + if (event.toState === 'enter') { + this._autoFocusFirstTabbableElement(); + this._afterEnter.next(); + } + + if (event.fromState === 'enter' && (event.toState === 'void' || event.toState === 'exit')) { + this._returnFocusAfterDialog(); + this._afterExit.next(); + } + }); } /** Destroy focus trap to place focus back to the element focused before the dialog opened. */ ngOnDestroy() { this._focusTrap.destroy(); + this._animationDone.complete(); } /** @@ -154,19 +177,6 @@ export class CdkDialogContainer extends BasePortalOutlet implements OnDestroy { } } - /** Emit lifecycle events based on animation `done` callback. */ - _onAnimationDone(event: AnimationEvent) { - if (event.toState === 'enter') { - this._autoFocusFirstTabbableElement(); - this._afterEnter.next(); - } - - if (event.fromState === 'enter' && (event.toState === 'void' || event.toState === 'exit')) { - this._returnFocusAfterDialog(); - this._afterExit.next(); - } - } - /** Starts the dialog exit animation. */ _startExiting(): void { this._state = 'exit';