Skip to content

Commit 0504e41

Browse files
committed
feat(progress-spinner): add support for custom diameters
1 parent 282c87f commit 0504e41

File tree

7 files changed

+164
-36
lines changed

7 files changed

+164
-36
lines changed

src/demo-app/progress-spinner/progress-spinner-demo.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ <h1>Determinate</h1>
99

1010
<div class="demo-progress-spinner">
1111
<mat-progress-spinner [mode]="isDeterminate ? 'determinate' : 'indeterminate'"
12-
[value]="progressValue" color="primary" [strokeWidth]="1"></mat-progress-spinner>
12+
[value]="progressValue" color="primary" [strokeWidth]="1" [diameter]="32"></mat-progress-spinner>
1313
<mat-progress-spinner [mode]="isDeterminate ? 'determinate' : 'indeterminate'"
14-
[value]="progressValue" color="accent"></mat-progress-spinner>
14+
[value]="progressValue" color="accent" [diameter]="50"></mat-progress-spinner>
1515
</div>
1616

1717
<h1>Indeterminate</h1>

src/lib/progress-spinner/progress-spinner-module.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,21 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import {NgModule} from '@angular/core';
9+
import {PlatformModule} from '@angular/cdk/platform';
910
import {MatCommonModule} from '@angular/material/core';
1011
import {MatProgressSpinner, MatSpinner} from './progress-spinner';
1112

12-
1313
@NgModule({
14-
imports: [MatCommonModule],
15-
exports: [MatProgressSpinner, MatSpinner, MatCommonModule],
16-
declarations: [MatProgressSpinner, MatSpinner],
14+
imports: [MatCommonModule, PlatformModule],
15+
exports: [
16+
MatProgressSpinner,
17+
MatSpinner,
18+
MatCommonModule
19+
],
20+
declarations: [
21+
MatProgressSpinner,
22+
MatSpinner
23+
],
1724
})
1825
class MatProgressSpinnerModule {}
1926

src/lib/progress-spinner/progress-spinner.html

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@
44
element containing the SVG. `focusable="false"` prevents IE from allowing the user to
55
tab into the SVG element.
66
-->
7+
78
<svg
8-
width="100"
9-
height="100"
10-
viewBox="0 0 100 100"
9+
[style.width.px]="_elementSize"
10+
[style.height.px]="_elementSize"
11+
[attr.viewBox]="_viewBox"
1112
preserveAspectRatio="xMidYMid meet"
1213
focusable="false">
1314

1415
<circle
1516
cx="50%"
1617
cy="50%"
1718
[attr.r]="_circleRadius"
18-
[style.stroke-dashoffset.px]="_getStrokeDashOffset()"
19+
[style.animation-name]="'mat-progress-spinner-stroke-rotate-' + diameter"
20+
[style.stroke-dashoffset.px]="_strokeDashOffset"
21+
[style.stroke-dasharray.px]="_strokeCircumference"
22+
[style.transform.rotate]="'360deg'"
1923
[style.stroke-width.px]="strokeWidth"></circle>
2024
</svg>

src/lib/progress-spinner/progress-spinner.scss

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55
$mat-progress-spinner-stroke-rotate-fallback-duration: 10 * 1000ms !default;
66
$mat-progress-spinner-stroke-rotate-fallback-ease: cubic-bezier(0.87, 0.03, 0.33, 1) !default;
77

8-
$_mat-progress-spinner-radius: 45px;
9-
$_mat-progress-spinner-circumference: $pi * $_mat-progress-spinner-radius * 2;
10-
8+
$_mat-progress-spinner-default-radius: 45px;
9+
$_mat-progress-spinner-default-circumference: $pi * $_mat-progress-spinner-default-radius * 2;
1110

1211
.mat-progress-spinner {
1312
display: block;
@@ -24,8 +23,6 @@ $_mat-progress-spinner-circumference: $pi * $_mat-progress-spinner-radius * 2;
2423

2524
circle {
2625
fill: transparent;
27-
stroke-dasharray: $_mat-progress-spinner-circumference;
28-
stroke-dashoffset: $_mat-progress-spinner-circumference;
2926
transform-origin: center;
3027
transition: stroke-dashoffset 225ms linear;
3128
}
@@ -35,10 +32,11 @@ $_mat-progress-spinner-circumference: $pi * $_mat-progress-spinner-radius * 2;
3532
linear infinite;
3633

3734
circle {
38-
// Note: we multiply the duration by 8, because the animation is spread out in 8 stages.
39-
animation: mat-progress-spinner-stroke-rotate $swift-ease-in-out-duration * 8
40-
$ease-in-out-curve-function infinite;
4135
transition-property: stroke;
36+
// Note: we multiply the duration by 8, because the animation is spread out in 8 stages.
37+
animation-duration: $swift-ease-in-out-duration * 8;
38+
animation-timing-function: $ease-in-out-curve-function;
39+
animation-iteration-count: infinite;
4240
}
4341
}
4442

