Skip to content

Commit 2c47b06

Browse files
authored
fix(material/progress-spinner): Progress spinner animation fails for floating point diameter values (#20192)
Fixes a bug in the Angular Material `progress-spinner` component where spinner animations do not work for custom diameters with float point values. This is because the animation-name CSS style on the spinner circle includes an unsupported period ‘.’ character when the diameter is a float point number. An underscore can be substituted instead of a period to fix this. Fixes #20158
1 parent 7ea505b commit 2c47b06

File tree

4 files changed

+83
-3
lines changed

4 files changed

+83
-3
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
cx="50%"
2626
cy="50%"
2727
[attr.r]="_getCircleRadius()"
28-
[style.animation-name]="'mat-progress-spinner-stroke-rotate-' + diameter"
28+
[style.animation-name]="'mat-progress-spinner-stroke-rotate-' + _spinnerAnimationLabel"
2929
[style.stroke-dashoffset.px]="_getStrokeDashOffset()"
3030
[style.stroke-dasharray.px]="_getStrokeCircumference()"
3131
[style.stroke-width.%]="_getCircleStrokeWidth()"></circle>

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

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ describe('MatProgressSpinner', () => {
1616
declarations: [
1717
BasicProgressSpinner,
1818
IndeterminateProgressSpinner,
19+
IndeterminateSpinnerCustomDiameter,
1920
ProgressSpinnerWithValueAndBoundMode,
2021
ProgressSpinnerWithColor,
2122
ProgressSpinnerCustomStrokeWidth,
@@ -185,6 +186,48 @@ describe('MatProgressSpinner', () => {
185186
expect(document.head.querySelectorAll('style[mat-spinner-animation="64"]').length).toBe(1);
186187
}));
187188

189+
it('should allow floating point values for custom diameter', () => {
190+
const fixture = TestBed.createComponent(ProgressSpinnerCustomDiameter);
191+
192+
fixture.componentInstance.diameter = 32.5;
193+
fixture.detectChanges();
194+
195+
const spinner = fixture.debugElement.query(By.css('mat-progress-spinner'))!.nativeElement;
196+
const svgElement = fixture.nativeElement.querySelector('svg');
197+
198+
expect(parseFloat(spinner.style.width))
199+
.toBe(32.5, 'Expected the custom diameter to be applied to the host element width.');
200+
expect(parseFloat(spinner.style.height))
201+
.toBe(32.5, 'Expected the custom diameter to be applied to the host element height.');
202+
expect(parseFloat(svgElement.style.width))
203+
.toBe(32.5, 'Expected the custom diameter to be applied to the svg element width.');
204+
expect(parseFloat(svgElement.style.height))
205+
.toBe(32.5, 'Expected the custom diameter to be applied to the svg element height.');
206+
expect(svgElement.getAttribute('viewBox'))
207+
.toBe('0 0 25.75 25.75', 'Expected the custom diameter to be applied to the svg viewBox.');
208+
});
209+
210+
it('should handle creating animation style tags based on a floating point diameter',
211+
inject([Platform], (platform: Platform) => {
212+
// On Edge and IE we use a fallback animation because the
213+
// browser doesn't support animating SVG correctly.
214+
if (platform.EDGE || platform.TRIDENT) {
215+
return;
216+
}
217+
218+
const fixture = TestBed.createComponent(IndeterminateSpinnerCustomDiameter);
219+
220+
fixture.componentInstance.diameter = 32.5;
221+
fixture.detectChanges();
222+
223+
const circleElement = fixture.nativeElement.querySelector('circle');
224+
225+
expect(circleElement.style.animationName).toBe('mat-progress-spinner-stroke-rotate-32_5',
226+
'Expected the spinner circle element to have an animation name based on the custom diameter');
227+
expect(document.head.querySelectorAll('style[mat-spinner-animation="32_5"]').length).toBe(1,
228+
'Expected a style tag with the indeterminate animation to be attached to the document head');
229+
}));
230+
188231
it('should allow a custom stroke width', () => {
189232
const fixture = TestBed.createComponent(ProgressSpinnerCustomStrokeWidth);
190233

@@ -200,6 +243,21 @@ describe('MatProgressSpinner', () => {
200243
.toBe('0 0 130 130', 'Expected the viewBox to be adjusted based on the stroke width.');
201244
});
202245

246+
it('should allow floating point values for custom stroke width', () => {
247+
const fixture = TestBed.createComponent(ProgressSpinnerCustomStrokeWidth);
248+
249+
fixture.componentInstance.strokeWidth = 40.5;
250+
fixture.detectChanges();
251+
252+
const circleElement = fixture.nativeElement.querySelector('circle');
253+
const svgElement = fixture.nativeElement.querySelector('svg');
254+
255+
expect(parseFloat(circleElement.style.strokeWidth)).toBe(40.5, 'Expected the custom stroke ' +
256+
'width to be applied to the circle element as a percentage of the element size.');
257+
expect(svgElement.getAttribute('viewBox'))
258+
.toBe('0 0 130.5 130.5', 'Expected the viewBox to be adjusted based on the stroke width.');
259+
});
260+
203261
it('should expand the host element if the stroke width is greater than the default', () => {
204262
const fixture = TestBed.createComponent(ProgressSpinnerCustomStrokeWidth);
205263
const element = fixture.debugElement.nativeElement.querySelector('.mat-progress-spinner');
@@ -444,6 +502,15 @@ class ProgressSpinnerCustomDiameter {
444502
@Component({template: '<mat-progress-spinner mode="indeterminate"></mat-progress-spinner>'})
445503
class IndeterminateProgressSpinner { }
446504

505+
@Component({
506+
template: `
507+
<mat-progress-spinner mode="indeterminate" [diameter]="diameter"></mat-progress-spinner>
508+
`,
509+
})
510+
class IndeterminateSpinnerCustomDiameter {
511+
diameter: number;
512+
}
513+
447514
@Component({
448515
template: '<mat-progress-spinner [value]="value" [mode]="mode"></mat-progress-spinner>'
449516
})

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,11 +147,15 @@ export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements
147147
/** Whether the _mat-animation-noopable class should be applied, disabling animations. */
148148
_noopAnimations: boolean;
149149

150+
/** A string that is used for setting the spinner animation-name CSS property */
151+
_spinnerAnimationLabel: string;
152+
150153
/** The diameter of the progress spinner (will set width and height of svg). */
151154
@Input()
152155
get diameter(): number { return this._diameter; }
153156
set diameter(size: number) {
154157
this._diameter = coerceNumberProperty(size);
158+
this._spinnerAnimationLabel = this._getSpinnerAnimationLabel();
155159

156160
// If this is set before `ngOnInit`, the style root may not have been resolved yet.
157161
if (!this._fallbackAnimation && this._styleRoot) {
@@ -190,6 +194,7 @@ export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements
190194
super(_elementRef);
191195

192196
const trackedDiameters = MatProgressSpinner._diameters;
197+
this._spinnerAnimationLabel = this._getSpinnerAnimationLabel();
193198

194199
// The base size is already inserted via the component's structural styles. We still
195200
// need to track it so we don't end up adding the same styles again.
@@ -273,7 +278,7 @@ export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements
273278

274279
if (!diametersForElement || !diametersForElement.has(currentDiameter)) {
275280
const styleTag: HTMLStyleElement = this._document.createElement('style');
276-
styleTag.setAttribute('mat-spinner-animation', currentDiameter + '');
281+
styleTag.setAttribute('mat-spinner-animation', this._spinnerAnimationLabel);
277282
styleTag.textContent = this._getAnimationText();
278283
styleRoot.appendChild(styleTag);
279284

@@ -293,7 +298,14 @@ export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements
293298
// Animation should begin at 5% and end at 80%
294299
.replace(/START_VALUE/g, `${0.95 * strokeCircumference}`)
295300
.replace(/END_VALUE/g, `${0.2 * strokeCircumference}`)
296-
.replace(/DIAMETER/g, `${this.diameter}`);
301+
.replace(/DIAMETER/g, `${this._spinnerAnimationLabel}`);
302+
}
303+
304+
/** Returns the circle diameter formatted for use with the animation-name CSS property. */
305+
private _getSpinnerAnimationLabel(): string {
306+
// The string of a float point number will include a period ‘.’ character,
307+
// which is not valid for a CSS animation-name.
308+
return this.diameter.toString().replace('.', '_');
297309
}
298310

299311
static ngAcceptInputType_diameter: NumberInput;

tools/public_api_guard/material/progress-spinner.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export declare function MAT_PROGRESS_SPINNER_DEFAULT_OPTIONS_FACTORY(): MatProgr
55
export declare class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements OnInit, CanColor {
66
_elementRef: ElementRef<HTMLElement>;
77
_noopAnimations: boolean;
8+
_spinnerAnimationLabel: string;
89
get diameter(): number;
910
set diameter(size: number);
1011
mode: ProgressSpinnerMode;

0 commit comments

Comments
 (0)