-
Notifications
You must be signed in to change notification settings - Fork 6.8k
feat(md-progress-circle) #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
|
||
|
||
&.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; } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add a comment that explains why this particular There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
<circle [style.strokeDashoffset]="strokeDashOffset()"></circle> | ||
</svg> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You shouldn't need this function any more; you can instead:
(see the button spec) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
|
||
|
||
|
||
/** Test component that contains an MdButton. */ | ||
@Component({ | ||
directives: [MdProgressCircle], | ||
template: '', | ||
}) | ||
class TestApp {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I forgot this earlier, but can you add this to the component config: changeDetection: ChangeDetectionStrategy.OnPush, Here and on (see #67 for details) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think you need this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From my understanding if we do not do a HostBinding here, then if the mode is changed by the component it will not send out the change without the host binding. For instance, when an attempt is made to update to an invalid progress mode it changes it to a valid mode (determinate, specifically) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, right, I forgot you're using the mode attribute for css. In the future we might want to revisit that due to IE11's performance with attribute selectors. |
||
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; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where does this
0.225s
come from?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that it was based initially off of the 0.25s value from material1, but 0.225s "felt correct" for the transition length.
I was not able to find anything in spec about the transition length.