diff --git a/src/components/progress-circle/progress-circle.scss b/src/components/progress-circle/progress-circle.scss new file mode 100644 index 00000000000..c314d1fe530 --- /dev/null +++ b/src/components/progress-circle/progress-circle.scss @@ -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; } +} diff --git a/src/components/progress-circle/progress_circle.html b/src/components/progress-circle/progress_circle.html new file mode 100644 index 00000000000..2f9bc9b636d --- /dev/null +++ b/src/components/progress-circle/progress_circle.html @@ -0,0 +1,9 @@ + + + + diff --git a/src/components/progress-circle/progress_circle.spec.ts b/src/components/progress-circle/progress_circle.spec.ts new file mode 100644 index 00000000000..5198f0fc5b4 --- /dev/null +++ b/src/components/progress-circle/progress_circle.spec.ts @@ -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, '') + .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, '') + .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, '') + .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, '') + .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, '') + .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 {} \ No newline at end of file diff --git a/src/components/progress-circle/progress_circle.ts b/src/components/progress-circle/progress_circle.ts new file mode 100644 index 00000000000..ffe9982728a --- /dev/null +++ b/src/components/progress-circle/progress_circle.ts @@ -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'; +} + + +/** + * 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)); + } +} + + + +/** + * component. + * + * This is a component definition to be used as a convenience reference to create an + * indeterminate 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; +} diff --git a/src/core/style/_default-theme.scss b/src/core/style/_default-theme.scss index 198589e37f1..24624767c82 100644 --- a/src/core/style/_default-theme.scss +++ b/src/core/style/_default-theme.scss @@ -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); diff --git a/src/core/style/_variables.scss b/src/core/style/_variables.scss index b4a50ae55f1..5d89815cc06 100644 --- a/src/core/style/_variables.scss +++ b/src/core/style/_variables.scss @@ -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; diff --git a/src/demo-app/demo-app.html b/src/demo-app/demo-app.html index 48dacf16d1d..18fb54d3bac 100644 --- a/src/demo-app/demo-app.html +++ b/src/demo-app/demo-app.html @@ -4,6 +4,7 @@
  • Button demo
  • Card demo
  • Sidenav demo
  • +
  • Progress Circle demo
  • + + + + +

    Indeterminate

    +
    + + + +
    diff --git a/src/demo-app/progress-circle/progress-circle-demo.scss b/src/demo-app/progress-circle/progress-circle-demo.scss new file mode 100644 index 00000000000..f93b650636e --- /dev/null +++ b/src/demo-app/progress-circle/progress-circle-demo.scss @@ -0,0 +1,9 @@ +.demo-progress-circle-container { + width: 100%; + + md-progress-circle, + md-spinner { + display: inline-block; + } + +} \ No newline at end of file diff --git a/src/demo-app/progress-circle/progress-circle-demo.ts b/src/demo-app/progress-circle/progress-circle-demo.ts new file mode 100644 index 00000000000..c908aafabe5 --- /dev/null +++ b/src/demo-app/progress-circle/progress-circle-demo.ts @@ -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; + } + +} \ No newline at end of file