diff --git a/packages/oui-angular/src/index.js b/packages/oui-angular/src/index.js
index 214320dd..471a1e6e 100644
--- a/packages/oui-angular/src/index.js
+++ b/packages/oui-angular/src/index.js
@@ -34,6 +34,7 @@ import "@oui-angular/oui-page-header/src";
import "@oui-angular/oui-tile/src";
import "@oui-angular/oui-guide-menu/src";
import "@oui-angular/oui-header-tabs/src";
+import "@oui-angular/oui-progress/src";
angular.module("oui", [
"oui.button",
@@ -71,5 +72,6 @@ angular.module("oui", [
"oui.page-header",
"oui.tile",
"oui.guide-menu",
- "oui.header-tabs"
+ "oui.header-tabs",
+ "oui.progress"
]);
diff --git a/packages/oui-angular/src/index.spec.js b/packages/oui-angular/src/index.spec.js
index 624cda3e..3014b647 100644
--- a/packages/oui-angular/src/index.spec.js
+++ b/packages/oui-angular/src/index.spec.js
@@ -35,6 +35,7 @@ loadTests(require.context("../../oui-page-header/src/", true, /.*((\.spec)|(inde
loadTests(require.context("../../oui-tile/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-guide-menu/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-header-tabs/src/", true, /.*((\.spec)|(index))$/));
+loadTests(require.context("../../oui-progress/src/", true, /.*((\.spec)|(index))$/));
function loadTests (context) {
context.keys().forEach(context);
diff --git a/packages/oui-progress/README.md b/packages/oui-progress/README.md
new file mode 100644
index 00000000..c639db5c
--- /dev/null
+++ b/packages/oui-progress/README.md
@@ -0,0 +1,134 @@
+# oui-progress
+
+
+
+## Usage
+
+### Simple
+
+```html:preview
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### With custom labels
+
+```html:preview
+
+
+
+
+```
+
+### Custom max value
+
+```html:preview
+
+
+
+
+```
+
+### Stacked
+
+```html:preview
+
+
+
+
+```
+
+### Thresholds
+
+```html:preview
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Compact mode
+
+```html:preview
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+## API
+
+### oui-progress
+
+| Attribute | Type | Binding | One-time binding | Values | default | Description
+| ---- | ---- | ---- | ---- | ---- | ---- | ----
+| compact | Boolean | | yes | | false | compact mode flag
+| min-value | Number | @? | yes | | 0 | min value of progress component
+| max-value | Number | @? | yes | | 100 | max value of progress component
+
+### oui-progress-bar
+| Attribute | Type | Binding | One-time binding | Values | default | Description
+| ---- | ---- | ---- | ---- | ---- | ---- | ----
+| type | String | @ | yes | `info`, `success`, `warning`, `error` | `info` | type of the progress bar
+| value | Number | < | no | | | current value of progress bar
+| text | String | @? | yes | | value% | text of progress bar. If undefined, the current value is display as a percentage.
+
+### oui-progress-threshold
+
+| Attribute | Type | Binding | One-time binding | Values | default | Description
+| ---- | ---- | ---- | ---- | ---- | ---- | ----
+| value | Number | < | yes | | | value at which the threshold should appear
diff --git a/packages/oui-progress/src/bar/progress-bar.component.js b/packages/oui-progress/src/bar/progress-bar.component.js
new file mode 100644
index 00000000..66ba2fd0
--- /dev/null
+++ b/packages/oui-progress/src/bar/progress-bar.component.js
@@ -0,0 +1,15 @@
+import controller from "./progress-bar.controller";
+import template from "./progress-bar.html";
+
+export default {
+ template,
+ controller,
+ bindings: {
+ type: "@",
+ value: "<",
+ text: "@?"
+ },
+ require: {
+ progressCtrl: "^^ouiProgress"
+ }
+};
diff --git a/packages/oui-progress/src/bar/progress-bar.controller.js b/packages/oui-progress/src/bar/progress-bar.controller.js
new file mode 100644
index 00000000..cd8cff25
--- /dev/null
+++ b/packages/oui-progress/src/bar/progress-bar.controller.js
@@ -0,0 +1,55 @@
+import { addDefaultParameter } from "@oui-angular/common/component-utils";
+import get from "lodash/get";
+export default class {
+ constructor ($attrs, $element, $timeout) {
+ "ngInject";
+
+ this.$attrs = $attrs;
+ this.$element = $element;
+ this.$timeout = $timeout;
+ }
+
+ $onInit () {
+ addDefaultParameter(this, "type", "info");
+
+ this.compact = this.progressCtrl.compact;
+ this.minValue = this.progressCtrl.minValue;
+ this.maxValue = this.progressCtrl.maxValue;
+ }
+
+ $onChanges (changes) {
+ const value = get(changes, "value.currentValue");
+
+ this.$timeout(() => {
+ this.$element
+ .attr("ariaValuenow", value);
+
+ if (!this.compact) {
+ this.$element
+ .css("width", this.progressCtrl.getPercentageValue(value));
+ }
+ });
+ }
+
+ $postLink () {
+ this.$timeout(() => {
+ this.$element
+ .addClass("oui-progress__bar")
+ .addClass(`oui-progress__bar_${this.type}`)
+ .attr("ariaValuenow", this.value)
+ .attr("ariaValuemin", this.minValue)
+ .attr("ariaValuemax", this.maxValue)
+ .attr("role", "progressbar");
+
+ if (this.text) {
+ this.$element
+ .attr("ariaValuetext", this.text);
+ }
+
+ if (!this.compact) {
+ this.$element
+ .css("width", this.progressCtrl.getPercentageValue(this.value));
+ }
+ });
+ }
+}
diff --git a/packages/oui-progress/src/bar/progress-bar.html b/packages/oui-progress/src/bar/progress-bar.html
new file mode 100644
index 00000000..9970e2a6
--- /dev/null
+++ b/packages/oui-progress/src/bar/progress-bar.html
@@ -0,0 +1,3 @@
+
+
diff --git a/packages/oui-progress/src/index.js b/packages/oui-progress/src/index.js
new file mode 100644
index 00000000..f6cb9fa3
--- /dev/null
+++ b/packages/oui-progress/src/index.js
@@ -0,0 +1,8 @@
+import Progress from "./progress.component.js";
+import ProgressBar from "./bar/progress-bar.component.js";
+import ProgressThreshold from "./threshold/progress-threshold.component.js";
+
+angular.module("oui.progress", [])
+ .component("ouiProgress", Progress)
+ .component("ouiProgressBar", ProgressBar)
+ .component("ouiProgressThreshold", ProgressThreshold);
diff --git a/packages/oui-progress/src/index.spec.js b/packages/oui-progress/src/index.spec.js
new file mode 100644
index 00000000..532b28de
--- /dev/null
+++ b/packages/oui-progress/src/index.spec.js
@@ -0,0 +1,163 @@
+describe("ouiProgress", () => {
+ let TestUtils;
+ let $timeout;
+
+ const progressClass = "oui-progress";
+ const progressBarClass = `${progressClass}__bar`;
+ const progressLabelClass = `${progressClass}__label`;
+ const progressThresholdClass = `${progressClass}__threshold`;
+
+ beforeEach(angular.mock.module("oui.progress"));
+ beforeEach(angular.mock.module("oui.test-utils"));
+
+ beforeEach(inject((_TestUtils_, _$timeout_) => {
+ TestUtils = _TestUtils_;
+ $timeout = _$timeout_;
+ }));
+
+ describe("ouiProgress Component", () => {
+ function getProgressBarComponent (element, type) {
+ if (type) {
+ return angular.element(element[0].querySelector(`.${progressBarClass}_${type}`));
+ }
+
+ return angular.element(element[0].querySelector(`.${progressBarClass}`));
+ }
+
+ function getProgressBarLabel (element) {
+ return angular.element(element[0].querySelector(`.${progressLabelClass}`));
+ }
+
+ function getProgressThreshold (element) {
+ return angular.element(element[0].querySelector(`.${progressThresholdClass}`));
+ }
+
+ it("should display a progress", () => {
+ const element = TestUtils.compileTemplate(`
+
+
+ `
+ );
+
+ $timeout.flush();
+
+ expect(element.hasClass(progressClass)).toBeTruthy();
+ expect(getProgressBarComponent(element).length).toBe(1);
+ });
+
+ it("should display a compact progress bar", () => {
+ const element = TestUtils.compileTemplate(`
+
+
+ `
+ );
+
+ $timeout.flush();
+
+ expect(element.hasClass(`${progressClass}_compact`)).toBeTruthy();
+ });
+
+ it("should set type info by default", () => {
+ const element = TestUtils.compileTemplate(`
+
+
+ `
+ );
+
+ $timeout.flush();
+
+ expect(getProgressBarComponent(element, "info").length).toBe(1);
+ });
+
+ it("should display a progress bar of each type", () => {
+ const element = TestUtils.compileTemplate(`
+
+
+
+
+
+ `
+ );
+
+ $timeout.flush();
+
+ expect(getProgressBarComponent(element, "info").length).toBe(1);
+ expect(getProgressBarComponent(element, "success").length).toBe(1);
+ expect(getProgressBarComponent(element, "warning").length).toBe(1);
+ expect(getProgressBarComponent(element, "error").length).toBe(1);
+ });
+
+ it("should have the correct width set", () => {
+ const value = 5;
+ const element = TestUtils.compileTemplate(`
+
+
+ `
+ );
+
+ $timeout.flush();
+
+ expect(getProgressBarComponent(element).css("width")).toBe(`${value}%`);
+ });
+
+ it("should have the correct width when max-value is used", () => {
+ const value = 10;
+ const expectedWidth = value / 2;
+ const element = TestUtils.compileTemplate(`
+
+
+ `
+ );
+
+ $timeout.flush();
+
+ expect(getProgressBarComponent(element).css("width")).toBe(`${expectedWidth}%`);
+ });
+
+ it("should have the correct default label", () => {
+ const value = 5;
+ const element = TestUtils.compileTemplate(`
+
+
+ `
+ );
+
+ $timeout.flush();
+
+ expect(getProgressBarLabel(element).text()).toBe(`${value}%`);
+ });
+
+ it("should have the correct label", () => {
+ const value = 5;
+ const text = `Progress: ${value}%`;
+ const element = TestUtils.compileTemplate(`
+
+
+ `
+ );
+
+ $timeout.flush();
+
+ expect(getProgressBarLabel(element).text()).toBe(text);
+ });
+
+ describe("ouiProgressThreshold Component", () => {
+ it("should have the correct position according to value", () => {
+ const value = 10;
+ const maxValue = 200;
+ const leftPosition = value / (maxValue / 100);
+ const element = TestUtils.compileTemplate(`
+
+
+ `
+ );
+
+ $timeout.flush();
+
+ const thresholdEl = getProgressThreshold(element);
+ expect(thresholdEl.length).toBe(1);
+ expect(thresholdEl.css("left")).toBe(`${leftPosition}%`);
+ });
+ });
+ });
+});
diff --git a/packages/oui-progress/src/progress.component.js b/packages/oui-progress/src/progress.component.js
new file mode 100644
index 00000000..ee84eb62
--- /dev/null
+++ b/packages/oui-progress/src/progress.component.js
@@ -0,0 +1,10 @@
+import controller from "./progress.controller";
+
+export default {
+ controller,
+ bindings: {
+ compact: "",
+ minValue: "@?",
+ maxValue: "@?"
+ }
+};
diff --git a/packages/oui-progress/src/progress.controller.js b/packages/oui-progress/src/progress.controller.js
new file mode 100644
index 00000000..154f4503
--- /dev/null
+++ b/packages/oui-progress/src/progress.controller.js
@@ -0,0 +1,34 @@
+import { addBooleanParameter, addDefaultParameter } from "@oui-angular/common/component-utils";
+export default class {
+ constructor ($attrs, $element, $timeout) {
+ "ngInject";
+ this.$attrs = $attrs;
+ this.$element = $element;
+ this.$timeout = $timeout;
+ }
+
+ $onInit () {
+ addBooleanParameter(this, "compact");
+ addDefaultParameter(this, "minValue", "0");
+ addDefaultParameter(this, "maxValue", "100");
+ }
+
+ $postLink () {
+ this.$timeout(() => {
+ this.$element.addClass("oui-progress");
+
+ if (this.compact) {
+ this.$element.addClass("oui-progress_compact");
+ }
+ });
+ }
+
+ getPercentageValue (value) {
+ const percent = 100;
+ const minValue = this.minValue;
+ const maxValue = Math.max(this.maxValue - this.minValue, minValue);
+ const currentValue = Math.max(value - this.minValue, minValue);
+
+ return `${(currentValue / maxValue) * percent}%`;
+ }
+}
diff --git a/packages/oui-progress/src/threshold/progress-threshold.component.js b/packages/oui-progress/src/threshold/progress-threshold.component.js
new file mode 100644
index 00000000..ff6c68ef
--- /dev/null
+++ b/packages/oui-progress/src/threshold/progress-threshold.component.js
@@ -0,0 +1,11 @@
+import controller from "./progress-threshold.controller";
+
+export default {
+ controller,
+ bindings: {
+ value: "<"
+ },
+ require: {
+ progressCtrl: "^^ouiProgress"
+ }
+};
diff --git a/packages/oui-progress/src/threshold/progress-threshold.controller.js b/packages/oui-progress/src/threshold/progress-threshold.controller.js
new file mode 100644
index 00000000..9f1b64fa
--- /dev/null
+++ b/packages/oui-progress/src/threshold/progress-threshold.controller.js
@@ -0,0 +1,16 @@
+export default class {
+ constructor ($element, $timeout) {
+ "ngInject";
+
+ this.$element = $element;
+ this.$timeout = $timeout;
+ }
+
+ $postLink () {
+ this.$timeout(() =>
+ this.$element
+ .addClass("oui-progress__threshold")
+ .css("left", this.progressCtrl.getPercentageValue(this.value))
+ );
+ }
+}