feat(md-progress-circle): Create ProgressCircle

Closes #60
This commit is contained in:
josephperrott
2016-02-05 10:00:54 -08:00
committed by Jeremy Elbourn
parent 15fb40e2a5
commit 5ccbfb1718
11 changed files with 401 additions and 3 deletions
@@ -0,0 +1,110 @@
@import "variables";
@import "default-theme";
/* Animation Durations */
$md-progress-circle-duration : 5.25s !default;
$md-progress-circle-value-change-duration : $md-progress-circle-duration * 0.25 !default;
$md-progress-circle-constant-rotate-duration : $md-progress-circle-duration * 0.55 !default;
$md-progress-circle-sporadic-rotate-duration : $md-progress-circle-duration !default;
/** Component sizing */
$md-progress-circle-stroke-width: 10px !default;
$md-progress-circle-radius: 40px !default;
$md-progress-circle-circumference: $pi * $md-progress-circle-radius * 2 !default;
$md-progress-circle-center-point: 50px !default;
// Height and weight of the viewport for md-progress-circle.
$md-progress-circle-viewport-size : 100px !default;
:host {
display: block;
/** Height and width are provided for md-progress-circle to act as a default.
The height and width are expected to be overwritten by application css. */
height: $md-progress-circle-viewport-size;
width: $md-progress-circle-viewport-size;
/** SVG's viewBox is defined as 0 0 100 100, this means that all SVG children will placed
based on a 100px by 100px box.
The SVG and Circle dimensions/location:
SVG
Height: 100px
Width: 100px
Circle
Radius: 40px
Circumference: 251.3274px
Center x: 50px
Center y: 50px
*/
svg {
height: 100%;
width: 100%;
}
circle {
cx: $md-progress-circle-center-point;
cy: $md-progress-circle-center-point;
fill: transparent;
r: $md-progress-circle-radius;
stroke: md-color($md-primary, 600);
/** Stroke width of 10px defines stroke as 10% of the viewBox */
stroke-width: $md-progress-circle-stroke-width;
/** SVG circle rotations begin rotated 90deg clockwise from the circle's center top */
transform: rotate(-90deg);
transform-origin: center;
transition: stroke-dashoffset 0.225s linear;
/** The dash array of the circle is defined as the circumference of the circle. */
stroke-dasharray: $md-progress-circle-circumference;
/** The stroke dashoffset is used to "fill" the circle, 0px represents an full circle,
while the circles full circumference represents an empty circle. */
stroke-dashoffset: 0px;
}
&.md-accent circle {
stroke: md-color($md-accent, 600);
}
&.md-warn circle {
stroke: md-color($md-warn, 600);
}
&[mode="indeterminate"] circle {
animation-duration: $md-progress-circle-sporadic-rotate-duration,
$md-progress-circle-constant-rotate-duration,
$md-progress-circle-value-change-duration;
animation-name: md-progress-circle-sporadic-rotate,
md-progress-circle-linear-rotate,
md-progress-circle-value-change;
animation-timing-function: $ease-in-out-curve-function,
linear,
$ease-in-out-curve-function;
animation-iteration-count: infinite;
transition: none;
}
}
/** Animations for indeterminate mode */
@keyframes md-progress-circle-linear-rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes md-progress-circle-sporadic-rotate {
12.5% { transform: rotate( 135deg); }
25% { transform: rotate( 270deg); }
37.5% { transform: rotate( 405deg); }
50% { transform: rotate( 540deg); }
62.5% { transform: rotate( 675deg); }
75% { transform: rotate( 810deg); }
87.5% { transform: rotate( 945deg); }
100% { transform: rotate(1080deg); }
}
@keyframes md-progress-circle-value-change {
0% { stroke-dashoffset: 261.3274px; }
100% { stroke-dashoffset: -241.3274px; }
}
@@ -0,0 +1,9 @@
<!--
preserveAspectRatio of xMidYMid meet as the center of the viewport is the circle's
center. The center of the circle with remain at the center of the md-progress-circle
element containing the SVG.
-->
<svg viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid meet">
<circle [style.strokeDashoffset]="strokeDashOffset()"></circle>
</svg>

After

Width:  |  Height:  |  Size: 354 B

