Skip to content

Commit

Permalink
fix(material/dialog): allow customizing animation duration (#25524)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mmalerba committed Aug 29, 2022
1 parent 7faafb4 commit 4cdc095
Show file tree
Hide file tree
Showing 12 changed files with 110 additions and 28 deletions.
6 changes: 3 additions & 3 deletions src/material/dialog/dialog-animations.ts
Expand Up @@ -21,7 +21,7 @@ import {
* Default parameters for the animation for backwards compatibility.
* @docs-private
*/
export const defaultParams = {
export const _defaultParams = {
params: {enterAnimationDuration: '150ms', exitAnimationDuration: '75ms'},
};

Expand All @@ -48,15 +48,15 @@ export const matDialogAnimations: {
),
query('@*', animateChild(), {optional: true}),
]),
defaultParams,
_defaultParams,
),
transition(
'* => void, * => exit',
group([
animate('{{exitAnimationDuration}} cubic-bezier(0.4, 0.0, 0.2, 1)', style({opacity: 0})),
query('@*', animateChild(), {optional: true}),
]),
defaultParams,
_defaultParams,
),
]),
};
18 changes: 13 additions & 5 deletions src/material/dialog/dialog-config.ts
Expand Up @@ -9,7 +9,7 @@
import {ViewContainerRef, ComponentFactoryResolver, Injector} from '@angular/core';
import {Direction} from '@angular/cdk/bidi';
import {ScrollStrategy} from '@angular/cdk/overlay';
import {defaultParams} from './dialog-animations';
import {_defaultParams} from './dialog-animations';

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

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

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

// TODO(jelbourn): add configuration for lifecycle hooks, ARIA labelling.
}
40 changes: 38 additions & 2 deletions src/material/dialog/dialog-container.ts
Expand Up @@ -24,6 +24,7 @@ import {MatDialogConfig} from './dialog-config';
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
import {cssClasses, numbers} from '@material/dialog';
import {CdkDialogContainer} from '@angular/cdk/dialog';
import {coerceNumberProperty} from '@angular/cdk/coercion';

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

const TRANSITION_DURATION_PROPERTY = '--mat-dialog-transition-duration';

// TODO(mmalerba): Remove this function after animation durations are required
// to be numbers.
/**
* Converts a CSS time string to a number in ms. If the given time is already a
* number, it is assumed to be in ms.
*/
function parseCssTime(time: string | number | undefined): number | null {
if (time == null) {
return null;
}
if (typeof time === 'number') {
return time;
}
if (time.endsWith('ms')) {
return coerceNumberProperty(time.substring(0, time.length - 2));
}
if (time.endsWith('s')) {
return coerceNumberProperty(time.substring(0, time.length - 1)) * 1000;
}
if (time === '0') {
return 0;
}
return null; // anything else is invalid.
}

