Skip to content

Commit 4cdc095

Browse files
authored
fix(material/dialog): allow customizing animation duration (#25524)
* fix(material/dialog): allow customizing animation duration BREAKING CHANGE: - Passing strings for MatDialogConfig.enterAnimationDuration and MatDialogConfig.exitAnimationDuration is deprecated, pass numbers in ms instead * deprecate the string animation durations in favor of numbers
1 parent 7faafb4 commit 4cdc095

File tree

12 files changed

+110
-28
lines changed

12 files changed

+110
-28
lines changed

src/material/dialog/dialog-animations.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
* Default parameters for the animation for backwards compatibility.
2222
* @docs-private
2323
*/
24-
export const defaultParams = {
24+
export const _defaultParams = {
2525
params: {enterAnimationDuration: '150ms', exitAnimationDuration: '75ms'},
2626
};
2727

@@ -48,15 +48,15 @@ export const matDialogAnimations: {
4848
),
4949
query('@*', animateChild(), {optional: true}),
5050
]),
51-
defaultParams,
51+
_defaultParams,
5252
),
5353
transition(
5454
'* => void, * => exit',
5555
group([
5656
animate('{{exitAnimationDuration}} cubic-bezier(0.4, 0.0, 0.2, 1)', style({opacity: 0})),
5757
query('@*', animateChild(), {optional: true}),
5858
]),
59-
defaultParams,
59+
_defaultParams,
6060
),
6161
]),
6262
};

src/material/dialog/dialog-config.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import {ViewContainerRef, ComponentFactoryResolver, Injector} from '@angular/core';
1010
import {Direction} from '@angular/cdk/bidi';
1111
import {ScrollStrategy} from '@angular/cdk/overlay';
12-
import {defaultParams} from './dialog-animations';
12+
import {_defaultParams} from './dialog-animations';
1313

1414
/** Options for where to set focus to automatically on dialog open */
1515
export type AutoFocusTarget = 'dialog' | 'first-tabbable' | 'first-heading';
@@ -133,11 +133,19 @@ export class MatDialogConfig<D = any> {
133133
/** Alternate `ComponentFactoryResolver` to use when resolving the associated component. */
134134
componentFactoryResolver?: ComponentFactoryResolver;
135135

136-
/** Duration of the enter animation. Has to be a valid CSS value (e.g. 100ms). */
137-
enterAnimationDuration?: string = defaultParams.params.enterAnimationDuration;
136+
/**
137+
* Duration of the enter animation in ms.
138+
* Should be a number, string type is deprecated.
139+
* @breaking-change 17.0.0 Remove string signature.
140+
*/
141+
enterAnimationDuration?: string | number;
138142

139-
/** Duration of the exit animation. Has to be a valid CSS value (e.g. 50ms). */
140-
exitAnimationDuration?: string = defaultParams.params.exitAnimationDuration;
143+
/**
144+
* Duration of the exit animation in ms.
145+
* Should be a number, string type is deprecated.
146+
* @breaking-change 17.0.0 Remove string signature.
147+
*/
148+
exitAnimationDuration?: string | number;
141149

142150
// TODO(jelbourn): add configuration for lifecycle hooks, ARIA labelling.
143151
}

src/material/dialog/dialog-container.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {MatDialogConfig} from './dialog-config';
2424
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
2525
import {cssClasses, numbers} from '@material/dialog';
2626
import {CdkDialogContainer} from '@angular/cdk/dialog';
27+
import {coerceNumberProperty} from '@angular/cdk/coercion';
2728

2829
/** Event that captures the state of dialog container animations. */
2930
interface LegacyDialogAnimationEvent {
@@ -85,6 +86,33 @@ export abstract class _MatDialogContainerBase extends CdkDialogContainer<MatDial
8586
}
8687
}
8788

