Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions src/components/progress-circle/progress-circle.scss
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;
Copy link
Member

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?

Copy link
Member Author

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.

/** 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0px > 0

}


&.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; }
}
9 changes: 9 additions & 0 deletions src/components/progress-circle/progress_circle.html
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">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment that explains why this particular preserveAspectRatio value is used?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

<circle [style.strokeDashoffset]="strokeDashOffset()"></circle>
</svg>
99 changes: 99 additions & 0 deletions src/components/progress-circle/progress_circle.spec.ts
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));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't need this function any more; you can instead:

import {By} from 'angular2/platform/browser';
...
fixture.debugElement.query(By.css('selector'))

(see the button spec)

Copy link
Member Author

Choose a reason for hiding this comment

The 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 {}
123 changes: 123 additions & 0 deletions src/components/progress-circle/progress_circle.ts
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'],
Copy link
Member

Choose a reason for hiding this comment

The 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 md-spinner

(see #67 for details)

Copy link
Member Author

Choose a reason for hiding this comment

The 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')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need this @HostBinding

Copy link
Member Author

Choose a reason for hiding this comment

The 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)

Copy link
Member

Choose a reason for hiding this comment

The 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;
}
2 changes: 1 addition & 1 deletion src/core/style/_default-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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-red, 500, 300, 800, $md-contrast-palettes);
$md-foreground: if($md-is-dark-theme, $md-dark-theme-foreground, $md-light-theme-foreground);
Expand Down
8 changes: 7 additions & 1 deletion src/core/style/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 change: 1 addition & 0 deletions src/demo-app/demo-app.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ <h1 class="title">Angular Material2 Demos</h1>
<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()}}
Expand Down
Loading