@@ -0,0 +1,99 @@
import {beforeEach, ddescribe, expect, inject, it, TestComponentBuilder} from 'angular2/testing';
import {Component, DebugElement} from 'angular2/core';
import {By} from 'angular2/platform/browser'
import {MdProgressCircle} from './progress_circle';
export function main() {
describe('MdProgressCircular', () => {
let builder:TestComponentBuilder;
beforeEach(inject([TestComponentBuilder], (tcb:TestComponentBuilder) => {
builder = tcb;
}));
it('should apply a mode of "determinate" if no mode is provided.', (done:() => void) => {
builder
.overrideTemplate(TestApp, '<md-progress-circle></md-progress-circle>')
.createAsync(TestApp)
.then((fixture) => {
fixture.detectChanges();
let progressElement = getChildDebugElement(fixture.debugElement, 'md-progress-circle');
expect(progressElement.componentInstance.mode).toBe('determinate');
done();
});
});
it('should apply a mode of "determinate" if an invalid mode is provided.', (done:() => void) => {
builder
.overrideTemplate(TestApp, '<md-progress-circle mode="spinny"></md-progress-circle>')
.createAsync(TestApp)
.then((fixture) => {
fixture.detectChanges();
let progressElement = getChildDebugElement(fixture.debugElement, 'md-progress-circle');
expect(progressElement.componentInstance.mode).toBe('determinate');
done();
});
});
it('should not modify the mode if a valid mode is provided.', (done:() => void) => {
builder
.overrideTemplate(TestApp, '<md-progress-circle mode="indeterminate"></md-progress-circle>')
.createAsync(TestApp)
.then((fixture) => {
fixture.detectChanges();
let progressElement = getChildDebugElement(fixture.debugElement, 'md-progress-circle');
expect(progressElement.componentInstance.mode).toBe('indeterminate');
done();
});
});
it('should define a default value for the value attribute', (done:() => void) => {
builder
.overrideTemplate(TestApp, '<md-progress-circle></md-progress-circle>')
.createAsync(TestApp)
.then((fixture) => {
fixture.detectChanges();
let progressElement = getChildDebugElement(fixture.debugElement, 'md-progress-circle');
expect(progressElement.componentInstance.value).toBe(0);
done();
});
});
it('should clamp the value of the progress between 0 and 100', (done:() => void) => {
builder
.overrideTemplate(TestApp, '<md-progress-circle></md-progress-circle>')
.createAsync(TestApp)
.then((fixture) => {
fixture.detectChanges();
let progressElement = getChildDebugElement(fixture.debugElement, 'md-progress-circle');
let progressComponent = progressElement.componentInstance;
progressComponent.value = 50;
expect(progressComponent.value).toBe(50);
progressComponent.value = 999;
expect(progressComponent.value).toBe(100);
progressComponent.value = -10;
expect(progressComponent.value).toBe(0);
done();
});
});
});
}
/** Gets a child DebugElement by tag name. */
function getChildDebugElement(parent: DebugElement, selector: string): DebugElement {
return parent.query(By.css(selector));
}
/** Test component that contains an MdButton. */
@Component({
directives: [MdProgressCircle],
template: '',
})
class TestApp {}
@@ -0,0 +1,123 @@
import {
Attribute,
Component,
ChangeDetectionStrategy,
ElementRef,
HostBinding,
Input,
ViewEncapsulation,
} from 'angular2/core';
import {isPresent, CONST} from 'angular2/src/facade/lang';
import {OneOf} from '../../core/annotations/one-of';
// TODO(josephperrott): Benchpress tests.
/** Display modes of Progress Circle */
@CONST()
class ProgressMode {
@CONST() static DETERMINATE = 'determinate';
@CONST() static INDETERMINATE = 'indeterminate';
}
/**
* <md-progress-circle> component.
*/
@Component({
selector: 'md-progress-circle',
host: {
'role': 'progressbar',
'aria-valuemin': '0',
'aria-valuemax': '100',
},
templateUrl: './components/progress-circle/progress_circle.html',
styleUrls: ['./components/progress-circle/progress-circle.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MdProgressCircle {
/**
* Value of the progress circle.
*
* Input:number, defaults to 0.
* value_ is bound to the host as the attribute aria-valuenow.
*/
@Input('value')
value_: number = 0;
@HostBinding('attr.aria-valuenow')
get _value() {
return this.value_;
}
/**
* Mode of the progress circle
*
* Input must be one of the values from ProgressMode, defaults to 'determinate'.
* mode is bound to the host as the attribute host.
*/
@Input()
@OneOf([ProgressMode.DETERMINATE, ProgressMode.INDETERMINATE])
mode: string;
@HostBinding('attr.mode')
get _mode() {
return this.mode;
}
/**
* Gets the current stroke dash offset to represent the progress circle.
*
* The stroke dash offset specifies the distance between dashes in the circle's stroke.
* Setting the offset to a percentage of the total circumference of the circle, fills this
* percentage of the overall circumference of the circle.
*/
strokeDashOffset() {
// To determine how far the offset should be, we multiple the current percentage by the
// total circumference.
// The total circumference is calculated based on the radius we use, 45.
// PI * 2 * 45
return 251.3274 * (100 - this.value_) / 100;
}
/** Gets the progress value, returning the clamped value. */
get value() {
return this.value_;
}
/** Sets the progress value, clamping before setting the internal value. */
set value(v: number) {
if (isPresent(v)) {
this.value_ = MdProgressCircle.clamp(v);
}
}
/** Clamps a value to be between 0 and 100. */
static clamp(v: number) {
return Math.max(0, Math.min(100, v));
}
}
/**
* <md-spinner> component.
*
* This is a component definition to be used as a convenience reference to create an
* indeterminate <md-progress-circle> instance.
*/
@Component({
selector: 'md-spinner',
host: {
'role': 'progressbar',
},
templateUrl: './components/progress-circle/progress_circle.html',
styleUrls: ['./components/progress-circle/progress-circle.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MdSpinner extends MdProgressCircle {
mode: string = ProgressMode.INDETERMINATE;
}
+1 -1
View File
@@ -6,7 +6,7 @@
$md-is-dark-theme: false;
$md-primary: md-palette($md-indigo, 500, 100, 700, $md-contrast-palettes);
$md-primary: md-palette($md-blue, 500, 100, 700, $md-contrast-palettes);
$md-accent: md-palette($md-red, A200, A100, A400, $md-contrast-palettes);
$md-warn: md-palette($md-orange, 500, 300, 800, $md-contrast-palettes);
$md-foreground: if($md-is-dark-theme, $md-dark-theme-foreground, $md-light-theme-foreground);
+7 -1
View File
@@ -12,8 +12,14 @@ $md-xsmall: "max-width: 600px";
$z-index-fab: 20 !default;
$z-index-drawer: 100 !default;
// Global constants
$pi: 3.14159265;
// Easing Curves
// TODO(jelbourn): all of these need to be revisited
$ease-in-out-curve-function: cubic-bezier(0.35, 0, 0.25, 1) !default;
$swift-ease-out-duration: 0.4s !default;
$swift-ease-out-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
$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;
$swift-ease-in: all $swift-ease-in-duration $swift-ease-in-timing-function !default;
$swift-ease-in-out-duration: 0.5s !default;
$swift-ease-in-out-timing-function: cubic-bezier(0.35, 0, 0.25, 1) !default;
$swift-ease-in-out-timing-function: $ease-in-out-curve-function !default;
$swift-ease-in-out: all $swift-ease-in-out-duration $swift-ease-in-out-timing-function !default;
+1
View File
@@ -4,6 +4,7 @@
<li><a [routerLink]="['ButtonDemo']">Button demo</a></li>
<li><a [routerLink]="['CardDemo']">Card demo</a></li>
<li><a [routerLink]="['SidenavDemo']">Sidenav demo</a></li>
<li><a [routerLink]="['ProgressCircleDemo']">Progress Circle demo</a></li>
</ul>
<button md-raised-button (click)="root.dir = (root.dir == 'rtl' ? 'ltr' : 'rtl')">
{{root.dir.toUpperCase()}}
+3 -1
View File
@@ -2,6 +2,7 @@ import {Component} from 'angular2/core';
import {CardDemo} from './card/card-demo';
import {ButtonDemo} from './button/button-demo';
import {SidenavDemo} from './sidenav/sidenav-demo';
import {ProgressCircleDemo} from './progress-circle/progress-circle-demo';
import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router';
import {Dir} from '../directives/dir/dir';
import {MdButton} from '../components/button/button';
@@ -24,6 +25,7 @@ export class Home {}
{path: '/', name: 'Home', component: Home, useAsDefault: true},
{path: '/button', name: 'ButtonDemo', component: ButtonDemo},
{path: '/card', name: 'CardDemo', component: CardDemo},
{path: '/sidenav', name: 'SidenavDemo', component: SidenavDemo}
{path: '/sidenav', name: 'SidenavDemo', component: SidenavDemo},
{path: '/progress-circle', name: 'ProgressCircleDemo', component: ProgressCircleDemo},
])
export class DemoApp { }
@@ -0,0 +1,21 @@
<h1>Determinate</h1>
<div class="demo-progress-circle-container">
<md-progress-circle mode="determinate"
[value]="progressValue"
class="md-primary"></md-progress-circle>
<md-progress-circle [value]="progressValue"
class="md-accent"></md-progress-circle>
</div>
<span>Value: {{progressValue}}</span>
<button md-raised-button (click)="step(10)">Increase</button>
<button md-raised-button (click)="step(-10)">Decrease</button>
<h1>Indeterminate</h1>
<div class="demo-progress-circle-container">
<md-progress-circle mode="indeterminate"></md-progress-circle>
<md-progress-circle mode="indeterminate"
class="md-accent"></md-progress-circle>
<md-spinner></md-spinner>
</div>
@@ -0,0 +1,9 @@
.demo-progress-circle-container {
width: 100%;
md-progress-circle,
md-spinner {
display: inline-block;
}
}
@@ -0,0 +1,18 @@
import {Component} from 'angular2/core';
import {MdButton} from '../../components/button/button';
import {MdProgressCircle, MdSpinner} from '../../components/progress-circle/progress_circle';
@Component({
selector: 'progress-circle-demo',
templateUrl: 'demo-app/progress-circle/progress-circle-demo.html',
styleUrls: ['demo-app/progress-circle/progress-circle-demo.css'],
directives: [MdProgressCircle, MdSpinner, MdButton]
})
export class ProgressCircleDemo {
progressValue: number = 40;
step(val: number) {
this.progressValue += val;
}
}