Skip to content

Commit f5999e3

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 c7e0f99 commit f5999e3

File tree

5 files changed

+80
-19
lines changed

5 files changed

+80
-19
lines changed

src/material/progress-spinner/BUILD.bazel

Lines changed: 1 addition & 0 deletions
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

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: 59 additions & 9 deletions
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

2732
/** Possible mode for a progress spinner. */
@@ -124,10 +129,12 @@ const INDETERMINATE_ANIMATION_TEMPLATE = `
124129
changeDetection: ChangeDetectionStrategy.OnPush,
125130
encapsulation: ViewEncapsulation.None,
126131
})
127-
export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnInit, CanColor {
132+
export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnInit, OnDestroy,
133+
CanColor {
128134
private _diameter = BASE_SIZE;
129135
private _value = 0;
130136
private _strokeWidth: number;
137+
private _resizeSubscription = Subscription.EMPTY;
131138

132139
/**
133140
* Element to which we should add the generated style tags for the indeterminate animation.
@@ -185,15 +192,19 @@ export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnIni
185192
}
186193

187194
constructor(elementRef: ElementRef<HTMLElement>,
188-
/**
189-
* @deprecated `_platform` parameter no longer being used.
190-
* @breaking-change 14.0.0
191-
*/
192-
_platform: Platform,
195+
private _platform: Platform,
193196
@Optional() @Inject(DOCUMENT) private _document: any,
194197
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode: string,
195198
@Inject(MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS)
196-
defaults?: MatProgressSpinnerDefaultOptions) {
199+
defaults?: MatProgressSpinnerDefaultOptions,
200+
/**
201+
* @deprecated `changeDetectorRef`, `viewportRuler` and `ngZone`
202+
* parameters to become required.
203+
* @breaking-change 14.0.0
204+
*/
205+
changeDetectorRef?: ChangeDetectorRef,
206+
viewportRuler?: ViewportRuler,
207+
ngZone?: NgZone) {
197208

198209
super(elementRef);
199210

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

223250
ngOnInit() {
@@ -231,6 +258,10 @@ export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnIni
231258
element.classList.add('mat-progress-spinner-indeterminate-animation');
232259
}
233260

261+
ngOnDestroy() {
262+
this._resizeSubscription.unsubscribe();
263+
}
264+
234265
/** The radius of the spinner, adjusted for stroke width. */
235266
_getCircleRadius() {
236267
return (this.diameter - BASE_STROKE_WIDTH) / 2;
@@ -261,6 +292,16 @@ export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnIni
261292
return this.strokeWidth / this.diameter * 100;
262293
}
263294

295+
/** Gets the `transform-origin` for the inner circle element. */
296+
_getCircleTransformOrigin(svg: HTMLElement): string {
297+
// Safari has an issue where the `transform-origin` doesn't work as expected when the page
298+
// has a different zoom level from the default. The problem appears to be that a zoom
299+
// is applied on the `svg` node itself. We can work around it by calculating the origin
300+
// based on the zoom level. On all other browsers the `currentScale` appears to always be 1.
301+
const scale = ((svg as unknown as SVGSVGElement).currentScale ?? 1) * 50;
302+
return `${scale}% ${scale}%`;
303+
}
304+
264305
/** Dynamically generates a style tag containing the correct animation for this diameter. */
265306
private _attachStyleNode(): void {
266307
const styleRoot = this._styleRoot;
@@ -333,8 +374,17 @@ export class MatSpinner extends MatProgressSpinner {
333374
@Optional() @Inject(DOCUMENT) document: any,
334375
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode: string,
335376
@Inject(MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS)
336-
defaults?: MatProgressSpinnerDefaultOptions) {
337-
super(elementRef, platform, document, animationMode, defaults);
377+
defaults?: MatProgressSpinnerDefaultOptions,
378+
/**
379+
* @deprecated `changeDetectorRef`, `viewportRuler` and `ngZone`
380+
* parameters to become required.
381+
* @breaking-change 14.0.0
382+
*/
383+
changeDetectorRef?: ChangeDetectorRef,
384+
viewportRuler?: ViewportRuler,
385+
ngZone?: NgZone) {
386+
super(elementRef, platform, document, animationMode, defaults, changeDetectorRef,
387+
viewportRuler, ngZone);
338388
this.mode = 'indeterminate';
339389
}
340390
}

tools/public_api_guard/material/progress-spinner.md

Lines changed: 14 additions & 6 deletions
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,13 +27,14 @@ 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: number);
3135
_getCircleRadius(): number;
3236
_getCircleStrokeWidth(): number;
37+
_getCircleTransformOrigin(svg: HTMLElement): string;
3338
_getStrokeCircumference(): number;
3439
_getStrokeDashOffset(): number | null;
3540
_getViewBox(): string;
@@ -41,6 +46,8 @@ export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnIni
4146
// (undocumented)
4247
static ngAcceptInputType_value: NumberInput;
4348
// (undocumented)
49+
ngOnDestroy(): void;
50+
// (undocumented)
4451
ngOnInit(): void;
4552
_noopAnimations: boolean;
4653
_spinnerAnimationLabel: string;
@@ -51,7 +58,7 @@ export class MatProgressSpinner extends _MatProgressSpinnerBase implements OnIni
5158
// (undocumented)
5259
static ɵcmp: i0.ɵɵComponentDeclaration<MatProgressSpinner, "mat-progress-spinner", ["matProgressSpinner"], { "color": "color"; "diameter": "diameter"; "strokeWidth": "strokeWidth"; "mode": "mode"; "value": "value"; }, {}, never, never>;
5360
// (undocumented)
54-
static ɵfac: i0.ɵɵFactoryDeclaration<MatProgressSpinner, [null, null, { optional: true; }, { optional: true; }, null]>;
61+
static ɵfac: i0.ɵɵFactoryDeclaration<MatProgressSpinner, [null, null, { optional: true; }, { optional: true; }, null, null, null, null]>;
5562
}
5663

5764
// @public
@@ -73,11 +80,12 @@ export class MatProgressSpinnerModule {
7380

7481
// @public
7582
export class MatSpinner extends MatProgressSpinner {
76-
constructor(elementRef: ElementRef<HTMLElement>, platform: Platform, document: any, animationMode: string, defaults?: MatProgressSpinnerDefaultOptions);
83+
constructor(elementRef: ElementRef<HTMLElement>, platform: Platform, document: any, animationMode: string, defaults?: MatProgressSpinnerDefaultOptions,
84+
changeDetectorRef?: ChangeDetectorRef, viewportRuler?: ViewportRuler, ngZone?: NgZone);
7785
// (undocumented)
7886
static ɵcmp: i0.ɵɵComponentDeclaration<MatSpinner, "mat-spinner", never, { "color": "color"; }, {}, never, never>;
7987
// (undocumented)
80-
static ɵfac: i0.ɵɵFactoryDeclaration<MatSpinner, [null, null, { optional: true; }, { optional: true; }, null]>;
88+
static ɵfac: i0.ɵɵFactoryDeclaration<MatSpinner, [null, null, { optional: true; }, { optional: true; }, null, null, null, null]>;
8189
}
8290

8391
// @public

0 commit comments

Comments
 (0)