Skip to content

Commit b827ae3

Browse files
committed
fix(material/progress-spinner): animation not working on some zoom levels in Safari
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 467e43b commit b827ae3

File tree

4 files changed

+65
-19
lines changed

4 files changed

+65
-19
lines changed

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

+48-9
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
Optional,
2020
ViewEncapsulation,
2121
OnInit,
22+
ChangeDetectorRef,
23+
OnDestroy,
2224
} from '@angular/core';
2325
import {CanColor, mixinColor} from '@angular/material/core';
2426
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
@@ -124,7 +126,8 @@ const INDETERMINATE_ANIMATION_TEMPLATE = `
124126
changeDetection: ChangeDetectionStrategy.OnPush,
125127
encapsulation: ViewEncapsulation.None,
126128
})
127-
export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnInit, CanColor {
129+
export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnInit, OnDestroy,
130+
CanColor {
128131
private _diameter = BASE_SIZE;
129132
private _value = 0;
130133
private _strokeWidth: number;
@@ -185,15 +188,16 @@ export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnIni
185188
}
186189

187190
constructor(elementRef: ElementRef<HTMLElement>,
188-
/**
189-
* @deprecated `_platform` parameter no longer being used.
190-
* @breaking-change 14.0.0
191-
*/
192-
_platform: Platform,
191+
private _platform: Platform,
193192
@Optional() @Inject(DOCUMENT) private _document: any,
194193
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode: string,
195194
@Inject(MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS)
196-
defaults?: MatProgressSpinnerDefaultOptions) {
195+
defaults?: MatProgressSpinnerDefaultOptions,
196+
/**
197+
* @deprecated `changeDetectorRef` parameter to become required.
198+
* @breaking-change 14.0.0
199+
*/
200+
private _changeDetectorRef?: ChangeDetectorRef) {
197201

198202
super(elementRef);
199203

@@ -218,6 +222,14 @@ export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnIni
218222
this.strokeWidth = defaults.strokeWidth;
219223
}
220224
}
225+
226+
// Safari has an issue where the circle isn't positioned correctly when the page has a
227+
// different zoom level from the default. This handler triggers a recalculation of the
228+
// `transform-origin` when the page zoom level changes.
229+
// See `_getCircleTransformOrigin` for more info.
230+
if (_platform.isBrowser && _platform.SAFARI) {
231+
window.addEventListener('resize', this._resizeHandler);
232+
}
221233
}
222234

223235
ngOnInit() {
@@ -231,6 +243,12 @@ export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnIni
231243
element.classList.add('mat-progress-spinner-indeterminate-animation');
232244
}
233245

246+
ngOnDestroy() {
247+
if (this._platform.isBrowser) {
248+
window.removeEventListener('resize', this._resizeHandler);
249+
}
250+
}
251+
234252
/** The radius of the spinner, adjusted for stroke width. */
235253
_getCircleRadius() {
236254
return (this.diameter - BASE_STROKE_WIDTH) / 2;
@@ -261,6 +279,16 @@ export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnIni
261279
return this.strokeWidth / this.diameter * 100;
262280
}
263281

282+
/** Gets the `transform-origin` for the inner circle element. */
283+
_getCircleTransformOrigin(svg: HTMLElement): string {
284+
// Safari has an issue where the `transform-origin` doesn't work as expected when the page
285+
// has a different zoom level from the default. The problem appears to be that a zoom
286+
// is applied on the `svg` node itself. We can work around it by calculating the origin
287+
// based on the zoom level. On all other browsers the `currentScale` appears to always be 1.
288+
const scale = ((svg as unknown as SVGSVGElement).currentScale ?? 1) * 50;
289+
return `${scale}% ${scale}%`;
290+
}
291+
264292
/** Dynamically generates a style tag containing the correct animation for this diameter. */
265293
private _attachStyleNode(): void {
266294
const styleRoot = this._styleRoot;
@@ -300,6 +328,16 @@ export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnIni
300328
return this.diameter.toString().replace('.', '_');
301329
}
302330

331+
/** Handles window `resize` events. */
332+
private _resizeHandler = () => {
333+
// When the window is resize while the spinner is in `indeterminate` mode, we
334+
// have to mark for check so the transform origin of the circle can be recomputed.
335+
if (this.mode === 'indeterminate') {
336+
// @breaking-change 14.0.0 Remove null check for `_changeDetectorRef`.
337+
this._changeDetectorRef?.markForCheck();
338+
}
339+
}
340+
303341
static ngAcceptInputType_diameter: NumberInput;
304342
static ngAcceptInputType_strokeWidth: NumberInput;
305343
static ngAcceptInputType_value: NumberInput;
@@ -333,8 +371,9 @@ export class MatSpinner extends MatProgressSpinner {
333371
@Optional() @Inject(DOCUMENT) document: any,
334372
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode: string,
335373
@Inject(MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS)
336-
defaults?: MatProgressSpinnerDefaultOptions) {
337-
super(elementRef, platform, document, animationMode, defaults);
374+
defaults?: MatProgressSpinnerDefaultOptions,
375+
changeDetectorRef?: ChangeDetectorRef) {
376+
super(elementRef, platform, document, animationMode, defaults, changeDetectorRef);
338377
this.mode = 'indeterminate';
339378
}
340379
}

tools/public_api_guard/material/progress-spinner.md

+11-6
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
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';
1516
import { NumberInput } from '@angular/cdk/coercion';
17+
import { OnDestroy } from '@angular/core';
1618
import { OnInit } from '@angular/core';
1719
import { Platform } from '@angular/cdk/platform';
1820

@@ -23,13 +25,14 @@ export const MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS: InjectionToken<MatProgressSpi
2325
export function MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS_FACTORY(): MatProgressSpinnerDefaultOptions;
2426

2527
// @public
26-
export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnInit, CanColor {
27-
constructor(elementRef: ElementRef<HTMLElement>,
28-
_platform: Platform, _document: any, animationMode: string, defaults?: MatProgressSpinnerDefaultOptions);
28+
export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnInit, OnDestroy, CanColor {
29+
constructor(elementRef: ElementRef<HTMLElement>, _platform: Platform, _document: any, animationMode: string, defaults?: MatProgressSpinnerDefaultOptions,
30+
_changeDetectorRef?: ChangeDetectorRef | undefined);
2931
get diameter(): number;
3032
set diameter(size: number);
3133
_getCircleRadius(): number;
3234
_getCircleStrokeWidth(): number;
35+
_getCircleTransformOrigin(svg: HTMLElement): string;
3336
_getStrokeCircumference(): number;
3437
_getStrokeDashOffset(): number | null;
3538
_getViewBox(): string;
@@ -41,6 +44,8 @@ export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnIni
4144
// (undocumented)
4245
static ngAcceptInputType_value: NumberInput;
4346
// (undocumented)
47+
ngOnDestroy(): void;
48+
// (undocumented)
4449
ngOnInit(): void;
4550
_noopAnimations: boolean;
4651
_spinnerAnimationLabel: string;
@@ -51,7 +56,7 @@ export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnIni
5156
// (undocumented)
5257
static ɵcmp: i0.ɵɵComponentDeclaration<MatProgressSpinner, "mat-progress-spinner", ["matProgressSpinner"], { "color": "color"; "diameter": "diameter"; "strokeWidth": "strokeWidth"; "mode": "mode"; "value": "value"; }, {}, never, never>;
5358
// (undocumented)
54-
static ɵfac: i0.ɵɵFactoryDeclaration<MatProgressSpinner, [null, null, { optional: true; }, { optional: true; }, null]>;
59+
static ɵfac: i0.ɵɵFactoryDeclaration<MatProgressSpinner, [null, null, { optional: true; }, { optional: true; }, null, null]>;
5560
}
5661

5762
// @public
@@ -73,11 +78,11 @@ export class MatProgressSpinnerModule {
7378

7479
// @public
7580
export class MatSpinner extends MatProgressSpinner {
76-
constructor(elementRef: ElementRef<HTMLElement>, platform: Platform, document: any, animationMode: string, defaults?: MatProgressSpinnerDefaultOptions);
81+
constructor(elementRef: ElementRef<HTMLElement>, platform: Platform, document: any, animationMode: string, defaults?: MatProgressSpinnerDefaultOptions, changeDetectorRef?: ChangeDetectorRef);
7782
// (undocumented)
7883
static ɵcmp: i0.ɵɵComponentDeclaration<MatSpinner, "mat-spinner", never, { "color": "color"; }, {}, never, never>;
7984
// (undocumented)
80-
static ɵfac: i0.ɵɵFactoryDeclaration<MatSpinner, [null, null, { optional: true; }, { optional: true; }, null]>;
85+
static ɵfac: i0.ɵɵFactoryDeclaration<MatSpinner, [null, null, { optional: true; }, { optional: true; }, null, null]>;
8186
}
8287

8388
// @public

0 commit comments

Comments
 (0)