@@ -49,7 +47,6 @@ $_mat-progress-spinner-circumference: $pi * $_mat-progress-spinner-radius * 2;
4947
infinite;
5048

5149
circle {
52-
stroke-dashoffset: (1 - 0.8) * $_mat-progress-spinner-circumference;
5350
transition-property: stroke;
5451
}
5552
}
@@ -63,11 +60,11 @@ $_mat-progress-spinner-circumference: $pi * $_mat-progress-spinner-radius * 2;
6360
}
6461

6562
@at-root {
66-
$start: (1 - 0.05) * $_mat-progress-spinner-circumference; // start the animation at 5%
67-
$end: (1 - 0.8) * $_mat-progress-spinner-circumference; // end the animation at 80%
63+
$start: (1 - 0.05) * $_mat-progress-spinner-default-circumference; // start the animation at 5%
64+
$end: (1 - 0.8) * $_mat-progress-spinner-default-circumference; // end the animation at 80%
6865
$fallback-iterations: 4;
6966

70-
@keyframes mat-progress-spinner-stroke-rotate {
67+
@keyframes mat-progress-spinner-stroke-rotate-100 {
7168
/*
7269
stylelint-disable declaration-block-single-line-max-declarations,
7370
declaration-block-semicolon-space-after

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ describe('MatProgressSpinner', () => {
1515
ProgressSpinnerWithValueAndBoundMode,
1616
ProgressSpinnerWithColor,
1717
ProgressSpinnerCustomStrokeWidth,
18+
ProgressSpinnerCustomDiameter,
1819
SpinnerWithColor,
1920
],
2021
}).compileComponents();
@@ -79,6 +80,26 @@ describe('MatProgressSpinner', () => {
7980
expect(progressComponent.value).toBe(0);
8081
});
8182

83+
it('should allow a custom diameter', () => {
84+
const fixture = TestBed.createComponent(ProgressSpinnerCustomDiameter);
85+
const spinner = fixture.debugElement.query(By.css('mat-progress-spinner')).nativeElement;
86+
const svgElement = fixture.nativeElement.querySelector('svg');
87+
88+
fixture.componentInstance.diameter = 32;
89+
fixture.detectChanges();
90+
91+
expect(parseInt(spinner.style.width))
92+
.toBe(32, 'Expected the custom diameter to be applied to the host element width.');
93+
expect(parseInt(spinner.style.height))
94+
.toBe(32, 'Expected the custom diameter to be applied to the host element height.');
95+
expect(parseInt(svgElement.style.width))
96+
.toBe(32, 'Expected the custom diameter to be applied to the svg element width.');
97+
expect(parseInt(svgElement.style.height))
98+
.toBe(32, 'Expected the custom diameter to be applied to the svg element height.');
99+
expect(svgElement.getAttribute('viewBox'))
100+
.toBe('0 0 32 32', 'Expected the custom diameter to be applied to the svg viewBox.');
101+
});
102+
82103
it('should allow a custom stroke width', () => {
83104
const fixture = TestBed.createComponent(ProgressSpinnerCustomStrokeWidth);
84105
const circleElement = fixture.nativeElement.querySelector('circle');
@@ -161,6 +182,11 @@ class ProgressSpinnerCustomStrokeWidth {
161182
strokeWidth: number;
162183
}
163184

185+
@Component({template: '<mat-progress-spinner [diameter]="diameter"></mat-progress-spinner>'})
186+
class ProgressSpinnerCustomDiameter {
187+
diameter: number;
188+
}
189+
164190
@Component({template: '<mat-progress-spinner mode="indeterminate"></mat-progress-spinner>'})
165191
class IndeterminateProgressSpinner { }
166192

src/lib/progress-spinner/progress-spinner.ts

Lines changed: 104 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,47 @@ import {
1515
SimpleChanges,
1616
OnChanges,
1717
ViewEncapsulation,
18+
Optional,
19+
Inject,
1820
} from '@angular/core';
1921
import {CanColor, mixinColor} from '@angular/material/core';
2022
import {Platform} from '@angular/cdk/platform';
23+
import {DOCUMENT} from '@angular/common';
2124

2225
/** Possible mode for a progress spinner. */
2326
export type ProgressSpinnerMode = 'determinate' | 'indeterminate';
2427

25-
// Boilerplate for applying mixins to MdProgressSpinner.
28+
// Boilerplate for applying mixins to MatProgressSpinner.
2629
/** @docs-private */
2730
export class MatProgressSpinnerBase {
2831
constructor(public _renderer: Renderer2, public _elementRef: ElementRef) {}
2932
}
3033
export const _MatProgressSpinnerMixinBase = mixinColor(MatProgressSpinnerBase, 'primary');
3134

35+
const INDETERMINATE_ANIMATION_TEMPLATE = `
36+
@keyframes mat-progress-spinner-stroke-rotate-DIAMETER {
37+
0% { stroke-dashoffset: START_VALUE; transform: rotate(0); }
38+
12.5% { stroke-dashoffset: END_VALUE; transform: rotate(0); }
39+
12.51% { stroke-dashoffset: END_VALUE; transform: rotateX(180deg) rotate(72.5deg); }
40+
25% { stroke-dashoffset: START_VALUE; transform: rotateX(180deg) rotate(72.5deg); }
41+
42+
25.1% { stroke-dashoffset: START_VALUE; transform: rotate(270deg); }
43+
37.5% { stroke-dashoffset: END_VALUE; transform: rotate(270deg); }
44+
37.51% { stroke-dashoffset: END_VALUE; transform: rotateX(180deg) rotate(161.5deg); }
45+
50% { stroke-dashoffset: START_VALUE; transform: rotateX(180deg) rotate(161.5deg); }
46+
47+
50.01% { stroke-dashoffset: START_VALUE; transform: rotate(180deg); }
48+
62.5% { stroke-dashoffset: END_VALUE; transform: rotate(180deg); }
49+
62.51% { stroke-dashoffset: END_VALUE; transform: rotateX(180deg) rotate(251.5deg); }
50+
75% { stroke-dashoffset: START_VALUE; transform: rotateX(180deg) rotate(251.5deg); }
51+
52+
75.01% { stroke-dashoffset: START_VALUE; transform: rotate(90deg); }
53+
87.5% { stroke-dashoffset: END_VALUE; transform: rotate(90deg); }
54+
87.51% { stroke-dashoffset: END_VALUE; transform: rotateX(180deg) rotate(341.5deg); }
55+
100% { stroke-dashoffset: START_VALUE; transform: rotateX(180deg) rotate(341.5deg); }
56+
}
57+
`;
58+
3259
/**
3360
* <mat-progress-spinner> component.
3461
*/
@@ -53,13 +80,30 @@ export const _MatProgressSpinnerMixinBase = mixinColor(MatProgressSpinnerBase, '
5380
encapsulation: ViewEncapsulation.None,
5481
preserveWhitespaces: false,
5582
})
56-
export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements CanColor, OnChanges {
83+
export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements CanColor,
84+
OnChanges {
85+
5786
private _value: number;
5887
private readonly _baseSize = 100;
5988
private readonly _baseStrokeWidth = 10;
89+
private _fallbackAnimation = false;
6090

91+
/** The width and height of the host element. Will grow with stroke width. **/
6192
_elementSize = this._baseSize;
62-
_circleRadius = 45;
93+
94+
/** Tracks diameters of existing instances to de-dupe generated styles (default d = 100) */
95+
static diameters = new Set<number>([100]);
96+
97+
/** The diameter of the progress spinner (will set width and height of svg). */
98+
@Input()
99+
get diameter(): number {
100+
return this._diameter;
101+
}
102+
103+
set diameter(size: number) {
104+
this._setDiameterAndInitStyles(size);
105+
}
106+
_diameter = this._baseSize;
63107

64108
/** Stroke width of the progress spinner. */
65109
@Input() strokeWidth: number = 10;
@@ -78,31 +122,76 @@ export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements
78122
}
79123
}
80124

81-
constructor(renderer: Renderer2, elementRef: ElementRef, platform: Platform) {
82-
super(renderer, elementRef);
125+
constructor(public _renderer: Renderer2, public _elementRef: ElementRef,
126+
platform: Platform, @Optional() @Inject(DOCUMENT) private _document: any) {
127+
super(_renderer, _elementRef);
83128

84-
// On IE and Edge we can't animate the `stroke-dashoffset`
129+
this._fallbackAnimation = platform.EDGE || platform.TRIDENT;
130+
131+
// On IE and Edge, we can't animate the `stroke-dashoffset`
85132
// reliably so we fall back to a non-spec animation.
86-
const animationClass = (platform.EDGE || platform.TRIDENT) ?
133+
const animationClass = this._fallbackAnimation ?
87134
'mat-progress-spinner-indeterminate-fallback-animation' :
88135
'mat-progress-spinner-indeterminate-animation';
89136

90-
renderer.addClass(elementRef.nativeElement, animationClass);
137+
_renderer.addClass(_elementRef.nativeElement, animationClass);
91138
}
92139

93140
ngOnChanges(changes: SimpleChanges) {
94-
if (changes.strokeWidth) {
95-
this._elementSize = this._baseSize + Math.max(this.strokeWidth - this._baseStrokeWidth, 0);
141+
if (changes.strokeWidth || changes.diameter) {
142+
this._elementSize =
143+
this._diameter + Math.max(this.strokeWidth - this._baseStrokeWidth, 0);
96144
}
97145
}
98146

99-
_getStrokeDashOffset() {
147+
/** The radius of the spinner, adjusted for stroke width. */
148+
get _circleRadius() {
149+
return (this.diameter - this._baseStrokeWidth) / 2;
150+
}
151+
152+
/** The view box of the spinner's svg element. */
153+
get _viewBox() {
154+
return `0 0 ${this._elementSize} ${this._elementSize}`;
155+
}
156+
157+
/** The stroke circumference of the svg circle. */
158+
get _strokeCircumference(): number {
159+
return 2 * Math.PI * this._circleRadius;
160+
}
161+
162+
/** The dash offset of the svg circle. */
163+
get _strokeDashOffset() {
100164
if (this.mode === 'determinate') {
101-
return 2 * Math.PI * this._circleRadius * (100 - this._value) / 100;
165+
return this._strokeCircumference * (100 - this._value) / 100;
102166
}
103167

104168
return null;
105169
}
170+
171+
/** Sets the diameter and adds diameter-specific styles if necessary. */
172+
private _setDiameterAndInitStyles(size: number): void {
173+
this._diameter = size;
174+
if (!MatProgressSpinner.diameters.has(this.diameter) && !this._fallbackAnimation) {
175+
this._attachStyleNode();
176+
}
177+
}
178+
179+
/** Dynamically generates a style tag containing the correct animation for this diameter. */
180+
private _attachStyleNode(): void {
181+
const styleTag = this._renderer.createElement('style');
182+
styleTag.textContent = this._getAnimationText();
183+
this._renderer.appendChild(this._document.head, styleTag);
184+
MatProgressSpinner.diameters.add(this.diameter);
185+
}
186+
187+
/** Generates animation styles adjusted for the spinner's diameter. */
188+
private _getAnimationText(): string {
189+
return INDETERMINATE_ANIMATION_TEMPLATE
190+
// Animation should begin at 5% and end at 80%
191+
.replace(/START_VALUE/g, `${0.95 * this._strokeCircumference}`)
192+
.replace(/END_VALUE/g, `${0.2 * this._strokeCircumference}`)
193+
.replace(/DIAMETER/g, `${this.diameter}`);
194+
}
106195
}
107196

108197

@@ -130,8 +219,9 @@ export class MatProgressSpinner extends _MatProgressSpinnerMixinBase implements
130219
preserveWhitespaces: false,
131220
})
132221
export class MatSpinner extends MatProgressSpinner {
133-
constructor(renderer: Renderer2, elementRef: ElementRef, platform: Platform) {
134-
super(renderer, elementRef, platform);
222+
constructor(renderer: Renderer2, elementRef: ElementRef, platform: Platform,
223+
@Optional() @Inject(DOCUMENT) document: any) {
224+
super(renderer, elementRef, platform, document);
135225
this.mode = 'indeterminate';
136226
}
137227
}

src/universal-app/kitchen-sink/kitchen-sink.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ <h2>Progress bar</h2>
142142
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
143143
<mat-progress-bar mode="query"></mat-progress-bar>
144144

145+
<h2>Progress spinner</h2>
146+
147+
<mat-progress-spinner mode="indeterminate" [diameter]="32"></mat-progress-spinner>
148+
<mat-progress-spinner mode="determinate" [value]="60"></mat-progress-spinner>
145149

146150
<h2>Radio buttons</h2>
147151

0 commit comments

Comments
 (0)