Skip to content

Commit 95066fa

Browse files
committed
fix(material/dialog): opening animation sometimes being skipped (#26971)
When we trigger a dialog animation, we set a CSS variable and a couple of classes to start it, however because we were adding the classes imemdiately after setting the variable, sometimes the animation would be skipped because the browser didn't have time to recalculate the styles. These changes resolve the issue by adding the classes in a `requestAnimationFrame` which gives the browser enough time to pick up the animation duration. Fixes #26931. (cherry picked from commit 1f51ecc)
1 parent 757a456 commit 95066fa

File tree

4 files changed

+26
-9
lines changed

4 files changed

+26
-9
lines changed

src/cdk/dialog/dialog-container.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig>
9696
@Optional() @Inject(DOCUMENT) _document: any,
9797
@Inject(DialogConfig) readonly _config: C,
9898
private _interactivityChecker: InteractivityChecker,
99-
private _ngZone: NgZone,
99+
protected _ngZone: NgZone,
100100
private _overlayRef: OverlayRef,
101101
private _focusMonitor?: FocusMonitor,
102102
) {

src/material/dialog/dialog-container.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes
166166
? parseCssTime(this._config.exitAnimationDuration) ?? CLOSE_ANIMATION_DURATION
167167
: 0;
168168
/** Current timer for dialog animations. */
169-
private _animationTimer: number | null = null;
169+
private _animationTimer: ReturnType<typeof setTimeout> | null = null;
170170

171171
constructor(
172172
elementRef: ElementRef,
@@ -221,14 +221,15 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes
221221
this._animationStateChanged.emit({state: 'opening', totalTime: this._openAnimationDuration});
222222

223223
if (this._animationsEnabled) {
224-
// One would expect that the open class is added once the animation finished, but MDC
225-
// uses the open class in combination with the opening class to start the animation.
226224
this._hostElement.style.setProperty(
227225
TRANSITION_DURATION_PROPERTY,
228226
`${this._openAnimationDuration}ms`,
229227
);
230-
this._hostElement.classList.add(OPENING_CLASS);
231-
this._hostElement.classList.add(OPEN_CLASS);
228+
229+
// We need to give the `setProperty` call from above some time to be applied.
230+
// One would expect that the open class is added once the animation finished, but MDC
231+
// uses the open class in combination with the opening class to start the animation.
232+
this._requestAnimationFrame(() => this._hostElement.classList.add(OPENING_CLASS, OPEN_CLASS));
232233
this._waitForAnimationToComplete(this._openAnimationDuration, this._finishDialogOpen);
233234
} else {
234235
this._hostElement.classList.add(OPEN_CLASS);
@@ -253,7 +254,9 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes
253254
TRANSITION_DURATION_PROPERTY,
254255
`${this._openAnimationDuration}ms`,
255256
);
256-
this._hostElement.classList.add(CLOSING_CLASS);
257+
258+
// We need to give the `setProperty` call from above some time to be applied.
259+
this._requestAnimationFrame(() => this._hostElement.classList.add(CLOSING_CLASS));
257260
this._waitForAnimationToComplete(this._closeAnimationDuration, this._finishDialogClose);
258261
} else {
259262
// This subscription to the `OverlayRef#backdropClick` observable in the `DialogRef` is
@@ -297,8 +300,7 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes
297300

298301
/** Clears all dialog animation classes. */
299302
private _clearAnimationClasses() {
300-
this._hostElement.classList.remove(OPENING_CLASS);
301-
this._hostElement.classList.remove(CLOSING_CLASS);
303+
this._hostElement.classList.remove(OPENING_CLASS, CLOSING_CLASS);
302304
}
303305

304306
private _waitForAnimationToComplete(duration: number, callback: () => void) {
@@ -310,4 +312,15 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes
310312
// the related events like `afterClosed` to be inside the zone as well.
311313
this._animationTimer = setTimeout(callback, duration);
312314
}
315+
316+
/** Runs a callback in `requestAnimationFrame`, if available. */
317+
private _requestAnimationFrame(callback: () => void) {
318+
this._ngZone.runOutsideAngular(() => {
319+
if (typeof requestAnimationFrame === 'function') {
320+
requestAnimationFrame(callback);
321+
} else {
322+
callback();
323+
}
324+
});
325+
}
313326
}

src/material/dialog/dialog.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2000,6 +2000,8 @@ describe('MDC-based MatDialog with animations enabled', () => {
20002000
expect(spy).not.toHaveBeenCalled();
20012001

20022002
tick(OPEN_ANIMATION_DURATION);
2003+
flush();
2004+
20032005
expect(spy).toHaveBeenCalled();
20042006
}));
20052007

tools/public_api_guard/cdk/dialog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ export class CdkDialogContainer<C extends DialogConfig = DialogConfig> extends B
6464
protected _focusTrapFactory: FocusTrapFactory;
6565
// (undocumented)
6666
ngOnDestroy(): void;
67+
// (undocumented)
68+
protected _ngZone: NgZone;
6769
_portalOutlet: CdkPortalOutlet;
6870
_recaptureFocus(): void;
6971
protected _trapFocus(): void;

0 commit comments

Comments
 (0)