Skip to content

Commit 19bab1d

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 7c16258 commit 19bab1d

File tree

3 files changed

+54
-13
lines changed

3 files changed

+54
-13
lines changed

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

Lines changed: 6 additions & 3 deletions
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

Lines changed: 0 additions & 1 deletion
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

Lines changed: 48 additions & 9 deletions
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
}

0 commit comments

Comments
 (0)