Skip to content

Commit 9b453c3

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

File tree

7 files changed

+182
-36
lines changed

7 files changed

+182
-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
<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: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@
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.stroke-dashoffset.px]="_strokeDashOffset"
20+
[style.stroke-dasharray.px]="_strokeCircumference"
21+
[style.transform.rotate]="'360deg'"
1922
[style.stroke-width.px]="strokeWidth"></circle>
2023
</svg>

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

Lines changed: 7 additions & 11 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
}
@@ -34,9 +31,9 @@ $_mat-progress-spinner-circumference: $pi * $_mat-progress-spinner-radius * 2;
3431
animation: mat-progress-spinner-linear-rotate $swift-ease-in-out-duration * 4
3532
linear infinite;
3633

37-
circle {
34+
&.mat-progress-spinner-100 circle {
3835
// 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
36+
animation: mat-progress-spinner-stroke-rotate-100 $swift-ease-in-out-duration * 8
4037
$ease-in-out-curve-function infinite;
4138
transition-property: stroke;
4239
}
@@ -49,7 +46,6 @@ $_mat-progress-spinner-circumference: $pi * $_mat-progress-spinner-radius * 2;
4946
infinite;
5047

5148
circle {
52-
stroke-dashoffset: (1 - 0.8) * $_mat-progress-spinner-circumference;
5349
transition-property: stroke;
5450
}
5551
}
@@ -63,11 +59,11 @@ $_mat-progress-spinner-circumference: $pi * $_mat-progress-spinner-radius * 2;
6359
}
6460

6561
@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%
62+
$start: (1 - 0.05) * $_mat-progress-spinner-default-circumference; // start the animation at 5%
63+
$end: (1 - 0.8) * $_mat-progress-spinner-default-circumference; // end the animation at 80%
6864
$fallback-iterations: 4;
6965

