Skip to content

Commit 653e46b

Browse files
authored
fix(material/progress-spinner): animation not working on some zoom levels in Safari (#23674)
Fixes that the progress spinner animation was broken on Safari on non-default zoom levels. The problem seems to be that the `transform-origin` was being offset by the amount of zoom. These changes work around it by setting the origin based on the zoom level. Fixes #23668.
1 parent 652f1e3 commit 653e46b

File tree

5 files changed

+87
-16
lines changed

5 files changed

+87
-16
lines changed

src/material/progress-spinner/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ ng_module(
2222
deps = [
2323
"//src/cdk/coercion",
2424
"//src/cdk/platform",
25+
"//src/cdk/scrolling",
2526
"//src/material/core",
2627
"@npm//@angular/animations",
2728
"@npm//@angular/common",

src/material/progress-spinner/progress-spinner.html

+6-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
preserveAspectRatio="xMidYMid meet"
1515
focusable="false"
1616
[ngSwitch]="mode === 'indeterminate'"
17-
aria-hidden="true">
17+
aria-hidden="true"
18+
#svg>
1819

1920
<!--
2021
Technically we can reuse the same `circle` element, however Safari has an issue that breaks
@@ -31,7 +32,8 @@
3132
[style.animation-name]="'mat-progress-spinner-stroke-rotate-' + _spinnerAnimationLabel"
3233
[style.stroke-dashoffset.px]="_getStrokeDashOffset()"
3334
[style.stroke-dasharray.px]="_getStrokeCircumference()"
34-
[style.stroke-width.%]="_getCircleStrokeWidth()"></circle>
35+
[style.stroke-width.%]="_getCircleStrokeWidth()"
36+
[style.transform-origin]="_getCircleTransformOrigin(svg)"></circle>
3537

3638
<circle
3739
*ngSwitchCase="false"
@@ -40,5 +42,6 @@
4042
[attr.r]="_getCircleRadius()"
4143
[style.stroke-dashoffset.px]="_getStrokeDashOffset()"
4244
[style.stroke-dasharray.px]="_getStrokeCircumference()"
43-
[style.stroke-width.%]="_getCircleStrokeWidth()"></circle>
45+
[style.stroke-width.%]="_getCircleStrokeWidth()"
46+
[style.transform-origin]="_getCircleTransformOrigin(svg)"></circle>
4447
</svg>

src/material/progress-spinner/progress-spinner.scss

-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ $_default-circumference: variables.$pi * $_default-radius * 2;
2626
circle {
2727
@include private.private-animation-noop();
2828
fill: transparent;
29-
transform-origin: center;
3029
transition: stroke-dashoffset 225ms linear;
3130

3231
@include a11y.high-contrast(active, off) {

src/material/progress-spinner/progress-spinner.ts

+66-6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {coerceNumberProperty, NumberInput} from '@angular/cdk/coercion';
1010
import {Platform, _getShadowRoot} from '@angular/cdk/platform';
11+
import {ViewportRuler} from '@angular/cdk/scrolling';
1112
import {DOCUMENT} from '@angular/common';
1213
import {
1314
ChangeDetectionStrategy,
@@ -19,9 +20,13 @@ import {
1920
Optional,
2021
ViewEncapsulation,
2122
OnInit,
23+
ChangeDetectorRef,
24+
OnDestroy,
25+
NgZone,
2226
} from '@angular/core';
2327
import {CanColor, mixinColor} from '@angular/material/core';
2428
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
29+
import {Subscription} from 'rxjs';
2530

2631
/** Possible mode for a progress spinner. */
2732
export type ProgressSpinnerMode = 'determinate' | 'indeterminate';
@@ -126,10 +131,14 @@ const INDETERMINATE_ANIMATION_TEMPLATE = `
126131
changeDetection: ChangeDetectionStrategy.OnPush,
127132
encapsulation: ViewEncapsulation.None,
128133
})
129-
export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnInit, CanColor {
134+
export class MatProgressSpinner
135+
extends _MatProgressSpinnerBase
136+
implements OnInit, OnDestroy, CanColor
137+
{
130138
private _diameter = BASE_SIZE;
131139
private _value = 0;
132140
private _strokeWidth: number;
141+
private _resizeSubscription = Subscription.EMPTY;
133142

134143
/**
135144
* Element to which we should add the generated style tags for the indeterminate animation.
@@ -190,15 +199,19 @@ export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnIni
190199

191200
constructor(
192201
elementRef: ElementRef<HTMLElement>,
193-
/**
194-
* @deprecated `_platform` parameter no longer being used.
195-
* @breaking-change 14.0.0
196-
*/
197202
_platform: Platform,
198203
@Optional() @Inject(DOCUMENT) private _document: any,
199204
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode: string,
200205
@Inject(MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS)
201206
defaults?: MatProgressSpinnerDefaultOptions,
207+
/**
208+
* @deprecated `changeDetectorRef`, `viewportRuler` and `ngZone`
209+
* parameters to become required.
210+
* @breaking-change 14.0.0
211+
*/
212+
changeDetectorRef?: ChangeDetectorRef,
213+
viewportRuler?: ViewportRuler,
214+
ngZone?: NgZone,
202215
) {
203216
super(elementRef);
204217

@@ -223,6 +236,22 @@ export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnIni
223236
this.strokeWidth = defaults.strokeWidth;
224237
}
225238
}
239+
240+
// Safari has an issue where the circle isn't positioned correctly when the page has a
241+
// different zoom level from the default. This handler triggers a recalculation of the
242+
// `transform-origin` when the page zoom level changes.
243+
// See `_getCircleTransformOrigin` for more info.
244+
// @breaking-change 14.0.0 Remove null checks for `_changeDetectorRef`,
245+
// `viewportRuler` and `ngZone`.
246+
if (_platform.isBrowser && _platform.SAFARI && viewportRuler && changeDetectorRef && ngZone) {
247+
this._resizeSubscription = viewportRuler.change(150).subscribe(() => {
248+
// When the window is resize while the spinner is in `indeterminate` mode, we
249+
// have to mark for check so the transform origin of the circle can be recomputed.
250+
if (this.mode === 'indeterminate') {
251+
ngZone.run(() => changeDetectorRef.markForCheck());
252+
}
253+
});
254+
}
226255
}
227256

228257
ngOnInit() {
@@ -236,6 +265,10 @@ export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnIni
236265
element.classList.add('mat-progress-spinner-indeterminate-animation');
237266
}
238267

268+
ngOnDestroy() {
269+
this._resizeSubscription.unsubscribe();
270+
}
271+
239272
/** The radius of the spinner, adjusted for stroke width. */
240273
_getCircleRadius() {
241274
return (this.diameter - BASE_STROKE_WIDTH) / 2;
@@ -266,6 +299,16 @@ export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnIni
266299
return (this.strokeWidth / this.diameter) * 100;
267300
}
268301

302+
/** Gets the `transform-origin` for the inner circle element. */
303+
_getCircleTransformOrigin(svg: HTMLElement): string {
304+
// Safari has an issue where the `transform-origin` doesn't work as expected when the page
305+
// has a different zoom level from the default. The problem appears to be that a zoom
306+
// is applied on the `svg` node itself. We can work around it by calculating the origin
307+
// based on the zoom level. On all other browsers the `currentScale` appears to always be 1.
308+
const scale = ((svg as unknown as SVGSVGElement).currentScale ?? 1) * 50;
309+
return `${scale}% ${scale}%`;
310+
}
311+
269312
/** Dynamically generates a style tag containing the correct animation for this diameter. */
270313
private _attachStyleNode(): void {
271314
const styleRoot = this._styleRoot;
@@ -338,8 +381,25 @@ export class MatSpinner extends MatProgressSpinner {
338381
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode: string,
339382
@Inject(MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS)
340383
defaults?: MatProgressSpinnerDefaultOptions,
384+
/**
385+
* @deprecated `changeDetectorRef`, `viewportRuler` and `ngZone`
386+
* parameters to become required.
387+
* @breaking-change 14.0.0
388+
*/
389+
changeDetectorRef?: ChangeDetectorRef,
390+
viewportRuler?: ViewportRuler,
391+
ngZone?: NgZone,
341392
) {
342-
super(elementRef, platform, document, animationMode, defaults);
393+
super(
394+
elementRef,
395+
platform,
396+
document,
397+
animationMode,
398+
defaults,
399+
changeDetectorRef,
400+
viewportRuler,
401+
ngZone,
402+
);
343403
this.mode = 'indeterminate';
344404
}
345405
}

tools/public_api_guard/material/progress-spinner.md

+14-6
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@
66

77
import { _AbstractConstructor } from '@angular/material/core';
88
import { CanColor } from '@angular/material/core';
9+
import { ChangeDetectorRef } from '@angular/core';
910
import { _Constructor } from '@angular/material/core';
1011
import { ElementRef } from '@angular/core';
1112
import * as i0 from '@angular/core';
1213
import * as i2 from '@angular/material/core';
1314
import * as i3 from '@angular/common';
1415
import { InjectionToken } from '@angular/core';
16+
import { NgZone } from '@angular/core';
1517
import { NumberInput } from '@angular/cdk/coercion';
18+
import { OnDestroy } from '@angular/core';
1619
import { OnInit } from '@angular/core';
1720
import { Platform } from '@angular/cdk/platform';
21+
import { ViewportRuler } from '@angular/cdk/scrolling';
1822

1923
// @public
2024
export const MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS: InjectionToken<MatProgressSpinnerDefaultOptions>;
@@ -23,18 +27,21 @@ export const MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS: InjectionToken<MatProgressSpi
2327
export function MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS_FACTORY(): MatProgressSpinnerDefaultOptions;
2428

2529
// @public
26-
export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnInit, CanColor {
27-
constructor(elementRef: ElementRef<HTMLElement>,
28-
_platform: Platform, _document: any, animationMode: string, defaults?: MatProgressSpinnerDefaultOptions);
30+
export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnInit, OnDestroy, CanColor {
31+
constructor(elementRef: ElementRef<HTMLElement>, _platform: Platform, _document: any, animationMode: string, defaults?: MatProgressSpinnerDefaultOptions,
32+
changeDetectorRef?: ChangeDetectorRef, viewportRuler?: ViewportRuler, ngZone?: NgZone);
2933
get diameter(): number;
3034
set diameter(size: NumberInput);
3135
_getCircleRadius(): number;
3236
_getCircleStrokeWidth(): number;
37+
_getCircleTransformOrigin(svg: HTMLElement): string;
3338
_getStrokeCircumference(): number;
3439
_getStrokeDashOffset(): number | null;
3540
_getViewBox(): string;
3641
mode: ProgressSpinnerMode;
3742
// (undocumented)
43+
ngOnDestroy(): void;
44+
// (undocumented)
3845
ngOnInit(): void;
3946
_noopAnimations: boolean;
4047
_spinnerAnimationLabel: string;
@@ -45,7 +52,7 @@ export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnIni
4552
// (undocumented)
4653
static ɵcmp: i0.ɵɵComponentDeclaration<MatProgressSpinner, "mat-progress-spinner", ["matProgressSpinner"], { "color": "color"; "diameter": "diameter"; "strokeWidth": "strokeWidth"; "mode": "mode"; "value": "value"; }, {}, never, never>;
4754
// (undocumented)
48-
static ɵfac: i0.ɵɵFactoryDeclaration<MatProgressSpinner, [null, null, { optional: true; }, { optional: true; }, null]>;
55+
static ɵfac: i0.ɵɵFactoryDeclaration<MatProgressSpinner, [null, null, { optional: true; }, { optional: true; }, null, null, null, null]>;
4956
}
5057

5158
// @public
@@ -67,11 +74,12 @@ export class MatProgressSpinnerModule {
6774

6875
// @public
6976
export class MatSpinner extends MatProgressSpinner {
70-
constructor(elementRef: ElementRef<HTMLElement>, platform: Platform, document: any, animationMode: string, defaults?: MatProgressSpinnerDefaultOptions);
77+
constructor(elementRef: ElementRef<HTMLElement>, platform: Platform, document: any, animationMode: string, defaults?: MatProgressSpinnerDefaultOptions,
78+
changeDetectorRef?: ChangeDetectorRef, viewportRuler?: ViewportRuler, ngZone?: NgZone);
7179
// (undocumented)
7280
static ɵcmp: i0.ɵɵComponentDeclaration<MatSpinner, "mat-spinner", never, { "color": "color"; }, {}, never, never>;
7381
// (undocumented)
74-
static ɵfac: i0.ɵɵFactoryDeclaration<MatSpinner, [null, null, { optional: true; }, { optional: true; }, null]>;
82+
static ɵfac: i0.ɵɵFactoryDeclaration<MatSpinner, [null, null, { optional: true; }, { optional: true; }, null, null, null, null]>;
7583
}
7684

7785
// @public

0 commit comments

Comments
 (0)