/**
* Internal component that wraps user-provided dialog content in a MDC dialog.
* @docs-private
Expand Down Expand Up @@ -117,11 +145,11 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes
private _hostElement: HTMLElement = this._elementRef.nativeElement;
/** Duration of the dialog open animation. */
private _openAnimationDuration = this._animationsEnabled
? numbers.DIALOG_ANIMATION_OPEN_TIME_MS
? parseCssTime(this._config.enterAnimationDuration) ?? numbers.DIALOG_ANIMATION_OPEN_TIME_MS
: 0;
/** Duration of the dialog close animation. */
private _closeAnimationDuration = this._animationsEnabled
? numbers.DIALOG_ANIMATION_CLOSE_TIME_MS
? parseCssTime(this._config.exitAnimationDuration) ?? numbers.DIALOG_ANIMATION_CLOSE_TIME_MS
: 0;
/** Current timer for dialog animations. */
private _animationTimer: number | null = null;
Expand Down Expand Up @@ -181,6 +209,10 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes
if (this._animationsEnabled) {
// One would expect that the open class is added once the animation finished, but MDC
// uses the open class in combination with the opening class to start the animation.
this._hostElement.style.setProperty(
TRANSITION_DURATION_PROPERTY,
`${this._openAnimationDuration}ms`,
);
this._hostElement.classList.add(cssClasses.OPENING);
this._hostElement.classList.add(cssClasses.OPEN);
this._waitForAnimationToComplete(this._openAnimationDuration, this._finishDialogOpen);
Expand All @@ -203,6 +235,10 @@ export class MatDialogContainer extends _MatDialogContainerBase implements OnDes
this._hostElement.classList.remove(cssClasses.OPEN);

if (this._animationsEnabled) {
this._hostElement.style.setProperty(
TRANSITION_DURATION_PROPERTY,
`${this._openAnimationDuration}ms`,
);
this._hostElement.classList.add(cssClasses.CLOSING);
this._waitForAnimationToComplete(this._closeAnimationDuration, this._finishDialogClose);
} else {
Expand Down
4 changes: 4 additions & 0 deletions src/material/dialog/dialog.scss
Expand Up @@ -56,6 +56,10 @@ $mat-dialog-button-horizontal-margin: 8px !default;
// The dialog container is focusable. We remove the default outline shown in browsers.
outline: 0;

.mdc-dialog__container {
transition-duration: var(--mat-dialog-transition-duration, 0ms);
}

// Angular Material supports disabling all animations when NoopAnimationsModule is imported.
// TODO(devversion): Look into using MDC's Sass queries to separate the animation styles and
// conditionally add them. Consider the size cost and churn when deciding whether to switch.
Expand Down
3 changes: 2 additions & 1 deletion src/material/dialog/dialog.ts
Expand Up @@ -74,6 +74,7 @@ export abstract class _MatDialogBase<C extends _MatDialogContainerBase> implemen
private _scrollStrategy: () => ScrollStrategy;
protected _idPrefix = 'mat-dialog-';
private _dialog: Dialog;
protected dialogConfigClass = MatDialogConfig;

/** Keeps track of the currently-open dialogs. */
get openDialogs(): MatDialogRef<any>[] {
Expand Down Expand Up @@ -175,7 +176,7 @@ export abstract class _MatDialogBase<C extends _MatDialogContainerBase> implemen
// Provide our config as the CDK config as well since it has the same interface as the
// CDK one, but it contains the actual values passed in by the user for things like
// `disableClose` which we disable for the CDK dialog since we handle it ourselves.
{provide: MatDialogConfig, useValue: config},
{provide: this.dialogConfigClass, useValue: config},
{provide: DialogConfig, useValue: config},
],
},
Expand Down
2 changes: 1 addition & 1 deletion src/material/dialog/public-api.ts
Expand Up @@ -12,4 +12,4 @@ export * from './dialog-ref';
export * from './dialog-content-directives';
export * from './dialog-container';
export * from './module';
export {matDialogAnimations} from './dialog-animations';
export {matDialogAnimations, _defaultParams} from './dialog-animations';
17 changes: 17 additions & 0 deletions src/material/legacy-dialog/dialog-config.ts
@@ -0,0 +1,17 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {MatDialogConfig as DialogConfigBase, _defaultParams} from '@angular/material/dialog';

export class MatLegacyDialogConfig<D = any> extends DialogConfigBase<D> {
/** Duration of the enter animation. Has to be a valid CSS value (e.g. 100ms). */
override enterAnimationDuration?: string = _defaultParams.params.enterAnimationDuration;

/** Duration of the exit animation. Has to be a valid CSS value (e.g. 50ms). */
override exitAnimationDuration?: string = _defaultParams.params.exitAnimationDuration;
}
9 changes: 3 additions & 6 deletions src/material/legacy-dialog/dialog-container.ts
Expand Up @@ -21,11 +21,8 @@ import {
ViewEncapsulation,
} from '@angular/core';
import {defaultParams} from './dialog-animations';
import {
_MatDialogContainerBase,
MatDialogConfig,
matDialogAnimations,
} from '@angular/material/dialog';
import {MatLegacyDialogConfig} from './dialog-config';
import {_MatDialogContainerBase, matDialogAnimations} from '@angular/material/dialog';

/**
* Internal component that wraps user-provided dialog content.
Expand Down Expand Up @@ -90,7 +87,7 @@ export class MatLegacyDialogContainer extends _MatDialogContainerBase {
elementRef: ElementRef,
focusTrapFactory: FocusTrapFactory,
@Optional() @Inject(DOCUMENT) document: any,
dialogConfig: MatDialogConfig,
dialogConfig: MatLegacyDialogConfig,
checker: InteractivityChecker,
ngZone: NgZone,
overlayRef: OverlayRef,
Expand Down
9 changes: 6 additions & 3 deletions src/material/legacy-dialog/dialog.ts
Expand Up @@ -11,14 +11,15 @@ import {Location} from '@angular/common';
import {Inject, Injectable, InjectionToken, Injector, Optional, SkipSelf} from '@angular/core';
import {MatLegacyDialogContainer} from './dialog-container';
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
import {_MatDialogBase, MatDialogConfig} from '@angular/material/dialog';
import {_MatDialogBase} from '@angular/material/dialog';
import {MatLegacyDialogRef} from './dialog-ref';
import {MatLegacyDialogConfig} from './dialog-config';

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

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

Expand Down Expand Up @@ -46,6 +47,8 @@ export const MAT_LEGACY_DIALOG_SCROLL_STRATEGY_PROVIDER = {
*/
@Injectable()
export class MatLegacyDialog extends _MatDialogBase<MatLegacyDialogContainer> {
protected override dialogConfigClass = MatLegacyDialogConfig;

constructor(
overlay: Overlay,
injector: Injector,
Expand All @@ -54,7 +57,7 @@ export class MatLegacyDialog extends _MatDialogBase<MatLegacyDialogContainer> {
* @breaking-change 10.0.0
*/
@Optional() _location: Location,
@Optional() @Inject(MAT_LEGACY_DIALOG_DEFAULT_OPTIONS) defaultOptions: MatDialogConfig,
@Optional() @Inject(MAT_LEGACY_DIALOG_DEFAULT_OPTIONS) defaultOptions: MatLegacyDialogConfig,
@Inject(MAT_LEGACY_DIALOG_SCROLL_STRATEGY) scrollStrategy: any,
@Optional() @SkipSelf() parentDialog: MatLegacyDialog,
/**
Expand Down
2 changes: 1 addition & 1 deletion src/material/legacy-dialog/public-api.ts
Expand Up @@ -11,13 +11,13 @@ export * from './dialog';
export * from './dialog-container';
export * from './dialog-content-directives';
export * from './dialog-ref';
export * from './dialog-config';
export {
_MatDialogBase as _MatLegacyDialogBase,
_MatDialogContainerBase as _MatLegacyDialogContainerBase,
AutoFocusTarget as LegacyAutoFocusTarget,
DialogRole as LegacyDialogRole,
DialogPosition as LegacyDialogPosition,
MatDialogConfig as MatLegacyDialogConfig,
_closeDialogVia as _closeLegacyDialogVia,
MatDialogState as MatLegacyDialogState,
matDialogAnimations as matLegacyDialogAnimations,
Expand Down
14 changes: 12 additions & 2 deletions tools/public_api_guard/material/dialog.md
Expand Up @@ -45,6 +45,14 @@ export type AutoFocusTarget = 'dialog' | 'first-tabbable' | 'first-heading';
// @public
export function _closeDialogVia<R>(ref: MatDialogRef<R>, interactionType: FocusOrigin, result?: R): void;

// @public
export const _defaultParams: {
params: {
enterAnimationDuration: string;
exitAnimationDuration: string;
};
};

// @public
export interface DialogPosition {
bottom?: string;
Expand Down Expand Up @@ -112,6 +120,8 @@ export abstract class _MatDialogBase<C extends _MatDialogContainerBase> implemen
readonly afterAllClosed: Observable<void>;
get afterOpened(): Subject<MatDialogRef<any>>;
closeAll(): void;
// (undocumented)
protected dialogConfigClass: typeof MatDialogConfig;
getDialogById(id: string): MatDialogRef<any> | undefined;
// (undocumented)
protected _idPrefix: string;
Expand Down Expand Up @@ -163,8 +173,8 @@ export class MatDialogConfig<D = any> {
delayFocusTrap?: boolean;
direction?: Direction;
disableClose?: boolean;
enterAnimationDuration?: string;
exitAnimationDuration?: string;
enterAnimationDuration?: string | number;
exitAnimationDuration?: string | number;
hasBackdrop?: boolean;
height?: string;
id?: string;
Expand Down
14 changes: 10 additions & 4 deletions tools/public_api_guard/material/legacy-dialog.md
Expand Up @@ -23,10 +23,10 @@ import { DialogPosition as LegacyDialogPosition } from '@angular/material/dialog
import { DialogRole as LegacyDialogRole } from '@angular/material/dialog';
import { Location as Location_2 } from '@angular/common';
import { MAT_DIALOG_SCROLL_STRATEGY_FACTORY as MAT_LEGACY_DIALOG_SCROLL_STRATEGY_FACTORY } from '@angular/material/dialog';
import { MatDialogConfig } from '@angular/material/dialog';
import { MatDialogRef } from '@angular/material/dialog';
import { matDialogAnimations as matLegacyDialogAnimations } from '@angular/material/dialog';
import { _MatDialogBase as _MatLegacyDialogBase } from '@angular/material/dialog';
import { MatDialogConfig as MatLegacyDialogConfig } from '@angular/material/dialog';
import { _MatDialogContainerBase as _MatLegacyDialogContainerBase } from '@angular/material/dialog';
import { MatDialogState as MatLegacyDialogState } from '@angular/material/dialog';
import { NgZone } from '@angular/core';
Expand Down Expand Up @@ -74,6 +74,8 @@ export class MatLegacyDialog extends _MatLegacyDialogBase<MatLegacyDialogContain
overlayContainer: OverlayContainer,
animationMode?: 'NoopAnimations' | 'BrowserAnimations');
// (undocumented)
protected dialogConfigClass: typeof MatLegacyDialogConfig;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<MatLegacyDialog, [null, null, { optional: true; }, { optional: true; }, null, { optional: true; skipSelf: true; }, null, { optional: true; }]>;
// (undocumented)
static ɵprov: i0.ɵɵInjectableDeclaration<MatLegacyDialog>;
Expand Down Expand Up @@ -115,7 +117,11 @@ export class MatLegacyDialogClose implements OnInit, OnChanges {
static ɵfac: i0.ɵɵFactoryDeclaration<MatLegacyDialogClose, [{ optional: true; }, null, null]>;
}

export { MatLegacyDialogConfig }
// @public (undocumented)
export class MatLegacyDialogConfig<D = any> extends MatDialogConfig<D> {
enterAnimationDuration?: string;
exitAnimationDuration?: string;
}

// @public
export class MatLegacyDialogContainer extends _MatLegacyDialogContainerBase {
Expand All @@ -124,8 +130,8 @@ export class MatLegacyDialogContainer extends _MatLegacyDialogContainerBase {
_getAnimationState(): {
value: "void" | "enter" | "exit";
params: {
enterAnimationDuration: string;
exitAnimationDuration: string;
enterAnimationDuration: string | number;
exitAnimationDuration: string | number;
};
};
_onAnimationDone({ toState, totalTime }: AnimationEvent_2): void;
Expand Down

0 comments on commit 4cdc095

Please sign in to comment.