70-
@keyframes mat-progress-spinner-stroke-rotate {
66+
@keyframes mat-progress-spinner-stroke-rotate-100 {
7167
/*
7268
stylelint-disable declaration-block-single-line-max-declarations,
7369
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: 125 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,21 @@
77
*/
88

99
import {
10+
AfterViewInit,
1011
Component,
1112
ChangeDetectionStrategy,
1213
Input,
1314
ElementRef,
1415
Renderer2,
15-
Directive,
16-
ViewChild,
1716
SimpleChanges,
1817
OnChanges,
1918
ViewEncapsulation,
19+
Optional,
20+
Inject,
2021
} from '@angular/core';
2122
import {CanColor, mixinColor} from '@angular/material/core';
2223
import {Platform} from '@angular/cdk/platform';
24+
import {DOCUMENT} from '@angular/common';
2325

2426
/** Possible mode for a progress spinner. */
2527
export type ProgressSpinnerMode = 'determinate' | 'indeterminate';
@@ -31,6 +33,38 @@ export class MdProgressSpinnerBase {
3133
}
3234
export const _MdProgressSpinnerMixinBase = mixinColor(MdProgressSpinnerBase, 'primary');
3335

36+
/* tslint:disable:max-line-length */
37+
const INDETERMINATE_ANIMATION_TEMPLATE = `
38+
.mat-progress-spinner-DIAMETER.mat-progress-spinner-indeterminate-animation[mode="indeterminate"] circle {
39+
animation: mat-progress-spinner-stroke-rotate-DIAMETER 4s
40+
cubic-bezier(0.35, 0, 0.25, 1) infinite;
41+
transition-property: stroke;
42+
}
43+
44+
@keyframes mat-progress-spinner-stroke-rotate-DIAMETER {
45+
0% { stroke-dashoffset: START_VALUE; transform: rotate(0); }
46+
12.5% { stroke-dashoffset: END_VALUE; transform: rotate(0); }
47+
12.51% { stroke-dashoffset: END_VALUE; transform: rotateX(180deg) rotate(72.5deg); }
48+
25% { stroke-dashoffset: START_VALUE; transform: rotateX(180deg) rotate(72.5deg); }
49+
50+
25.1% { stroke-dashoffset: START_VALUE; transform: rotate(270deg); }
51+
37.5% { stroke-dashoffset: END_VALUE; transform: rotate(270deg); }
52+
37.51% { stroke-dashoffset: END_VALUE; transform: rotateX(180deg) rotate(161.5deg); }
53+
50% { stroke-dashoffset: START_VALUE; transform: rotateX(180deg) rotate(161.5deg); }
54+
55+
50.01% { stroke-dashoffset: START_VALUE; transform: rotate(180deg); }
56+
62.5% { stroke-dashoffset: END_VALUE; transform: rotate(180deg); }
57+
62.51% { stroke-dashoffset: END_VALUE; transform: rotateX(180deg) rotate(251.5deg); }
58+
75% { stroke-dashoffset: START_VALUE; transform: rotateX(180deg) rotate(251.5deg); }
59+
60+
75.01% { stroke-dashoffset: START_VALUE; transform: rotate(90deg); }
61+
87.5% { stroke-dashoffset: END_VALUE; transform: rotate(90deg); }
62+
87.51% { stroke-dashoffset: END_VALUE; transform: rotateX(180deg) rotate(341.5deg); }
63+
100% { stroke-dashoffset: START_VALUE; transform: rotateX(180deg) rotate(341.5deg); }
64+
}
65+
`;
66+
/* tslint:enable:max-line-length */
67+
3468
/**
3569
* <md-progress-spinner> component.
3670
*/
@@ -54,13 +88,30 @@ export const _MdProgressSpinnerMixinBase = mixinColor(MdProgressSpinnerBase, 'pr
5488
encapsulation: ViewEncapsulation.None,
5589
preserveWhitespaces: false,
5690
})
57-
export class MdProgressSpinner extends _MdProgressSpinnerMixinBase implements CanColor, OnChanges {
91+
export class MdProgressSpinner extends _MdProgressSpinnerMixinBase implements CanColor,
92+
OnChanges, AfterViewInit {
93+
5894
private _value: number;
5995
private readonly _baseSize = 100;
6096
private readonly _baseStrokeWidth = 10;
97+
private _fallbackAnimation = false;
6198

99+
/** The width and height of the host element. Will grow with stroke width. **/
62100
_elementSize = this._baseSize;
63-
_circleRadius = 45;
101+
102+
/** Tracks diameters of existing instances to de-dupe generated styles (default d = 100) */
103+
static diameters = new Set<number>([100]);
104+
105+
/** The diameter of the progress spinner (will set width and height of svg). */
106+
@Input()
107+
get diameter(): number {
108+
return this._diameter;
109+
}
110+
111+
set diameter(size: number) {
112+
this._setDiameterAndInitStyles(size);
113+
}
114+
_diameter = this._baseSize;
64115

65116
/** Stroke width of the progress spinner. */
66117
@Input() strokeWidth: number = 10;
@@ -79,31 +130,89 @@ export class MdProgressSpinner extends _MdProgressSpinnerMixinBase implements Ca
79130
}
80131
}
81132

82-
constructor(renderer: Renderer2, elementRef: ElementRef, platform: Platform) {
83-
super(renderer, elementRef);
133+
constructor(public _renderer: Renderer2, public _elementRef: ElementRef,
134+
platform: Platform, @Optional() @Inject(DOCUMENT) private _document: any) {
135+
super(_renderer, _elementRef);
136+
137+
this._fallbackAnimation = platform.EDGE || platform.TRIDENT;
84138

85-
// On IE and Edge we can't animate the `stroke-dashoffset`
139+
// On IE and Edge, we can't animate the `stroke-dashoffset`
86140
// reliably so we fall back to a non-spec animation.
87-
const animationClass = (platform.EDGE || platform.TRIDENT) ?
141+
const animationClass = this._fallbackAnimation ?
88142
'mat-progress-spinner-indeterminate-fallback-animation' :
89143
'mat-progress-spinner-indeterminate-animation';
90144

91-
renderer.addClass(elementRef.nativeElement, animationClass);
145+
_renderer.addClass(_elementRef.nativeElement, animationClass);
146+
}
147+
148+
ngAfterViewInit() {
149+
this._setDiameterClass(this.diameter);
92150
}
93151

94152
ngOnChanges(changes: SimpleChanges) {
95-
if (changes.strokeWidth) {
96-
this._elementSize = this._baseSize + Math.max(this.strokeWidth - this._baseStrokeWidth, 0);
153+
if (changes.strokeWidth || changes.diameter) {
154+
this._elementSize =
155+
this._diameter + Math.max(this.strokeWidth - this._baseStrokeWidth, 0);
97156
}
98157
}
99158

100-
_getStrokeDashOffset() {
159+
/** The radius of the spinner, adjusted for stroke width. */
160+
get _circleRadius() {
161+
return (this.diameter - this._baseStrokeWidth) / 2;
162+
}
163+
164+
/** The view box of the spinner's svg element. */
165+
get _viewBox() {
166+
return `0 0 ${this._elementSize} ${this._elementSize}`;
167+
}
168+
169+
/** The stroke circumference of the svg circle. */
170+
get _strokeCircumference(): number {
171+
return 2 * Math.PI * this._circleRadius;
172+
}
173+
174+
/** The dash offset of the svg circle. */
175+
get _strokeDashOffset() {
101176
if (this.mode === 'determinate') {
102-
return 2 * Math.PI * this._circleRadius * (100 - this._value) / 100;
177+
return this._strokeCircumference * (100 - this._value) / 100;
103178
}
104179

105180
return null;
106181
}
182+
183+
/** Sets the diameter and adds diameter-specific styles if necessary. */
184+
private _setDiameterAndInitStyles(size: number): void {
185+
this._setDiameterClass(size);
186+
this._diameter = size;
187+
if (!MdProgressSpinner.diameters.has(this.diameter) && !this._fallbackAnimation) {
188+
this._attachStyleNode();
189+
}
190+
}
191+
192+
/** Adds a diameter-specific class and removes any existing diameter classes. */
193+
private _setDiameterClass(size: number): void {
194+
this._renderer.removeClass(
195+
this._elementRef.nativeElement, `mat-progress-spinner-${this.diameter}`);
196+
this._renderer.addClass(
197+
this._elementRef.nativeElement, `mat-progress-spinner-${size}`);
198+
}
199+
200+
/** Dynamically generates a style tag containing the correct animation for this diameter. */
201+
private _attachStyleNode(): void {
202+
const styleTag = this._renderer.createElement('style');
203+
styleTag.textContent = this._getAnimationText();
204+
this._renderer.appendChild(this._document.head, styleTag);
205+
MdProgressSpinner.diameters.add(this.diameter);
206+
}
207+
208+
/** Generates animation styles adjusted for the spinner's diameter. */
209+
private _getAnimationText(): string {
210+
return INDETERMINATE_ANIMATION_TEMPLATE
211+
// Animation should begin at 5% and end at 80%
212+
.replace(/START_VALUE/g, `${0.95 * this._strokeCircumference}`)
213+
.replace(/END_VALUE/g, `${0.2 * this._strokeCircumference}`)
214+
.replace(/DIAMETER/g, `${this.diameter}`);
215+
}
107216
}
108217

109218

@@ -131,8 +240,9 @@ export class MdProgressSpinner extends _MdProgressSpinnerMixinBase implements Ca
131240
preserveWhitespaces: false,
132241
})
133242
export class MdSpinner extends MdProgressSpinner {
134-
constructor(renderer: Renderer2, elementRef: ElementRef, platform: Platform) {
135-
super(renderer, elementRef, platform);
243+
constructor(renderer: Renderer2, elementRef: ElementRef, platform: Platform,
244+
@Optional() @Inject(DOCUMENT) document: any) {
245+
super(renderer, elementRef, platform, document);
136246
this.mode = 'indeterminate';
137247
}
138248
}

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)