diff --git a/goldens/material/core/index.api.md b/goldens/material/core/index.api.md index 1d16f9ab5ec9..cc5512058715 100644 --- a/goldens/material/core/index.api.md +++ b/goldens/material/core/index.api.md @@ -132,6 +132,9 @@ export class _ErrorStateTracker { updateErrorState(): void; } +// @public +export function _getAnimationsState(): 'enabled' | 'di-disabled' | 'reduced-motion'; + // @public export function _getOptionScrollPosition(optionOffset: number, optionHeight: number, currentScrollPosition: number, panelHeight: number): number; diff --git a/src/material/core/animation/animation.ts b/src/material/core/animation/animation.ts index c228579047ab..aa20e2d8f919 100644 --- a/src/material/core/animation/animation.ts +++ b/src/material/core/animation/animation.ts @@ -41,18 +41,28 @@ export class AnimationDurations { static EXITING = '195ms'; } +let reducedMotion: boolean | null = null; + /** - * Returns whether animations have been disabled by DI. Must be called in a DI context. + * Gets the the configured animations state. * @docs-private */ -export function _animationsDisabled(): boolean { +export function _getAnimationsState(): 'enabled' | 'di-disabled' | 'reduced-motion' { if ( inject(MATERIAL_ANIMATIONS, {optional: true})?.animationsDisabled || inject(ANIMATION_MODULE_TYPE, {optional: true}) === 'NoopAnimations' ) { - return true; + return 'di-disabled'; } - const mediaMatcher = inject(MediaMatcher); - return mediaMatcher.matchMedia('(prefers-reduced-motion)').matches; + reducedMotion ??= inject(MediaMatcher).matchMedia('(prefers-reduced-motion)').matches; + return reducedMotion ? 'reduced-motion' : 'enabled'; +} + +/** + * Returns whether animations have been disabled by DI. Must be called in a DI context. + * @docs-private + */ +export function _animationsDisabled(): boolean { + return _getAnimationsState() !== 'enabled'; } diff --git a/src/material/progress-bar/progress-bar.scss b/src/material/progress-bar/progress-bar.scss index 228019f64469..235cb73664da 100644 --- a/src/material/progress-bar/progress-bar.scss +++ b/src/material/progress-bar/progress-bar.scss @@ -6,6 +6,8 @@ $fallbacks: m3-progress-bar.get-tokens(); .mat-mdc-progress-bar { + --mat-progress-bar-animation-multiplier: 1; + // Explicitly set to `block` since the browser defaults custom elements to `inline`. display: block; @@ -35,6 +37,12 @@ $fallbacks: m3-progress-bar.get-tokens(); } } +// Slow down the animation by 100% when the user configured their OS to reduce +// motion since some animations like the indeterminate one can be quite dynamic. +.mat-progress-bar-reduced-motion { + --mat-progress-bar-animation-multiplier: 2; +} + .mdc-linear-progress { position: relative; width: 100%; @@ -105,7 +113,8 @@ $fallbacks: m3-progress-bar.get-tokens(); background-repeat: repeat-x; flex: auto; transform: rotate(180deg); - animation: mdc-linear-progress-buffering 250ms infinite linear; + animation: mdc-linear-progress-buffering + calc(250ms * var(--mat-progress-bar-animation-multiplier)) infinite linear; background-color: token-utils.slot(progress-bar-track-color, $fallbacks); @include cdk.high-contrast { @@ -113,7 +122,8 @@ $fallbacks: m3-progress-bar.get-tokens(); } [dir='rtl'] & { - animation: mdc-linear-progress-buffering-reverse 250ms infinite linear; + animation: mdc-linear-progress-buffering-reverse + calc(250ms * var(--mat-progress-bar-animation-multiplier)) infinite linear; transform: rotate(0); } } @@ -132,12 +142,14 @@ $fallbacks: m3-progress-bar.get-tokens(); } .mdc-linear-progress--indeterminate.mdc-linear-progress--animation-ready & { - animation: mdc-linear-progress-primary-indeterminate-translate 2s infinite linear; + animation: mdc-linear-progress-primary-indeterminate-translate + calc(2s * var(--mat-progress-bar-animation-multiplier)) infinite linear; } .mdc-linear-progress--indeterminate.mdc-linear-progress--animation-ready & { > .mdc-linear-progress__bar-inner { - animation: mdc-linear-progress-primary-indeterminate-scale 2s infinite linear; + animation: mdc-linear-progress-primary-indeterminate-scale + calc(2s * var(--mat-progress-bar-animation-multiplier)) infinite linear; } } @@ -160,12 +172,14 @@ $fallbacks: m3-progress-bar.get-tokens(); } .mdc-linear-progress--indeterminate.mdc-linear-progress--animation-ready & { - animation: mdc-linear-progress-secondary-indeterminate-translate 2s infinite linear; + animation: mdc-linear-progress-secondary-indeterminate-translate + calc(2s * var(--mat-progress-bar-animation-multiplier)) infinite linear; } .mdc-linear-progress--indeterminate.mdc-linear-progress--animation-ready & { > .mdc-linear-progress__bar-inner { - animation: mdc-linear-progress-secondary-indeterminate-scale 2s infinite linear; + animation: mdc-linear-progress-secondary-indeterminate-scale + calc(2s * var(--mat-progress-bar-animation-multiplier)) infinite linear; } } diff --git a/src/material/progress-bar/progress-bar.ts b/src/material/progress-bar/progress-bar.ts index 5c94e128205c..190eeac642ff 100644 --- a/src/material/progress-bar/progress-bar.ts +++ b/src/material/progress-bar/progress-bar.ts @@ -25,7 +25,7 @@ import { DOCUMENT, } from '@angular/core'; -import {_animationsDisabled, ThemePalette} from '../core'; +import {_getAnimationsState, ThemePalette} from '../core'; /** Last animation end data. */ export interface ProgressAnimationEnd { @@ -121,10 +121,18 @@ export class MatProgressBar implements AfterViewInit, OnDestroy { constructor(...args: unknown[]); constructor() { + const animationsState = _getAnimationsState(); + const defaults = inject(MAT_PROGRESS_BAR_DEFAULT_OPTIONS, { optional: true, }); + this._isNoopAnimation = animationsState === 'di-disabled'; + + if (animationsState === 'reduced-motion') { + this._elementRef.nativeElement.classList.add('mat-progress-bar-reduced-motion'); + } + if (defaults) { if (defaults.color) { this.color = this._defaultColor = defaults.color; @@ -135,7 +143,7 @@ export class MatProgressBar implements AfterViewInit, OnDestroy { } /** Flag that indicates whether NoopAnimations mode is set to true. */ - _isNoopAnimation = _animationsDisabled(); + _isNoopAnimation: boolean; // TODO: should be typed as `ThemePalette` but internal apps pass in arbitrary strings. /** diff --git a/src/material/progress-spinner/progress-spinner.scss b/src/material/progress-spinner/progress-spinner.scss index b0992804873f..e27428fdf46d 100644 --- a/src/material/progress-spinner/progress-spinner.scss +++ b/src/material/progress-spinner/progress-spinner.scss @@ -5,6 +5,8 @@ $fallbacks: m3-progress-spinner.get-tokens(); .mat-mdc-progress-spinner { + --mat-progress-spinner-animation-multiplier: 1; + // Explicitly set to `block` since the browser defaults custom elements to `inline`. display: block; @@ -54,6 +56,11 @@ $fallbacks: m3-progress-spinner.get-tokens(); } } +// Slow down the animation by 25% when the user configured their OS to reduce motion. +.mat-progress-spinner-reduced-motion { + --mat-progress-spinner-animation-multiplier: 1.25; +} + .mdc-circular-progress__determinate-container, .mdc-circular-progress__indeterminate-circle-graphic, .mdc-circular-progress__indeterminate-container, @@ -79,7 +86,8 @@ $fallbacks: m3-progress-spinner.get-tokens(); .mdc-circular-progress--indeterminate & { opacity: 1; - animation: mdc-circular-progress-container-rotate 1568.2352941176ms linear infinite; + animation: mdc-circular-progress-container-rotate + calc(1568.2352941176ms * var(--mat-progress-spinner-animation-multiplier)) linear infinite; } } @@ -127,11 +135,15 @@ $fallbacks: m3-progress-spinner.get-tokens(); } .mdc-circular-progress--indeterminate .mdc-circular-progress__circle-left & { - animation: mdc-circular-progress-left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: mdc-circular-progress-left-spin + calc(1333ms * var(--mat-progress-spinner-animation-multiplier)) + cubic-bezier(0.4, 0, 0.2, 1) infinite both; } .mdc-circular-progress--indeterminate .mdc-circular-progress__circle-right & { - animation: mdc-circular-progress-right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: mdc-circular-progress-right-spin + calc(1333ms * var(--mat-progress-spinner-animation-multiplier)) + cubic-bezier(0.4, 0, 0.2, 1) infinite both; } } @@ -145,7 +157,8 @@ $fallbacks: m3-progress-spinner.get-tokens(); .mdc-circular-progress__spinner-layer { .mdc-circular-progress--indeterminate & { - animation: mdc-circular-progress-spinner-layer-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) + animation: mdc-circular-progress-spinner-layer-rotate + calc(5332ms * var(--mat-progress-spinner-animation-multiplier)) cubic-bezier(0.4, 0, 0.2, 1) infinite both; } } diff --git a/src/material/progress-spinner/progress-spinner.ts b/src/material/progress-spinner/progress-spinner.ts index d24223ead811..ecec729e465a 100644 --- a/src/material/progress-spinner/progress-spinner.ts +++ b/src/material/progress-spinner/progress-spinner.ts @@ -17,7 +17,7 @@ import { numberAttribute, inject, } from '@angular/core'; -import {_animationsDisabled, ThemePalette} from '../core'; +import {_getAnimationsState, ThemePalette} from '../core'; import {NgTemplateOutlet} from '@angular/common'; /** Possible mode for a progress spinner. */ @@ -128,12 +128,16 @@ export class MatProgressSpinner { constructor() { const defaults = inject(MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS); + const animationsState = _getAnimationsState(); + const element = this._elementRef.nativeElement; - this._noopAnimations = _animationsDisabled() && !!defaults && !defaults._forceAnimations; - this.mode = - this._elementRef.nativeElement.nodeName.toLowerCase() === 'mat-spinner' - ? 'indeterminate' - : 'determinate'; + this._noopAnimations = + animationsState === 'di-disabled' && !!defaults && !defaults._forceAnimations; + this.mode = element.nodeName.toLowerCase() === 'mat-spinner' ? 'indeterminate' : 'determinate'; + + if (!this._noopAnimations && animationsState === 'reduced-motion') { + element.classList.add('mat-progress-spinner-reduced-motion'); + } if (defaults) { if (defaults.color) {