feat(progress-bar): initial progress-bar impl. (#130)

Closes #40
This commit is contained in:
Joey Perrott
2016-04-08 13:26:01 -07:00
committed by Jeremy Elbourn
parent 1a33a5b99d
commit c640f0c866
9 changed files with 608 additions and 1 deletions
@@ -0,0 +1,5 @@
<!-- The background div is named as such because it appears below the other divs and is not sized based on values. -->
<div class="md-progress-bar-background"></div>
<div class="md-progress-bar-buffer" [style.transform]="bufferTransform()"></div>
<div class="md-progress-bar-primary md-progress-bar-fill" [style.transform]="primaryTransform()"></div>
<div class="md-progress-bar-secondary md-progress-bar-fill"></div>
@@ -0,0 +1,297 @@
@import "variables";
@import "default-theme";
$md-progress-bar-height: 5px !default;
$md-progress-bar-full-animation-duration: 2s !default;
$md-progress-bar-piece-animation-duration: 250ms !default;
// TODO(josephperrott): Find better way to inline svgs.
/** In buffer mode a repeated SVG object is used as a background. Each of the following defines the SVG object for each
of the class defined colors.
Each string is a URL encoded version of:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1" x="0px" y="0px" enable-background="new 0 0 5 2"
xml:space="preserve" viewBox="0 0 5 2" preserveAspectRatio="none slice">
<circle cx="1" cy="1" r="1" fill="{INJECTED_COLOR}"/>
</svg>
*/
$md-buffer-bubbles-primary: (
"data:image/svg+xml;charset=UTF-8,%3Csvg%20version%3D%271.1%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27" +
"%20xmlns%3Axlink%3D%27http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%27%20x%3D%270px%27%20y%3D%270px%27%20enable-backgroun" +
"d%3D%27new%200%200%205%202%27%20xml%3Aspace%3D%27preserve%27%20viewBox%3D%270%200%205%202%27%20preserveAspectRatio" +
"%3D%27none%20slice%27%3E%3Ccircle%20cx%3D%271%27%20cy%3D%271%27%20r%3D%271%27%20fill%3D%27" +
md-color($md-primary, 100) + "%27%2F%3E%3C%2Fsvg%3E") !default;
$md-buffer-bubbles-accent: (
"data:image/svg+xml;charset=UTF-8,%3Csvg%20version%3D%271.1%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27" +
"%20xmlns%3Axlink%3D%27http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%27%20x%3D%270px%27%20y%3D%270px%27%20enable-backgroun" +
"d%3D%27new%200%200%205%202%27%20xml%3Aspace%3D%27preserve%27%20viewBox%3D%270%200%205%202%27%20preserveAspectRatio" +
"%3D%27none%20slice%27%3E%3Ccircle%20cx%3D%271%27%20cy%3D%271%27%20r%3D%271%27%20fill%3D%27" +
md-color($md-accent, 100) + "%27%2F%3E%3C%2Fsvg%3E") !default;
$md-buffer-bubbles-warn: (
"data:image/svg+xml;charset=UTF-8,%3Csvg%20version%3D%271.1%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27" +
"%20xmlns%3Axlink%3D%27http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%27%20x%3D%270px%27%20y%3D%270px%27%20enable-backgroun" +
"d%3D%27new%200%200%205%202%27%20xml%3Aspace%3D%27preserve%27%20viewBox%3D%270%200%205%202%27%20preserveAspectRatio" +
"%3D%27none%20slice%27%3E%3Ccircle%20cx%3D%271%27%20cy%3D%271%27%20r%3D%271%27%20fill%3D%27" +
md-color($md-warn, 100) + "%27%2F%3E%3C%2Fsvg%3E") !default;
:host {
display: block;
// Height is provided for md-progress-bar to act as a default.
height: $md-progress-bar-height;
overflow: hidden;
position: relative;
// translateZ is added to force the md-progress-bar into its own GPU layer.
transform: translateZ(0);
transition: opacity $md-progress-bar-piece-animation-duration linear;
width: 100%;
// The progress bar background is used to show the bubble animation scrolling behind a buffering progress bar.
.md-progress-bar-background {
background: url($md-buffer-bubbles-primary);
background-repeat: repeat-x;
background-size: 10px 4px;
height: 100%;
position: absolute;
visibility: hidden;
width: 100%;
}
/**
* The progress bar buffer is the bar indicator showing the buffer value and is only visible beyond the current value
* of the primary progress bar.
*/
.md-progress-bar-buffer {
background-color: md-color($md-primary, 100);
height: 100%;
position: absolute;
transform-origin: top left;
transition: transform $md-progress-bar-piece-animation-duration ease;
width: 100%;
}
/**
* The secondary progress bar is only used in the indeterminate animation, because of this it is hidden in other uses.
*/
.md-progress-bar-secondary {
visibility: hidden;
}
/**
* The progress bar fill fills the progress bar with the indicator color.
*/
.md-progress-bar-fill {
animation: none;
height: 100%;
position: absolute;
transform-origin: top left;
transition: transform $md-progress-bar-piece-animation-duration ease;
width: 100%;
}
/**
* A pseudo element is created for each progress bar bar that fills with the indicator color.
*/
.md-progress-bar-fill::after {
animation: none;
background-color: md-color($md-primary, 600);
content: '';
display: inline-block;
height: 100%;
position: absolute;
width: 100%;
}
&[color="accent"] {
.md-progress-bar-background {
background: url($md-buffer-bubbles-accent);
background-repeat: repeat-x;
background-size: 10px 4px;
}
.md-progress-bar-buffer {
background-color: md-color($md-accent, 100);
}
.md-progress-bar-fill::after {
background-color: md-color($md-accent, 600);
}
}
&[color="warn"] {
.md-progress-bar-background {
background: url($md-buffer-bubbles-warn);
background-repeat: repeat-x;
background-size: 10px 4px;
}
.md-progress-bar-buffer {
background-color: md-color($md-warn, 100);
}
.md-progress-bar-fill::after {
background-color: md-color($md-warn, 600);
}
}
&[mode="query"] {
transform: rotateZ(180deg);
}
&[mode="indeterminate"],
&[mode="query"] {
.md-progress-bar-fill {
transition: none;
}
.md-progress-bar-primary {
animation: md-progress-bar-primary-indeterminate-translate $md-progress-bar-full-animation-duration infinite linear;
left: -145.166611%;
}
.md-progress-bar-primary.md-progress-bar-fill::after {
animation: md-progress-bar-primary-indeterminate-scale $md-progress-bar-full-animation-duration infinite linear;
}
.md-progress-bar-secondary {
animation: md-progress-bar-secondary-indeterminate-translate $md-progress-bar-full-animation-duration infinite linear;
left: -54.888891%;
visibility: visible;
}
.md-progress-bar-secondary.md-progress-bar-fill::after {
animation: md-progress-bar-secondary-indeterminate-scale $md-progress-bar-full-animation-duration infinite linear;
}
}
&[mode="buffer"] {
.md-progress-bar-background {
animation: md-progress-bar-background-scroll $md-progress-bar-piece-animation-duration infinite linear;
visibility: visible;
}
}
}
// Reverse the apparent directionality of progress vars for rtl.
:host-context([dir="rtl"]) {
transform: rotateY(180deg);
}
/** The values used for animations in md-progress-bar, both timing and transformation, can be considered magic values.
They are sourced from the Material Design example spec and duplicate the values of the original designers
definitions.
The indeterminate state is essentially made up of two progress bars, one primary (the one that is shown in both the
determinate and indeterminate states) and one secondary, which essentially mirrors the primary progress bar in
appearance but is only shown to assist with the indeterminate animations.
KEYFRAME BLOCK DESCRIPTION
primary-indeterminate-translate Translation of the primary progressbar across the screen
primary-indeterminate-scale Scaling of the primary progressbar as it's being translated across the screen
secondary-indeterminate-translate Translation of the secondary progressbar across the screen
secondary-indeterminate-scale Scaling of the secondary progressbar as it's being translated across the screen
Because two different transform animations need to be applied at once, the translation is applied to the outer
element and the scaling is applied to the inner element, which provides the illusion necessary to make the animation
work.
*/
// Progress Bar Timing functions:
// $md-progress-bar-primary-indeterminate-translate-step-1 has no timing function.
$md-progress-bar-primary-indeterminate-translate-step-2: cubic-bezier(.5, 0, .701732, .495819) !default;
$md-progress-bar-primary-indeterminate-translate-step-3: cubic-bezier(.302435, .381352, .55, .956352) !default;
// $md-progress-bar-primary-indeterminate-translate-step-4 has no timing function.
// $md-progress-bar-primary-indeterminate-scale-step-1 has no timing function
$md-progress-bar-primary-indeterminate-scale-step-2: cubic-bezier(.334731, .124820, .785844, 1) !default;
$md-progress-bar-primary-indeterminate-scale-step-3: cubic-bezier(.06, .11, .6, 1) !default;
// $md-progress-bar-primary-indeterminate-scale-step-4 has no timing function
$md-progress-bar-secondary-indeterminate-translate-step-1: cubic-bezier(.15, 0, .515058, .409685) !default;
$md-progress-bar-secondary-indeterminate-translate-step-2: cubic-bezier(.310330, .284058, .8, .733712) !default;
$md-progress-bar-secondary-indeterminate-translate-step-3: cubic-bezier(.4, .627035, .6, .902026) !default;
// $md-progress-bar-secondary-indeterminate-translate-step-4 has no timing function
$md-progress-bar-secondary-indeterminate-scale-step-1: cubic-bezier(.15, 0, .515058, .409685) !default;
$md-progress-bar-secondary-indeterminate-scale-step-2: cubic-bezier(.310330, .284058, .8, .733712) !default;
$md-progress-bar-secondary-indeterminate-scale-step-3: cubic-bezier(.4, .627035, .6, .902026) !default;
// $md-progress-bar-secondary-indeterminate-scale-step-4 has no timing function
/** Animations for indeterminate and query mode. */
// Primary indicator.
@keyframes md-progress-bar-primary-indeterminate-translate {
0% {
transform: translateX(0px);
}
20% {
animation-timing-function: $md-progress-bar-primary-indeterminate-translate-step-2;
transform: translateX(0px);
}
59.15% {
animation-timing-function: $md-progress-bar-primary-indeterminate-translate-step-3;
transform: translateX(83.67142%);
}
100% {
transform: translateX(200.611057%);
}
}
@keyframes md-progress-bar-primary-indeterminate-scale {
0% {
transform: scaleX(.08);
}
36.65% {
animation-timing-function: $md-progress-bar-primary-indeterminate-scale-step-2;
transform: scaleX(.08);
}
69.15% {
animation-timing-function: $md-progress-bar-primary-indeterminate-scale-step-3;
transform: scaleX(.661479);
}
100% {
transform: scaleX(.08);
}
}
// Secondary indicator.
@keyframes md-progress-bar-secondary-indeterminate-translate {
0% {
animation-timing-function: $md-progress-bar-secondary-indeterminate-translate-step-1;
transform: translateX(0px);
}
25% {
animation-timing-function: $md-progress-bar-secondary-indeterminate-translate-step-2;
transform: translateX(37.651913%);
}
48.35% {
animation-timing-function: $md-progress-bar-secondary-indeterminate-translate-step-3;
transform: translateX(84.386165%);
}
100% {
transform: translateX(160.277782%);
}
}
@keyframes md-progress-bar-secondary-indeterminate-scale {
0% {
animation-timing-function: $md-progress-bar-secondary-indeterminate-scale-step-1;
transform: scaleX(.08);
}
19.15% {
animation-timing-function: $md-progress-bar-secondary-indeterminate-scale-step-2;
transform: scaleX(.457104);
}
44.15% {
animation-timing-function: $md-progress-bar-secondary-indeterminate-scale-step-3;
transform: scaleX(.727960);
}
100% {
transform: scaleX(.08);
}
}
/** Animation for buffer mode. */
@keyframes md-progress-bar-background-scroll {
to {
transform: translateX(-10px);
}
}
@@ -0,0 +1,124 @@
import {beforeEach, describe, expect, inject, it, TestComponentBuilder} from 'angular2/testing';
import {Component} from 'angular2/core';
import {By} from 'angular2/platform/browser';
import {MdProgressBar} from './progress-bar';
export function main() {
describe('MdProgressBar', () => {
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-bar></md-progress-bar>')
.createAsync(TestApp)
.then((fixture) => {
fixture.detectChanges();
let progressElement = fixture.debugElement.query(By.css('md-progress-bar'));
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-bar mode="buffer"></md-progress-bar>')
.createAsync(TestApp)
.then((fixture) => {
fixture.detectChanges();
let progressElement = fixture.debugElement.query(By.css('md-progress-bar'));
expect(progressElement.componentInstance.mode).toBe('buffer');
done();
});
});
it('should define default values for value and bufferValue attributes', (done: () => void) => {
builder
.overrideTemplate(TestApp, '<md-progress-bar></md-progress-bar>')
.createAsync(TestApp)
.then((fixture) => {
fixture.detectChanges();
let progressElement = fixture.debugElement.query(By.css('md-progress-bar'));
expect(progressElement.componentInstance.value).toBe(0);
expect(progressElement.componentInstance.bufferValue).toBe(0);
done();
});
});
it('should clamp value and bufferValue between 0 and 100', (done: () => void) => {
builder
.overrideTemplate(TestApp, '<md-progress-bar></md-progress-bar>')
.createAsync(TestApp)
.then((fixture) => {
fixture.detectChanges();
let progressElement = fixture.debugElement.query(By.css('md-progress-bar'));
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);
progressComponent.bufferValue = -29;
expect(progressComponent.bufferValue).toBe(0);
progressComponent.bufferValue = 9;
expect(progressComponent.bufferValue).toBe(9);
progressComponent.bufferValue = 1320;
expect(progressComponent.bufferValue).toBe(100);
done();
});
});
it('should return the transform attribute for bufferValue and mode', (done: () => void) => {
builder
.overrideTemplate(TestApp, '<md-progress-bar></md-progress-bar>')
.createAsync(TestApp)
.then((fixture) => {
fixture.detectChanges();
let progressElement = fixture.debugElement.query(By.css('md-progress-bar'));
let progressComponent = progressElement.componentInstance;
expect(progressComponent.primaryTransform()).toBe('scaleX(0)');
expect(progressComponent.bufferTransform()).toBe(undefined);
progressComponent.value = 40;
expect(progressComponent.primaryTransform()).toBe('scaleX(0.4)');
expect(progressComponent.bufferTransform()).toBe(undefined);
progressComponent.value = 35;
progressComponent.bufferValue = 55;
expect(progressComponent.primaryTransform()).toBe('scaleX(0.35)');
expect(progressComponent.bufferTransform()).toBe(undefined);
progressComponent.mode = 'buffer';
expect(progressComponent.primaryTransform()).toBe('scaleX(0.35)');
expect(progressComponent.bufferTransform()).toBe('scaleX(0.55)');
progressComponent.value = 60;
progressComponent.bufferValue = 60;
expect(progressComponent.primaryTransform()).toBe('scaleX(0.6)');
expect(progressComponent.bufferTransform()).toBe('scaleX(0.6)');
done();
});
});
});
}
/** Test component that contains an MdButton. */
@Component({
directives: [MdProgressBar],
template: '',
})
class TestApp {}
+100
View File
@@ -0,0 +1,100 @@
import {
Component,
ChangeDetectionStrategy,
HostBinding,
Input,
} from 'angular2/core';
import {isPresent} from 'angular2/src/facade/lang';
// TODO(josephperrott): Benchpress tests.
// TODO(josephperrott): Add ARIA attributes for progressbar "for".
/**
* <md-progress-bar> component.
*/
@Component({
selector: 'md-progress-bar',
host: {
'role': 'progressbar',
'aria-valuemin': '0',
'aria-valuemax': '100',
},
templateUrl: './components/progress-bar/progress-bar.html',
styleUrls: ['./components/progress-bar/progress-bar.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MdProgressBar {
/**
* Value of the progressbar.
*
* Defaults to zero. Mirrored to aria-valuenow.
*/
private _value: number = 0;
@Input()
@HostBinding('attr.aria-valuenow')
get value() {
return this._value;
}
set value(v: number) {
if (isPresent(v)) {
this._value = MdProgressBar.clamp(v);
}
}
/**
* Buffer value of the progress bar.
*
* Defaults to zero.
*/
private _bufferValue: number = 0;
@Input()
get bufferValue() {
return this._bufferValue;
}
set bufferValue(v: number) {
if (isPresent(v)) {
this._bufferValue = MdProgressBar.clamp(v);
}
}
/**
* Mode of the progress bar.
*
* Input must be one of these values: determinate, indeterminate, buffer, query, defaults to
* 'determinate'.
* Mirrored to mode attribute.
*/
@Input()
@HostBinding('attr.mode')
mode: 'determinate' | 'indeterminate' | 'buffer' | 'query' = 'determinate';
/** Gets the current transform value for the progress bar's primary indicator. */
primaryTransform() {
let scale = this.value / 100;
return `scaleX(${scale})`;
}
/**
* Gets the current transform value for the progress bar's buffer indicator. Only used if the
* progress mode is set to buffer, otherwise returns an undefined, causing no transformation.
*/
bufferTransform() {
if (this.mode == 'buffer') {
let scale = this.bufferValue / 100;
return `scaleX(${scale})`;
}
}
/** Clamps a value to be between two numbers, by default 0 and 100. */
static clamp(v: number, min = 0, max = 100) {
return Math.max(min, Math.min(max, v));
}
}
+2 -1
View File
@@ -3,8 +3,8 @@
<ul>
<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>
<li><a [routerLink]="['ProgressBarDemo']">Progress Bar demo</a></li>
<li><a [routerLink]="['PortalDemo']">Portal demo</a></li>
<li><a [routerLink]="['OverlayDemo']">Overlay demo</a></li>
<li><a [routerLink]="['CheckboxDemo']">Checkbox demo</a></li>
@@ -13,6 +13,7 @@
<li><a [routerLink]="['RadioDemo']">Radio demo</a></li>
<li><a [routerLink]="['ListDemo']">List demo</a></li>
<li><a [routerLink]="['LiveAnnouncerDemo']">Live Announcer demo</a></li>
<li><a [routerLink]="['SidenavDemo']">Sidenav demo</a></li>
</ul>
<button md-raised-button (click)="root.dir = (root.dir == 'rtl' ? 'ltr' : 'rtl')">
{{root.dir.toUpperCase()}}
+2
View File
@@ -4,6 +4,7 @@ import {CardDemo} from './card/card-demo';
import {ButtonDemo} from './button/button-demo';
import {RadioDemo} from './radio/radio-demo';
import {SidenavDemo} from './sidenav/sidenav-demo';
import {ProgressBarDemo} from './progress-bar/progress-bar-demo';
import {ProgressCircleDemo} from './progress-circle/progress-circle-demo';
import {CheckboxDemo} from './checkbox/checkbox-demo';
import {Dir} from '../core/rtl/dir';
@@ -37,6 +38,7 @@ export class Home {}
new Route({path: '/radio', name: 'RadioDemo', component: RadioDemo}),
new Route({path: '/sidenav', name: 'SidenavDemo', component: SidenavDemo}),
new Route({path: '/progress-circle', name: 'ProgressCircleDemo', component: ProgressCircleDemo}),
new Route({path: '/progress-bar', name: 'ProgressBarDemo', component: ProgressBarDemo}),
new Route({path: '/portal', name: 'PortalDemo', component: PortalDemo}),
new Route({path: '/overlay', name: 'OverlayDemo', component: OverlayDemo}),
new Route({path: '/checkbox', name: 'CheckboxDemo', component: CheckboxDemo}),
@@ -0,0 +1,37 @@
<h1>Determinate</h1>
<div class="demo-progress-bar-container">
<md-progress-bar mode="determinate"
[value]="determinateProgressValue"
color="primary"
class="demo-progress-bar-margins"></md-progress-bar>
</div>
<span>Value: {{determinateProgressValue}}</span>
<button md-raised-button (click)="stepDeterminateProgressVal(10)">Increase</button>
<button md-raised-button (click)="stepDeterminateProgressVal(-10)">Decrease</button>
<h1>Buffer</h1>
<div class="demo-progress-bar-container">
<md-progress-bar [value]="bufferProgressValue"
[bufferValue]="bufferBufferValue"
mode="buffer"
color="warn"
class="demo-progress-bar-margins"></md-progress-bar>
</div>
<span>Value: {{bufferProgressValue}}</span>
<button md-raised-button (click)="stepBufferProgressVal(10)">Increase</button>
<button md-raised-button (click)="stepBufferProgressVal(-10)">Decrease</button>
<span class="demo-progress-bar-spacer"></span>
<span>Buffer Value: {{bufferBufferValue}}</span>
<button md-raised-button (click)="stepBufferBufferVal(10)">Increase</button>
<button md-raised-button (click)="stepBufferBufferVal(-10)">Decrease</button>
<h1>Indeterminate</h1>
<div class="demo-progress-bar-container">
<md-progress-bar mode="indeterminate"
class="demo-progress-bar-margins"></md-progress-bar>
</div>
<h1>Query</h1>
<div class="demo-progress-bar-container">
<md-progress-bar mode="query"
class="demo-progress-bar-margins"></md-progress-bar>
</div>
@@ -0,0 +1,12 @@
.demo-progress-bar-container {
width: 100%;
}
.demo-progress-bar-margins {
margin: 20px 0;
}
.demo-progress-bar-spacer {
display: inline-block;
width: 50px;
}
@@ -0,0 +1,29 @@
import {Component} from 'angular2/core';
import {MdButton} from '../../components/button/button';
import {MdProgressBar} from '../../components/progress-bar/progress-bar';
// TODO(josephperrott): Add an automatically filling example progress bar.
@Component({
selector: 'progress-bar-demo',
templateUrl: 'demo-app/progress-bar/progress-bar-demo.html',
styleUrls: ['demo-app/progress-bar/progress-bar-demo.css'],
directives: [MdProgressBar, MdButton]
})
export class ProgressBarDemo {
determinateProgressValue: number = 30;
bufferProgressValue: number = 30;
bufferBufferValue: number = 40;
stepDeterminateProgressVal(val: number) {
this.determinateProgressValue += val;
}
stepBufferProgressVal(val: number) {
this.bufferProgressValue += val;
}
stepBufferBufferVal(val: number) {
this.bufferBufferValue += val;
}
}