Skip to content

Commit 71c5cb6

Browse files
committed
feat(progress-spinner): add support for custom diameters
1 parent f7f4b07 commit 71c5cb6

File tree

7 files changed

+163
-37
lines changed

7 files changed

+163
-37
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
<md-progress-spinner [mode]="isDeterminate ? 'determinate' : 'indeterminate'"
12-
[value]="progressValue" color="primary" [strokeWidth]="1"></md-progress-spinner>
12+
[value]="progressValue" color="primary" [strokeWidth]="1" [diameter]="32"></md-progress-spinner>
1313
<md-progress-spinner [mode]="isDeterminate ? 'determinate' : 'indeterminate'"
14-
[value]="progressValue" color="accent"></md-progress-spinner>
14+
[value]="progressValue" color="accent" [diameter]="50"></md-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 {MdCommonModule} from '@angular/material/core';
1011
import {MdProgressSpinner, MdSpinner} from './progress-spinner';
1112

12-
1313
@NgModule({
14-
imports: [MdCommonModule],
15-
exports: [MdProgressSpinner, MdSpinner, MdCommonModule],
16-
declarations: [MdProgressSpinner, MdSpinner],
14+
imports: [MdCommonModule, PlatformModule],
15+
exports: [
16+
MdProgressSpinner,
17+
MdSpinner,
18+
MdCommonModule
19+
],
20+
declarations: [
21+
MdProgressSpinner,
22+
MdSpinner
23+
],
1724
})
1825
class MdProgressSpinnerModule {}
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('MdProgressSpinner', () => {
1515
ProgressSpinnerWithValueAndBoundMode,
1616
ProgressSpinnerWithColor,
1717
ProgressSpinnerCustomStrokeWidth,
18+
ProgressSpinnerCustomDiameter,
1819
SpinnerWithColor,
1920
],
2021
}).compileComponents();
@@ -79,6 +80,26 @@ describe('MdProgressSpinner', () => {
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('md-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: '<md-progress-spinner [diameter]="diameter"></md-progress-spinner>'})
186+
class ProgressSpinnerCustomDiameter {
187+
diameter: number;
188+
}
189+
164190
@Component({template: '<md-progress-spinner mode="indeterminate"></md-progress-spinner>'})
165191
class IndeterminateProgressSpinner { }
166192

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

Lines changed: 103 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ import {
1212
Input,
1313
ElementRef,
1414
Renderer2,
15-
Directive,
16-
ViewChild,
1715
SimpleChanges,
1816
OnChanges,
1917
ViewEncapsulation,
18+
Optional,
19+
Inject,
2020
} from '@angular/core';
2121
import {CanColor, mixinColor} from '@angular/material/core';
2222
import {Platform} from '@angular/cdk/platform';
23+
import {DOCUMENT} from '@angular/common';
2324

2425
/** Possible mode for a progress spinner. */
2526
export type ProgressSpinnerMode = 'determinate' | 'indeterminate';
@@ -31,6 +32,30 @@ export class MdProgressSpinnerBase {
3132
}
3233
export const _MdProgressSpinnerMixinBase = mixinColor(MdProgressSpinnerBase, 'primary');
3334

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+
3459
/**
3560
* <md-progress-spinner> component.
3661
*/
@@ -54,13 +79,30 @@ export const _MdProgressSpinnerMixinBase = mixinColor(MdProgressSpinnerBase, 'pr
5479
encapsulation: ViewEncapsulation.None,
5580
preserveWhitespaces: false,
5681
})
57-
export class MdProgressSpinner extends _MdProgressSpinnerMixinBase implements CanColor, OnChanges {
82+
export class MdProgressSpinner extends _MdProgressSpinnerMixinBase implements CanColor,
83+
OnChanges {
84+
5885
private _value: number;
5986
private readonly _baseSize = 100;
6087
private readonly _baseStrokeWidth = 10;
88+
private _fallbackAnimation = false;
6189

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

65107
/** Stroke width of the progress spinner. */
66108
@Input() strokeWidth: number = 10;
@@ -79,31 +121,76 @@ export class MdProgressSpinner extends _MdProgressSpinnerMixinBase implements Ca
79121
}
80122
}
81123

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

85-
// On IE and Edge we can't animate the `stroke-dashoffset`
128+
this._fallbackAnimation = platform.EDGE || platform.TRIDENT;
129+
130+
// On IE and Edge, we can't animate the `stroke-dashoffset`
86131
// reliably so we fall back to a non-spec animation.
87-
const animationClass = (platform.EDGE || platform.TRIDENT) ?
132+
const animationClass = this._fallbackAnimation ?
88133
'mat-progress-spinner-indeterminate-fallback-animation' :
89134
'mat-progress-spinner-indeterminate-animation';
90135

91-
renderer.addClass(elementRef.nativeElement, animationClass);
136+
_renderer.addClass(_elementRef.nativeElement, animationClass);
92137
}
93138

94139
ngOnChanges(changes: SimpleChanges) {
95-
if (changes.strokeWidth) {
96-
this._elementSize = this._baseSize + Math.max(this.strokeWidth - this._baseStrokeWidth, 0);
140+
if (changes.strokeWidth || changes.diameter) {
141+
this._elementSize =
142+
this._diameter + Math.max(this.strokeWidth - this._baseStrokeWidth, 0);
97143
}
98144
}
99145

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

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

109196

@@ -131,8 +218,9 @@ export class MdProgressSpinner extends _MdProgressSpinnerMixinBase implements Ca
131218
preserveWhitespaces: false,
132219
})
133220
export class MdSpinner extends MdProgressSpinner {
134-
constructor(renderer: Renderer2, elementRef: ElementRef, platform: Platform) {
135-
super(renderer, elementRef, platform);
221+
constructor(renderer: Renderer2, elementRef: ElementRef, platform: Platform,
222+
@Optional() @Inject(DOCUMENT) document: any) {
223+
super(renderer, elementRef, platform, document);
136224
this.mode = 'indeterminate';
137225
}
138226
}

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
<md-progress-bar mode="indeterminate"></md-progress-bar>
143143
<md-progress-bar mode="query"></md-progress-bar>
144144

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

146150
<h2>Radio buttons</h2>
147151

0 commit comments

Comments
 (0)