89+
const TRANSITION_DURATION_PROPERTY = '--mat-dialog-transition-duration';
90+
91+
// TODO(mmalerba): Remove this function after animation durations are required
92+
// to be numbers.
93+
/**
94+
* Converts a CSS time string to a number in ms. If the given time is already a
95+
* number, it is assumed to be in ms.
96+
*/
97+
function parseCssTime(time: string | number | undefined): number | null {
98+
if (time == null) {
99+
return null;
100+
}
101+
if (typeof time === 'number') {
102+
return time;
103+
}
104+
if (time.endsWith('ms')) {
105+
return coerceNumberProperty(time.substring(0, time.length - 2));
106+
}
107+
if (time.endsWith('s')) {
108+
return coerceNumberProperty(time.substring(0, time.length - 1)) * 1000;
109+
}
110+
if (time === '0') {
111+
return 0;
112+
}
113+
return null; // anything else is invalid.
114+
}
115+
88116
/**
89117
* Internal component that wraps user-provided dialog content in a MDC dialog.
90118
* @docs-private
@@ -117,11 +145,11 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes
117145
private _hostElement: HTMLElement = this._elementRef.nativeElement;
118146
/** Duration of the dialog open animation. */
119147
private _openAnimationDuration = this._animationsEnabled
120-
? numbers.DIALOG_ANIMATION_OPEN_TIME_MS
148+
? parseCssTime(this._config.enterAnimationDuration) ?? numbers.DIALOG_ANIMATION_OPEN_TIME_MS
121149
: 0;
122150
/** Duration of the dialog close animation. */
123151
private _closeAnimationDuration = this._animationsEnabled
124-
? numbers.DIALOG_ANIMATION_CLOSE_TIME_MS
152+
? parseCssTime(this._config.exitAnimationDuration) ?? numbers.DIALOG_ANIMATION_CLOSE_TIME_MS
125153
: 0;
126154
/** Current timer for dialog animations. */
127155
private _animationTimer: number | null = null;
@@ -181,6 +209,10 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes
181209
if (this._animationsEnabled) {
182210
// One would expect that the open class is added once the animation finished, but MDC
183211
// uses the open class in combination with the opening class to start the animation.
212+
this._hostElement.style.setProperty(
213+
TRANSITION_DURATION_PROPERTY,
214+
`${this._openAnimationDuration}ms`,
215+
);
184216
this._hostElement.classList.add(cssClasses.OPENING);
185217
this._hostElement.classList.add(cssClasses.OPEN);
186218
this._waitForAnimationToComplete(this._openAnimationDuration, this._finishDialogOpen);
@@ -203,6 +235,10 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes
203235
this._hostElement.classList.remove(cssClasses.OPEN);
204236

205237
if (this._animationsEnabled) {
238+
this._hostElement.style.setProperty(
239+
TRANSITION_DURATION_PROPERTY,
240+
`${this._openAnimationDuration}ms`,
241+
);
206242
this._hostElement.classList.add(cssClasses.CLOSING);
207243
this._waitForAnimationToComplete(this._closeAnimationDuration, this._finishDialogClose);
208244
} else {

src/material/dialog/dialog.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ $mat-dialog-button-horizontal-margin: 8px !default;
5656
// The dialog container is focusable. We remove the default outline shown in browsers.
5757
outline: 0;
5858

59+
.mdc-dialog__container {
60+
transition-duration: var(--mat-dialog-transition-duration, 0ms);
61+
}
62+
5963
// Angular Material supports disabling all animations when NoopAnimationsModule is imported.
6064
// TODO(devversion): Look into using MDC's Sass queries to separate the animation styles and
6165
// conditionally add them. Consider the size cost and churn when deciding whether to switch.

src/material/dialog/dialog.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export abstract class _MatDialogBase<C extends _MatDialogContainerBase> implemen
7474
private _scrollStrategy: () => ScrollStrategy;
7575
protected _idPrefix = 'mat-dialog-';
7676
private _dialog: Dialog;
77+
protected dialogConfigClass = MatDialogConfig;
7778

7879
/** Keeps track of the currently-open dialogs. */
7980
get openDialogs(): MatDialogRef<any>[] {
@@ -175,7 +176,7 @@ export abstract class _MatDialogBase<C extends _MatDialogContainerBase> implemen
175176
// Provide our config as the CDK config as well since it has the same interface as the
176177
// CDK one, but it contains the actual values passed in by the user for things like
177178
// `disableClose` which we disable for the CDK dialog since we handle it ourselves.
178-
{provide: MatDialogConfig, useValue: config},
179+
{provide: this.dialogConfigClass, useValue: config},
179180
{provide: DialogConfig, useValue: config},
180181
],
181182
},

src/material/dialog/public-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ export * from './dialog-ref';
1212
export * from './dialog-content-directives';
1313
export * from './dialog-container';
1414
export * from './module';
15-
export {matDialogAnimations} from './dialog-animations';
15+
export {matDialogAnimations, _defaultParams} from './dialog-animations';
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {MatDialogConfig as DialogConfigBase, _defaultParams} from '@angular/material/dialog';
10+
11+
export class MatLegacyDialogConfig<D = any> extends DialogConfigBase<D> {
12+
/** Duration of the enter animation. Has to be a valid CSS value (e.g. 100ms). */
13+
override enterAnimationDuration?: string = _defaultParams.params.enterAnimationDuration;
14+
15+
/** Duration of the exit animation. Has to be a valid CSS value (e.g. 50ms). */
16+
override exitAnimationDuration?: string = _defaultParams.params.exitAnimationDuration;
17+
}

src/material/legacy-dialog/dialog-container.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,8 @@ import {
2121
ViewEncapsulation,
2222
} from '@angular/core';
2323
import {defaultParams} from './dialog-animations';
24-
import {
25-
_MatDialogContainerBase,
26-
MatDialogConfig,
27-
matDialogAnimations,
28-
} from '@angular/material/dialog';
24+
import {MatLegacyDialogConfig} from './dialog-config';
25+
import {_MatDialogContainerBase, matDialogAnimations} from '@angular/material/dialog';
2926

3027
/**
3128
* Internal component that wraps user-provided dialog content.
@@ -90,7 +87,7 @@ export class MatLegacyDialogContainer extends _MatDialogContainerBase {
9087
elementRef: ElementRef,
9188
focusTrapFactory: FocusTrapFactory,
9289
@Optional() @Inject(DOCUMENT) document: any,
93-
dialogConfig: MatDialogConfig,
90+
dialogConfig: MatLegacyDialogConfig,
9491
checker: InteractivityChecker,
9592
ngZone: NgZone,
9693
overlayRef: OverlayRef,

src/material/legacy-dialog/dialog.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,15 @@ import {Location} from '@angular/common';
1111
import {Inject, Injectable, InjectionToken, Injector, Optional, SkipSelf} from '@angular/core';
1212
import {MatLegacyDialogContainer} from './dialog-container';
1313
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
14-
import {_MatDialogBase, MatDialogConfig} from '@angular/material/dialog';
14+
import {_MatDialogBase} from '@angular/material/dialog';
1515
import {MatLegacyDialogRef} from './dialog-ref';
16+
import {MatLegacyDialogConfig} from './dialog-config';
1617

1718
/** Injection token that can be used to access the data that was passed in to a dialog. */
1819
export const MAT_LEGACY_DIALOG_DATA = new InjectionToken<any>('MatDialogData');
1920

2021
/** Injection token that can be used to specify default dialog options. */
21-
export const MAT_LEGACY_DIALOG_DEFAULT_OPTIONS = new InjectionToken<MatDialogConfig>(
22+
export const MAT_LEGACY_DIALOG_DEFAULT_OPTIONS = new InjectionToken<MatLegacyDialogConfig>(
2223
'mat-dialog-default-options',
2324
);
2425

@@ -46,6 +47,8 @@ export const MAT_LEGACY_DIALOG_SCROLL_STRATEGY_PROVIDER = {
4647
*/
4748
@Injectable()
4849
export class MatLegacyDialog extends _MatDialogBase<MatLegacyDialogContainer> {
50+
protected override dialogConfigClass = MatLegacyDialogConfig;
51+
4952
constructor(
5053
overlay: Overlay,
5154
injector: Injector,
@@ -54,7 +57,7 @@ export class MatLegacyDialog extends _MatDialogBase<MatLegacyDialogContainer> {
5457
* @breaking-change 10.0.0
5558
*/
5659
@Optional() _location: Location,
57-
@Optional() @Inject(MAT_LEGACY_DIALOG_DEFAULT_OPTIONS) defaultOptions: MatDialogConfig,
60+
@Optional() @Inject(MAT_LEGACY_DIALOG_DEFAULT_OPTIONS) defaultOptions: MatLegacyDialogConfig,
5861
@Inject(MAT_LEGACY_DIALOG_SCROLL_STRATEGY) scrollStrategy: any,
5962
@Optional() @SkipSelf() parentDialog: MatLegacyDialog,
6063
/**

src/material/legacy-dialog/public-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ export * from './dialog';
1111
export * from './dialog-container';
1212
export * from './dialog-content-directives';
1313
export * from './dialog-ref';
14+
export * from './dialog-config';
1415
export {
1516
_MatDialogBase as _MatLegacyDialogBase,
1617
_MatDialogContainerBase as _MatLegacyDialogContainerBase,
1718
AutoFocusTarget as LegacyAutoFocusTarget,
1819
DialogRole as LegacyDialogRole,
1920
DialogPosition as LegacyDialogPosition,
20-
MatDialogConfig as MatLegacyDialogConfig,
2121
_closeDialogVia as _closeLegacyDialogVia,
2222
MatDialogState as MatLegacyDialogState,
2323
matDialogAnimations as matLegacyDialogAnimations,

tools/public_api_guard/material/dialog.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ export type AutoFocusTarget = 'dialog' | 'first-tabbable' | 'first-heading';
4545
// @public
4646
export function _closeDialogVia<R>(ref: MatDialogRef<R>, interactionType: FocusOrigin, result?: R): void;
4747

48+
// @public
49+
export const _defaultParams: {
50+
params: {
51+
enterAnimationDuration: string;
52+
exitAnimationDuration: string;
53+
};
54+
};
55+
4856
// @public
4957
export interface DialogPosition {
5058
bottom?: string;
@@ -112,6 +120,8 @@ export abstract class _MatDialogBase<C extends _MatDialogContainerBase> implemen
112120
readonly afterAllClosed: Observable<void>;
113121
get afterOpened(): Subject<MatDialogRef<any>>;
114122
closeAll(): void;
123+
// (undocumented)
124+
protected dialogConfigClass: typeof MatDialogConfig;
115125
getDialogById(id: string): MatDialogRef<any> | undefined;
116126
// (undocumented)
117127
protected _idPrefix: string;
@@ -163,8 +173,8 @@ export class MatDialogConfig<D = any> {
163173
delayFocusTrap?: boolean;
164174
direction?: Direction;
165175
disableClose?: boolean;
166-
enterAnimationDuration?: string;
167-
exitAnimationDuration?: string;
176+
enterAnimationDuration?: string | number;
177+
exitAnimationDuration?: string | number;
168178
hasBackdrop?: boolean;
169179
height?: string;
170180
id?: string;

tools/public_api_guard/material/legacy-dialog.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ import { DialogPosition as LegacyDialogPosition } from '@angular/material/dialog
2323
import { DialogRole as LegacyDialogRole } from '@angular/material/dialog';
2424
import { Location as Location_2 } from '@angular/common';
2525
import { MAT_DIALOG_SCROLL_STRATEGY_FACTORY as MAT_LEGACY_DIALOG_SCROLL_STRATEGY_FACTORY } from '@angular/material/dialog';
26+
import { MatDialogConfig } from '@angular/material/dialog';
2627
import { MatDialogRef } from '@angular/material/dialog';
2728
import { matDialogAnimations as matLegacyDialogAnimations } from '@angular/material/dialog';
2829
import { _MatDialogBase as _MatLegacyDialogBase } from '@angular/material/dialog';
29-
import { MatDialogConfig as MatLegacyDialogConfig } from '@angular/material/dialog';
3030
import { _MatDialogContainerBase as _MatLegacyDialogContainerBase } from '@angular/material/dialog';
3131
import { MatDialogState as MatLegacyDialogState } from '@angular/material/dialog';
3232
import { NgZone } from '@angular/core';
@@ -74,6 +74,8 @@ export class MatLegacyDialog extends _MatLegacyDialogBase<MatLegacyDialogContain
7474
overlayContainer: OverlayContainer,
7575
animationMode?: 'NoopAnimations' | 'BrowserAnimations');
7676
// (undocumented)
77+
protected dialogConfigClass: typeof MatLegacyDialogConfig;
78+
// (undocumented)
7779
static ɵfac: i0.ɵɵFactoryDeclaration<MatLegacyDialog, [null, null, { optional: true; }, { optional: true; }, null, { optional: true; skipSelf: true; }, null, { optional: true; }]>;
7880
// (undocumented)
7981
static ɵprov: i0.ɵɵInjectableDeclaration<MatLegacyDialog>;
@@ -115,7 +117,11 @@ export class MatLegacyDialogClose implements OnInit, OnChanges {
115117
static ɵfac: i0.ɵɵFactoryDeclaration<MatLegacyDialogClose, [{ optional: true; }, null, null]>;
116118
}
117119

118-
export { MatLegacyDialogConfig }
120+
// @public (undocumented)
121+
export class MatLegacyDialogConfig<D = any> extends MatDialogConfig<D> {
122+
enterAnimationDuration?: string;
123+
exitAnimationDuration?: string;
124+
}
119125

120126
// @public
121127
export class MatLegacyDialogContainer extends _MatLegacyDialogContainerBase {
@@ -124,8 +130,8 @@ export class MatLegacyDialogContainer extends _MatLegacyDialogContainerBase {
124130
_getAnimationState(): {
125131
value: "void" | "enter" | "exit";
126132
params: {
127-
enterAnimationDuration: string;
128-
exitAnimationDuration: string;
133+
enterAnimationDuration: string | number;
134+
exitAnimationDuration: string | number;
129135
};
130136
};
131137
_onAnimationDone({ toState, totalTime }: AnimationEvent_2): void;

0 commit comments

Comments
 (0)