diff --git a/src/components/progress-bar/progress-bar.html b/src/components/progress-bar/progress-bar.html new file mode 100644 index 000000000000..6bfa16cc1fa7 --- /dev/null +++ b/src/components/progress-bar/progress-bar.html @@ -0,0 +1,5 @@ + +
+
+
+
diff --git a/src/components/progress-bar/progress-bar.scss b/src/components/progress-bar/progress-bar.scss new file mode 100644 index 000000000000..b3388f8d2a02 --- /dev/null +++ b/src/components/progress-bar/progress-bar.scss @@ -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: + + + + + + */ +$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); + } +} diff --git a/src/components/progress-bar/progress-bar.spec.ts b/src/components/progress-bar/progress-bar.spec.ts new file mode 100644 index 000000000000..8e7932d16386 --- /dev/null +++ b/src/components/progress-bar/progress-bar.spec.ts @@ -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, '') + .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, '') + .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, '') + .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, '') + .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, '') + .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 {} diff --git a/src/components/progress-bar/progress-bar.ts b/src/components/progress-bar/progress-bar.ts new file mode 100644 index 000000000000..d07581d5ff05 --- /dev/null +++ b/src/components/progress-bar/progress-bar.ts @@ -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". + + +/** + * 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)); + } +} diff --git a/src/demo-app/demo-app.html b/src/demo-app/demo-app.html index 1b2a4e037789..c7e961ce53ba 100644 --- a/src/demo-app/demo-app.html +++ b/src/demo-app/demo-app.html @@ -3,8 +3,8 @@

Angular Material2 Demos

+ +

Buffer

+
+ +
+Value: {{bufferProgressValue}} + + + +Buffer Value: {{bufferBufferValue}} + + + +

Indeterminate

+
+ +
+ +

Query

+
+ +
diff --git a/src/demo-app/progress-bar/progress-bar-demo.scss b/src/demo-app/progress-bar/progress-bar-demo.scss new file mode 100644 index 000000000000..b6de28f90cb5 --- /dev/null +++ b/src/demo-app/progress-bar/progress-bar-demo.scss @@ -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; +} \ No newline at end of file diff --git a/src/demo-app/progress-bar/progress-bar-demo.ts b/src/demo-app/progress-bar/progress-bar-demo.ts new file mode 100644 index 000000000000..72162a5ff854 --- /dev/null +++ b/src/demo-app/progress-bar/progress-bar-demo.ts @@ -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; + } +}