Skip to content

Commit 5ccbfb1

Browse files
josephperrottjelbourn
authored andcommitted
feat(md-progress-circle): Create ProgressCircle
Closes #60
1 parent 15fb40e commit 5ccbfb1

File tree

11 files changed

+401
-3
lines changed

11 files changed

+401
-3
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
@import "variables";
2+
3+
@import "default-theme";
4+
5+
/* Animation Durations */
6+
$md-progress-circle-duration : 5.25s !default;
7+
$md-progress-circle-value-change-duration : $md-progress-circle-duration * 0.25 !default;
8+
$md-progress-circle-constant-rotate-duration : $md-progress-circle-duration * 0.55 !default;
9+
$md-progress-circle-sporadic-rotate-duration : $md-progress-circle-duration !default;
10+
11+
/** Component sizing */
12+
$md-progress-circle-stroke-width: 10px !default;
13+
$md-progress-circle-radius: 40px !default;
14+
$md-progress-circle-circumference: $pi * $md-progress-circle-radius * 2 !default;
15+
$md-progress-circle-center-point: 50px !default;
16+
// Height and weight of the viewport for md-progress-circle.
17+
$md-progress-circle-viewport-size : 100px !default;
18+
19+
20+
:host {
21+
display: block;
22+
/** Height and width are provided for md-progress-circle to act as a default.
23+
The height and width are expected to be overwritten by application css. */
24+
height: $md-progress-circle-viewport-size;
25+
width: $md-progress-circle-viewport-size;
26+
27+
/** SVG's viewBox is defined as 0 0 100 100, this means that all SVG children will placed
28+
based on a 100px by 100px box.
29+
30+
The SVG and Circle dimensions/location:
31+
SVG
32+
Height: 100px
33+
Width: 100px
34+
Circle
35+
Radius: 40px
36+
Circumference: 251.3274px
37+
Center x: 50px
38+
Center y: 50px
39+
*/
40+
svg {
41+
height: 100%;
42+
width: 100%;
43+
}
44+
45+
46+
circle {
47+
cx: $md-progress-circle-center-point;
48+
cy: $md-progress-circle-center-point;
49+
fill: transparent;
50+
r: $md-progress-circle-radius;
51+
stroke: md-color($md-primary, 600);
52+
/** Stroke width of 10px defines stroke as 10% of the viewBox */
53+
stroke-width: $md-progress-circle-stroke-width;
54+
/** SVG circle rotations begin rotated 90deg clockwise from the circle's center top */
55+
transform: rotate(-90deg);
56+
transform-origin: center;
57+
transition: stroke-dashoffset 0.225s linear;
58+
/** The dash array of the circle is defined as the circumference of the circle. */
59+
stroke-dasharray: $md-progress-circle-circumference;
60+
/** The stroke dashoffset is used to "fill" the circle, 0px represents an full circle,
61+
while the circles full circumference represents an empty circle. */
62+
stroke-dashoffset: 0px;
63+
}
64+
65+
66+
&.md-accent circle {
67+
stroke: md-color($md-accent, 600);
68+
}
69+
70+
71+
&.md-warn circle {
72+
stroke: md-color($md-warn, 600);
73+
}
74+
75+
76+
&[mode="indeterminate"] circle {
77+
animation-duration: $md-progress-circle-sporadic-rotate-duration,
78+
$md-progress-circle-constant-rotate-duration,
79+
$md-progress-circle-value-change-duration;
80+
animation-name: md-progress-circle-sporadic-rotate,
81+
md-progress-circle-linear-rotate,
82+
md-progress-circle-value-change;
83+
animation-timing-function: $ease-in-out-curve-function,
84+
linear,
85+
$ease-in-out-curve-function;
86+
animation-iteration-count: infinite;
87+
transition: none;
88+
}
89+
}
90+
91+
92+
/** Animations for indeterminate mode */
93+
@keyframes md-progress-circle-linear-rotate {
94+
0% { transform: rotate(0deg); }
95+
100% { transform: rotate(360deg); }
96+
}
97+
@keyframes md-progress-circle-sporadic-rotate {
98+
12.5% { transform: rotate( 135deg); }
99+
25% { transform: rotate( 270deg); }
100+
37.5% { transform: rotate( 405deg); }
101+
50% { transform: rotate( 540deg); }
102+
62.5% { transform: rotate( 675deg); }
103+
75% { transform: rotate( 810deg); }
104+
87.5% { transform: rotate( 945deg); }
105+
100% { transform: rotate(1080deg); }
106+
}
107+
@keyframes md-progress-circle-value-change {
108+
0% { stroke-dashoffset: 261.3274px; }
109+
100% { stroke-dashoffset: -241.3274px; }
110+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!--
2+
preserveAspectRatio of xMidYMid meet as the center of the viewport is the circle's
3+
center. The center of the circle with remain at the center of the md-progress-circle
4+
element containing the SVG.
5+
-->
6+
<svg viewBox="0 0 100 100"
7+
preserveAspectRatio="xMidYMid meet">
8+
<circle [style.strokeDashoffset]="strokeDashOffset()"></circle>
9+
</svg>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import {beforeEach, ddescribe, expect, inject, it, TestComponentBuilder} from 'angular2/testing';
2+
import {Component, DebugElement} from 'angular2/core';
3+
import {By} from 'angular2/platform/browser'
4+
import {MdProgressCircle} from './progress_circle';
5+
6+
7+
export function main() {
8+
describe('MdProgressCircular', () => {
9+
let builder:TestComponentBuilder;
10+
11+
beforeEach(inject([TestComponentBuilder], (tcb:TestComponentBuilder) => {
12+
builder = tcb;
13+
}));
14+
15+
it('should apply a mode of "determinate" if no mode is provided.', (done:() => void) => {
16+
builder
17+
.overrideTemplate(TestApp, '<md-progress-circle></md-progress-circle>')
18+
.createAsync(TestApp)
19+
.then((fixture) => {
20+
fixture.detectChanges();
21+
let progressElement = getChildDebugElement(fixture.debugElement, 'md-progress-circle');
22+
expect(progressElement.componentInstance.mode).toBe('determinate');
23+
done();
24+
});
25+
});
26+
27+
it('should apply a mode of "determinate" if an invalid mode is provided.', (done:() => void) => {
28+
builder
29+
.overrideTemplate(TestApp, '<md-progress-circle mode="spinny"></md-progress-circle>')
30+
.createAsync(TestApp)
31+
.then((fixture) => {
32+
fixture.detectChanges();
33+
let progressElement = getChildDebugElement(fixture.debugElement, 'md-progress-circle');
34+
expect(progressElement.componentInstance.mode).toBe('determinate');
35+
done();
36+
});
37+
});
38+
39+
it('should not modify the mode if a valid mode is provided.', (done:() => void) => {
40+
builder
41+
.overrideTemplate(TestApp, '<md-progress-circle mode="indeterminate"></md-progress-circle>')
42+
.createAsync(TestApp)
43+
.then((fixture) => {
44+
fixture.detectChanges();
45+
let progressElement = getChildDebugElement(fixture.debugElement, 'md-progress-circle');
46+
expect(progressElement.componentInstance.mode).toBe('indeterminate');
47+
done();
48+
});
49+
});
50+
51+
it('should define a default value for the value attribute', (done:() => void) => {
52+
builder
53+
.overrideTemplate(TestApp, '<md-progress-circle></md-progress-circle>')
54+
.createAsync(TestApp)
55+
.then((fixture) => {
56+
fixture.detectChanges();
57+
let progressElement = getChildDebugElement(fixture.debugElement, 'md-progress-circle');
58+
expect(progressElement.componentInstance.value).toBe(0);
59+
done();
60+
});
61+
});
62+
63+
it('should clamp the value of the progress between 0 and 100', (done:() => void) => {
64+
builder
65+
.overrideTemplate(TestApp, '<md-progress-circle></md-progress-circle>')
66+
.createAsync(TestApp)
67+
.then((fixture) => {
68+
fixture.detectChanges();
69+
let progressElement = getChildDebugElement(fixture.debugElement, 'md-progress-circle');
70+
let progressComponent = progressElement.componentInstance;
71+
72+
progressComponent.value = 50;
73+
expect(progressComponent.value).toBe(50);
74+
75+
progressComponent.value = 999;
76+
expect(progressComponent.value).toBe(100);
77+
78+
progressComponent.value = -10;
79+
expect(progressComponent.value).toBe(0);
80+
done();
81+
});
82+
});
83+
});
84+
}
85+
86+
87+
/** Gets a child DebugElement by tag name. */
88+
function getChildDebugElement(parent: DebugElement, selector: string): DebugElement {
89+
return parent.query(By.css(selector));
90+
}
91+
92+
93+
94+
/** Test component that contains an MdButton. */
95+
@Component({
96+
directives: [MdProgressCircle],
97+
template: '',
98+
})
99+
class TestApp {}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import {
2+
Attribute,
3+
Component,
4+
ChangeDetectionStrategy,
5+
ElementRef,
6+
HostBinding,
7+
Input,
8+
ViewEncapsulation,
9+
} from 'angular2/core';
10+
import {isPresent, CONST} from 'angular2/src/facade/lang';
11+
import {OneOf} from '../../core/annotations/one-of';
12+
13+
14+
// TODO(josephperrott): Benchpress tests.
15+
16+
/** Display modes of Progress Circle */
17+
@CONST()
18+
class ProgressMode {
19+
@CONST() static DETERMINATE = 'determinate';
20+
@CONST() static INDETERMINATE = 'indeterminate';
21+
}
22+
23+
24+
/**
25+
* <md-progress-circle> component.
26+
*/
27+
@Component({
28+
selector: 'md-progress-circle',
29+
host: {
30+
'role': 'progressbar',
31+
'aria-valuemin': '0',
32+
'aria-valuemax': '100',
33+
},
34+
templateUrl: './components/progress-circle/progress_circle.html',
35+
styleUrls: ['./components/progress-circle/progress-circle.css'],
36+
changeDetection: ChangeDetectionStrategy.OnPush,
37+
})
38+
export class MdProgressCircle {
39+
/**
40+
* Value of the progress circle.
41+
*
42+
* Input:number, defaults to 0.
43+
* value_ is bound to the host as the attribute aria-valuenow.
44+
*/
45+
@Input('value')
46+
value_: number = 0;
47+
@HostBinding('attr.aria-valuenow')
48+
get _value() {
49+
return this.value_;
50+
}
51+
52+
/**
53+
* Mode of the progress circle
54+
*
55+
* Input must be one of the values from ProgressMode, defaults to 'determinate'.
56+
* mode is bound to the host as the attribute host.
57+
*/
58+
@Input()
59+
@OneOf([ProgressMode.DETERMINATE, ProgressMode.INDETERMINATE])
60+
mode: string;
61+
@HostBinding('attr.mode')
62+
get _mode() {
63+
return this.mode;
64+
}
65+
66+
67+
/**
68+
* Gets the current stroke dash offset to represent the progress circle.
69+
*
70+
* The stroke dash offset specifies the distance between dashes in the circle's stroke.
71+
* Setting the offset to a percentage of the total circumference of the circle, fills this
72+
* percentage of the overall circumference of the circle.
73+
*/
74+
strokeDashOffset() {
75+
// To determine how far the offset should be, we multiple the current percentage by the
76+
// total circumference.
77+
78+
// The total circumference is calculated based on the radius we use, 45.
79+
// PI * 2 * 45
80+
return 251.3274 * (100 - this.value_) / 100;
81+
}
82+
83+
84+
/** Gets the progress value, returning the clamped value. */
85+
get value() {
86+
return this.value_;
87+
}
88+
89+
90+
/** Sets the progress value, clamping before setting the internal value. */
91+
set value(v: number) {
92+
if (isPresent(v)) {
93+
this.value_ = MdProgressCircle.clamp(v);
94+
}
95+
}
96+
97+
98+
/** Clamps a value to be between 0 and 100. */
99+
static clamp(v: number) {
100+
return Math.max(0, Math.min(100, v));
101+
}
102+
}
103+
104+
105+
106+
/**
107+
* <md-spinner> component.
108+
*
109+
* This is a component definition to be used as a convenience reference to create an
110+
* indeterminate <md-progress-circle> instance.
111+
*/
112+
@Component({
113+
selector: 'md-spinner',
114+
host: {
115+
'role': 'progressbar',
116+
},
117+
templateUrl: './components/progress-circle/progress_circle.html',
118+
styleUrls: ['./components/progress-circle/progress-circle.css'],
119+
changeDetection: ChangeDetectionStrategy.OnPush,
120+
})
121+
export class MdSpinner extends MdProgressCircle {
122+
mode: string = ProgressMode.INDETERMINATE;
123+
}

src/core/style/_default-theme.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
$md-is-dark-theme: false;
77

88

9-
$md-primary: md-palette($md-indigo, 500, 100, 700, $md-contrast-palettes);
9+
$md-primary: md-palette($md-blue, 500, 100, 700, $md-contrast-palettes);
1010
$md-accent: md-palette($md-red, A200, A100, A400, $md-contrast-palettes);
1111
$md-warn: md-palette($md-orange, 500, 300, 800, $md-contrast-palettes);
1212
$md-foreground: if($md-is-dark-theme, $md-dark-theme-foreground, $md-light-theme-foreground);

src/core/style/_variables.scss

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@ $md-xsmall: "max-width: 600px";
1212
$z-index-fab: 20 !default;
1313
$z-index-drawer: 100 !default;
1414

15+
// Global constants
16+
$pi: 3.14159265;
17+
1518
// Easing Curves
1619
// TODO(jelbourn): all of these need to be revisited
20+
21+
$ease-in-out-curve-function: cubic-bezier(0.35, 0, 0.25, 1) !default;
22+
1723
$swift-ease-out-duration: 0.4s !default;
1824
$swift-ease-out-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
1925
$swift-ease-out: all $swift-ease-out-duration $swift-ease-out-timing-function !default;
@@ -23,5 +29,5 @@ $swift-ease-in-timing-function: cubic-bezier(0.55, 0, 0.55, 0.2) !default;
2329
$swift-ease-in: all $swift-ease-in-duration $swift-ease-in-timing-function !default;
2430

2531
$swift-ease-in-out-duration: 0.5s !default;
26-
$swift-ease-in-out-timing-function: cubic-bezier(0.35, 0, 0.25, 1) !default;
32+
$swift-ease-in-out-timing-function: $ease-in-out-curve-function !default;
2733
$swift-ease-in-out: all $swift-ease-in-out-duration $swift-ease-in-out-timing-function !default;

src/demo-app/demo-app.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ <h1 class="title">Angular Material2 Demos</h1>
44
<li><a [routerLink]="['ButtonDemo']">Button demo</a></li>
55
<li><a [routerLink]="['CardDemo']">Card demo</a></li>
66
<li><a [routerLink]="['SidenavDemo']">Sidenav demo</a></li>
7+
<li><a [routerLink]="['ProgressCircleDemo']">Progress Circle demo</a></li>
78
</ul>
89
<button md-raised-button (click)="root.dir = (root.dir == 'rtl' ? 'ltr' : 'rtl')">
910
{{root.dir.toUpperCase()}}

0 commit comments

Comments
 (0)