+
-
-
-
-
-
-
-
-
-
- |
-
-
-
- |
- |
-
-
-
-
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+ |
+
+
+
-
-
-
-
+
+
+
diff --git a/packages/oui-datagrid/src/index.js b/packages/oui-datagrid/src/index.js
index e21aaf88..6c543f43 100644
--- a/packages/oui-datagrid/src/index.js
+++ b/packages/oui-datagrid/src/index.js
@@ -1,4 +1,6 @@
import Cell from "./cell/cell.component";
+import Checkbox from "@ovh-ui/oui-checkbox";
+import Criteria from "@ovh-ui/oui-criteria";
import Datagrid from "./datagrid.directive";
import DatagridColumnBuilder from "./datagrid-column-builder.service";
import DatagridExtraTop from "./extra-top/extra-top.component";
@@ -6,14 +8,16 @@ import DatagridPaging from "./paging/datagrid-paging.service";
import DatagridParameters from "./parameters/datagrid-parameters.component";
import DatagridProvider from "./datagrid.provider";
import DatagridService from "./datagrid.service";
+import Pagination from "@ovh-ui/oui-pagination";
+import Spinner from "@ovh-ui/oui-spinner";
export default angular
.module("oui.datagrid", [
- "oui.pagination",
- "oui.dropdown",
- "oui.criteria-container",
- "oui.search",
- "ngAria"
+ "ngAria",
+ Checkbox,
+ Criteria,
+ Pagination,
+ Spinner
])
.service("ouiDatagridColumnBuilder", DatagridColumnBuilder)
.directive("ouiDatagrid", Datagrid)
diff --git a/packages/oui-datagrid/src/index.spec.js b/packages/oui-datagrid/src/index.spec.js
index 7eeb6b07..6c2d23c6 100644
--- a/packages/oui-datagrid/src/index.spec.js
+++ b/packages/oui-datagrid/src/index.spec.js
@@ -28,7 +28,6 @@ describe("ouiDatagrid", () => {
beforeEach(angular.mock.module("oui.datagrid"));
beforeEach(angular.mock.module("oui.test-utils"));
beforeEach(angular.mock.module("oui.action-menu"));
- beforeEach(angular.mock.module("oui.checkbox"));
beforeEach(inject((_TestUtils_, _$rootScope_, _$timeout_, _ouiDatagridService_) => {
TestUtils = _TestUtils_;
diff --git a/packages/oui-datagrid/tests/index.js b/packages/oui-datagrid/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-datagrid/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-dropdown/tests/index.js b/packages/oui-dropdown/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-dropdown/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-dual-list/tests/index.js b/packages/oui-dual-list/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-dual-list/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-field/README.md b/packages/oui-field/README.md
index 8b740adb..81651370 100644
--- a/packages/oui-field/README.md
+++ b/packages/oui-field/README.md
@@ -172,13 +172,23 @@
+
+
+
+
+
-
+ required
+ multiple>
diff --git a/packages/oui-field/src/field.controller.js b/packages/oui-field/src/field.controller.js
index 88eff445..4d1fd7e2 100644
--- a/packages/oui-field/src/field.controller.js
+++ b/packages/oui-field/src/field.controller.js
@@ -192,7 +192,7 @@ export default class FieldController {
}
getMessageString (errorName) {
- return (this.errorMessages && this.errorMessages[errorName]) || this.ouiFieldConfiguration.translations.errors[errorName];
+ return (this.errorMessages && this.errorMessages[errorName]) || this.ouiFieldConfiguration.translations.errors[errorName] || this.ouiFieldConfiguration.translations.errors.invalid;
}
getErrorMessage (errorName) {
diff --git a/packages/oui-field/src/field.provider.js b/packages/oui-field/src/field.provider.js
index 738e1d56..c215c568 100644
--- a/packages/oui-field/src/field.provider.js
+++ b/packages/oui-field/src/field.provider.js
@@ -4,9 +4,11 @@ export default class {
constructor () {
this.translations = {
errors: {
+ invalid: "Invalid field.",
required: "Mandatory.",
number: "Invalid number.",
email: "Invalid email.",
+ password: "Invalid password.",
min: "Too low ({{min}} min).",
max: "Too high ({{max}} max).",
minlength: "Too short ({{minlength}} characters min).",
diff --git a/packages/oui-field/tests/index.js b/packages/oui-field/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-field/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-file/tests/index.js b/packages/oui-file/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-file/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-form-actions/tests/index.js b/packages/oui-form-actions/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-form-actions/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-guide-menu/tests/index.js b/packages/oui-guide-menu/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-guide-menu/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-header-tabs/tests/index.js b/packages/oui-header-tabs/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-header-tabs/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-inline-adder/tests/index.js b/packages/oui-inline-adder/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-inline-adder/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-message/tests/index.js b/packages/oui-message/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-message/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-modal/tests/index.js b/packages/oui-modal/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-modal/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-navbar/tests/index.js b/packages/oui-navbar/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-navbar/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-numeric/tests/index.js b/packages/oui-numeric/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-numeric/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-page-header/tests/index.js b/packages/oui-page-header/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-page-header/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-pagination/tests/index.js b/packages/oui-pagination/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-pagination/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-password/README.md b/packages/oui-password/README.md
new file mode 100644
index 00000000..4647e5f0
--- /dev/null
+++ b/packages/oui-password/README.md
@@ -0,0 +1,143 @@
+# oui-password
+
+
+
+## Usage
+
+### Basic
+
+```html:preview
+
+```
+
+### Placeholder
+
+```html:preview
+
+```
+
+### Disabled
+
+```html:preview
+
+```
+
+### Form validation
+
+```html:preview
+
+```
+
+### Password rules & strength
+
+
+ The component doesn't include any password strength estimator. You can use one like zxcvbn to provide a score, like in this example.
+
+
+```html:preview
+
+```
+
+#### Custom strength feedback
+
+The feedback of password strength can be overridden by adding your custom feedback in `oui-password-strength`.
+It can also be globally changed with `ouiPasswordProvider` (see **Configuration** below).
+
+```html:preview
+
+
+ Score 4: Etiam volutpat congue odio imperdiet tincidunt.
+ Score 3: Suspendisse vehicula ut nisl non laoreet.
+ Score 2: Curabitur malesuada mi lectus, eget pharetra erat malesuada sed.
+ Score 1: Vestibulum pulvinar congue lacus sed ultricies.
+ Score 0: Lorem ipsum dolor sit amet.
+
+
+```
+
+#### Score scale
+
+`oui-password-strength`'s score scale is based on
zxcvbn scale:
+
+* `0`: Risky password,
+* `1`: Bad password,
+* `2`: Weak password,
+* `3`: Good password,
+* `4`: Strong password
+
+## API
+
+### oui-password
+
+| Attribute | Type | Binding | One-time binding | Values | Default | Description
+| ---- | ---- | ---- | ---- | ---- | ---- | ----
+| `model` | string | = | no | n/a | n/a | model bound to component
+| `id` | string | @? | yes | n/a | n/a | id attribute of the input
+| `name` | string | @? | yes | n/a | n/a | name attritebu of the input
+| `placeholder` | string | @? | yes | `true`, `false` | `false` | placeholder text
+| `disabled` | boolean | | no | n/a | n/a | disabled flag
+| `maxlength` | number | | yes | n/a | n/a | max length of the model value
+| `minlength` | number | | yes | n/a | n/a | min length of the model value
+| `pattern` | string<regexp> | @? | yes | n/a | n/a | pattern of the model value
+| `required` | boolean | | no | `true`, `false` | `false` | required flag
+| `on-change` | function | & | no | n/a | n/a | handler triggered when value has changed
+
+### oui-rule
+
+| Attribute | Type | Binding | One-time binding | Values | Default | Description
+| ---- | ---- | ---- | ---- | ---- | ---- | ----
+| `pattern` | string<regexp> | @? | no | n/a | n/a | pattern of the model value
+| `validator` | function | & | no | n/a | n/a | validator function to test the password value; should return a boolean
+
+### oui-strength
+
+| Attribute | Type | Binding | One-time binding | Values | Default | Description
+| ---- | ---- | ---- | ---- | ---- | ---- | ----
+| `score` | number | | no | See **Score scale** | n/a | score provided by a password strength estimator
+
+## Configuration
+
+The password translations can be globally configured with a provider.
+
+```js
+angular.module("myModule", [
+ "oui.password"
+]).config(ouiPasswordConfigurationProvider => {
+ ouiPasswordConfigurationProvider.setTranslations({
+ allRulesValidLabel: "All password rules are met.",
+ ariaHidePasswordLabel: "Hide password",
+ ariaShowPasswordLabel: "Show password",
+ ariaValidRuleLabel: "Valid rule.",
+ ariaInvalidRuleLabel: "Invalid rule.",
+ riskyPasswordLabel: "Risky password.",
+ badPasswordLabel: "Bad password.",
+ weakPasswordLabel: "Weak password.",
+ goodPasswordLabel: "Good password.",
+ strongPasswordLabel: "Strong password."
+ });
+});
+```
diff --git a/packages/oui-password/package.json b/packages/oui-password/package.json
new file mode 100644
index 00000000..e0f266b4
--- /dev/null
+++ b/packages/oui-password/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "@ovh-ui/oui-password",
+ "version": "1.0.0",
+ "main": "./src/index.js",
+ "license": "BSD-3-Clause",
+ "author": "OVH SAS"
+}
diff --git a/packages/oui-password/src/index.js b/packages/oui-password/src/index.js
new file mode 100644
index 00000000..967eaed3
--- /dev/null
+++ b/packages/oui-password/src/index.js
@@ -0,0 +1,12 @@
+import Password from "./password.component";
+import PasswordConfigurationProvider from "./password.provider";
+import PasswordRule from "./rule/password-rule.component";
+import PasswordStrength from "./strength/password-strength.component";
+
+export default angular
+ .module("oui.password", [])
+ .component("ouiPassword", Password)
+ .component("ouiPasswordRule", PasswordRule)
+ .component("ouiPasswordStrength", PasswordStrength)
+ .provider("ouiPasswordConfiguration", PasswordConfigurationProvider)
+ .name;
diff --git a/packages/oui-password/src/index.spec.js b/packages/oui-password/src/index.spec.js
new file mode 100644
index 00000000..abab4053
--- /dev/null
+++ b/packages/oui-password/src/index.spec.js
@@ -0,0 +1,250 @@
+describe("ouiPassword", () => {
+ let $timeout;
+ let TestUtils;
+
+ const getInput = (element) => angular.element(element[0].querySelector(".oui-password__input"));
+ const getStrengthMeter = (element) => angular.element(element[0].querySelector(".oui-progress"));
+ const getVisibilityButton = (element) => angular.element(element[0].querySelector(".oui-password__visibility"));
+
+ beforeEach(angular.mock.module("oui.password"));
+ beforeEach(angular.mock.module("oui.password.configuration"));
+ beforeEach(angular.mock.module("oui.test-utils"));
+
+ beforeEach(inject((_$timeout_, _TestUtils_) => {
+ $timeout = _$timeout_;
+ TestUtils = _TestUtils_;
+ }));
+
+ describe("Provider", () => {
+ let configuration;
+ const foo = { foo: "bar" };
+
+ angular.module("oui.password.configuration", [
+ "oui.password"
+ ]).config(ouiPasswordConfigurationProvider => {
+ ouiPasswordConfigurationProvider.setTranslations(foo);
+ });
+
+ beforeEach(inject(_ouiPasswordConfiguration_ => {
+ configuration = _ouiPasswordConfiguration_;
+ }));
+
+ it("should have custom translations", () => {
+ expect(configuration.translations.foo).toEqual("bar");
+ });
+ });
+
+ describe("Component", () => {
+ describe("Basic", () => {
+ let element;
+ let input;
+ let controller;
+
+ beforeEach(() => {
+ element = TestUtils.compileTemplate('
');
+
+ $timeout.flush();
+
+ controller = element.controller("ouiPassword");
+ input = getInput(element);
+ });
+
+ it("should have a default classname, id and name", () => {
+ expect(element.hasClass("oui-password")).toBeTruthy();
+ });
+
+ it("should move id and name on input", () => {
+ expect(element.attr("id")).toBeUndefined();
+ expect(element.attr("name")).toBeUndefined();
+
+ expect(input.attr("id")).toBe("foo");
+ expect(input.attr("name")).toBe("bar");
+ });
+
+ it("should switch between input password and text", () => {
+ const button = getVisibilityButton(element);
+
+ expect(input.attr("type")).toBe("password");
+ button.triggerHandler("click");
+ expect(input.attr("type")).toBe("text");
+ button.triggerHandler("click");
+ expect(input.attr("type")).toBe("password");
+ });
+
+ it("should have disabled input", () => {
+ expect(input.attr("disabled")).toBeDefined();
+
+ controller.disabled = false;
+ element.scope().$digest();
+ expect(input.attr("disabled")).toBeUndefined();
+
+ controller.disabled = true;
+ element.scope().$digest();
+ expect(input.attr("disabled")).toBeDefined();
+ });
+ });
+
+ describe("Validation", () => {
+ let form;
+ let element;
+ let controller;
+ let input;
+
+ beforeEach(() => {
+ form = TestUtils.compileTemplate(`
+
`);
+
+ $timeout.flush();
+
+ element = form.find("oui-password");
+ controller = element.controller("ouiPassword");
+ input = getInput(element);
+ });
+
+ it("should get an error 'minlength'", () => {
+ input.val("foo");
+ input.triggerHandler("input");
+
+ expect(controller.form.$error).toBeTruthy();
+ expect(controller.form.$error.minlength).toBeTruthy();
+ });
+
+ it("should get an error 'maxlength'", () => {
+ input.val("valueoversixteencharacters");
+ input.triggerHandler("input");
+
+ expect(controller.form.$error).toBeTruthy();
+ expect(controller.form.$error.maxlength).toBeTruthy();
+ });
+
+ it("should return error 'pattern'", () => {
+ input.val("!&()$");
+ input.triggerHandler("input");
+
+ expect(controller.form.$error).toBeTruthy();
+ expect(controller.form.$error.pattern).toBeTruthy();
+ });
+
+ it("should return error 'required'", () => {
+ form.triggerHandler("submit");
+
+ expect(controller.form.$error).toBeTruthy();
+ expect(controller.form.$error.required).toBeTruthy();
+ });
+ });
+
+ describe("Strength", () => {
+ const compileStrength = (score) => TestUtils.compileTemplate(`
+
+
+ `, {
+ score
+ });
+
+ it("should have a default classname", () => {
+ const element = compileStrength();
+ $timeout.flush();
+
+ const strength = element.find("oui-password-strength");
+ expect(strength.hasClass("oui-password-strength")).toBeTruthy();
+
+ const meter = getStrengthMeter(element);
+ expect(meter.hasClass("oui-progress_error")).toBeTruthy();
+ });
+
+ it("should have bad score", () => {
+ const element = compileStrength(1);
+ const meter = getStrengthMeter(element);
+ expect(meter.hasClass("oui-progress_error")).toBeTruthy();
+ });
+
+ it("should have weak score", () => {
+ const element = compileStrength(2);
+ const meter = getStrengthMeter(element);
+ expect(meter.hasClass("oui-progress_warning")).toBeTruthy();
+ });
+
+ it("should have good score", () => {
+ const element = compileStrength(3);
+ const meter = getStrengthMeter(element);
+ expect(meter.hasClass("oui-progress_success")).toBeTruthy();
+ });
+
+ it("should have strong score", () => {
+ const element = compileStrength(4);
+ const meter = getStrengthMeter(element);
+ expect(meter.hasClass("oui-progress_success")).toBeTruthy();
+ });
+ });
+
+ describe("Rule", () => {
+ let form;
+ let element;
+ let controller;
+ let input;
+
+ beforeEach(() => {
+ form = TestUtils.compileTemplate(`
+
`, {
+ checkPasswordLength: (password) => {
+ const minLength = 8;
+ const maxLength = 30;
+ return angular.isString(password) && password.length >= minLength && password.length <= maxLength;
+ }
+ });
+
+ $timeout.flush();
+
+ element = form.find("oui-password");
+ controller = element.controller("ouiPassword");
+ input = getInput(element);
+ });
+
+ it("should have a default classname", () => {
+ expect(element.find("oui-password-rule").hasClass("oui-password-rule")).toBeTruthy();
+ });
+
+ it("should return error 'password' for invalid rules", () => {
+ const invalidRules = 3;
+ input.val("foo");
+ input.triggerHandler("input");
+
+ expect(controller.valid).toBeFalsy();
+ expect(Object.keys(controller.errors).length).toBe(invalidRules);
+ expect(controller.form.$error).toBeTruthy();
+ expect(controller.form.$error.password).toBeTruthy();
+ expect(controller.form.$invalid).toBeTruthy();
+ });
+
+ it("should return error 'password' for invalid rules", () => {
+ const invalidRules = 0;
+ input.val("F0azeruiop");
+ input.triggerHandler("input");
+
+ expect(controller.valid).toBeTruthy();
+ expect(Object.keys(controller.errors).length).toBe(invalidRules);
+ expect(controller.form.$valid).toBeTruthy();
+ });
+ });
+ });
+});
diff --git a/packages/oui-password/src/password.component.js b/packages/oui-password/src/password.component.js
new file mode 100644
index 00000000..3575abbd
--- /dev/null
+++ b/packages/oui-password/src/password.component.js
@@ -0,0 +1,26 @@
+import controller from "./password.controller";
+import template from "./password.html";
+
+export default {
+ require: {
+ form: "?^^form"
+ },
+ bindings: {
+ model: "=",
+ id: "@?",
+ name: "@?",
+ placeholder: "@?",
+ disabled: "",
+ maxlength: "",
+ minlength: "",
+ pattern: "@?",
+ required: "",
+ onChange: "&"
+ },
+ controller,
+ template,
+ transclude: {
+ ruleSlot: "?ouiPasswordRule",
+ strengthSlot: "?ouiPasswordStrength"
+ }
+};
diff --git a/packages/oui-password/src/password.controller.js b/packages/oui-password/src/password.controller.js
new file mode 100644
index 00000000..c6bc5c44
--- /dev/null
+++ b/packages/oui-password/src/password.controller.js
@@ -0,0 +1,48 @@
+import { addBooleanParameter, addDefaultParameter } from "@ovh-ui/common/component-utils";
+
+export default class {
+ constructor ($attrs, $element, $scope, $timeout, ouiPasswordConfiguration) {
+ "ngInject";
+
+ this.$attrs = $attrs;
+ this.$element = $element;
+ this.$id = $scope.$id;
+ this.$timeout = $timeout;
+ this.translations = ouiPasswordConfiguration.translations;
+ }
+
+ toggleVisibility () {
+ this.isVisible = !this.isVisible;
+ }
+
+ updateValidity (key, isValid) {
+ if (isValid) {
+ delete this.errors[key];
+ } else {
+ this.errors[key] = true;
+ }
+
+ this.valid = !Object.keys(this.errors).length;
+ this.form[this.name].$setValidity("password", this.valid);
+ }
+
+ $onInit () {
+ addBooleanParameter(this, "disabled");
+ addBooleanParameter(this, "required");
+
+ addDefaultParameter(this, "id", `ouiPassword${this.$id}`);
+ addDefaultParameter(this, "name", `ouiPassword${this.$id}`);
+
+ this.errors = {};
+ this.isVisible = false;
+ }
+
+ $postLink () {
+ this.$timeout(() =>
+ this.$element
+ .removeAttr("id")
+ .removeAttr("name")
+ .addClass("oui-password")
+ );
+ }
+}
diff --git a/packages/oui-password/src/password.html b/packages/oui-password/src/password.html
new file mode 100644
index 00000000..baaffdae
--- /dev/null
+++ b/packages/oui-password/src/password.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/oui-password/src/password.provider.js b/packages/oui-password/src/password.provider.js
new file mode 100644
index 00000000..0ec2ec0f
--- /dev/null
+++ b/packages/oui-password/src/password.provider.js
@@ -0,0 +1,33 @@
+import merge from "lodash/merge";
+
+export default class {
+ constructor () {
+ this.translations = {
+ allRulesValidLabel: "All password rules are met.",
+ ariaHidePasswordLabel: "Hide password",
+ ariaShowPasswordLabel: "Show password",
+ ariaValidRuleLabel: "Valid rule.",
+ ariaInvalidRuleLabel: "Invalid rule.",
+ riskyPasswordLabel: "Risky password.",
+ badPasswordLabel: "Bad password.",
+ weakPasswordLabel: "Weak password.",
+ goodPasswordLabel: "Good password.",
+ strongPasswordLabel: "Strong password."
+ };
+ }
+
+ /**
+ * Set the translations
+ * @param {Object} translations a map of translations
+ */
+ setTranslations (translations) {
+ this.translations = merge(this.translations, translations);
+ return this;
+ }
+
+ $get () {
+ return {
+ translations: this.translations
+ };
+ }
+}
diff --git a/packages/oui-password/src/rule/password-rule.component.js b/packages/oui-password/src/rule/password-rule.component.js
new file mode 100644
index 00000000..0ae06be2
--- /dev/null
+++ b/packages/oui-password/src/rule/password-rule.component.js
@@ -0,0 +1,16 @@
+import controller from "./password-rule.controller";
+import template from "./password-rule.html";
+
+export default {
+ require: {
+ password: "^ouiPassword"
+ },
+ bindings: {
+ caption: "@?",
+ pattern: "@?",
+ validator: "&"
+ },
+ controller,
+ template,
+ transclude: true
+};
diff --git a/packages/oui-password/src/rule/password-rule.controller.js b/packages/oui-password/src/rule/password-rule.controller.js
new file mode 100644
index 00000000..21cd42c5
--- /dev/null
+++ b/packages/oui-password/src/rule/password-rule.controller.js
@@ -0,0 +1,38 @@
+export default class {
+ constructor ($attrs, $element, $scope, $timeout, ouiPasswordConfiguration) {
+ "ngInject";
+
+ this.$attrs = $attrs;
+ this.$element = $element;
+ this.$timeout = $timeout;
+ this.$scope = $scope;
+ this.translations = ouiPasswordConfiguration.translations;
+ }
+
+ setValidity (value) {
+ if (this.pattern) {
+ const regexp = new RegExp(this.pattern);
+ this.valid = regexp.test(value);
+ } else if (this.validator) {
+ this.valid = this.validator({ modelValue: value });
+ }
+
+ this.password.updateValidity(this.name, this.valid);
+ }
+
+ $onInit () {
+ this.name = `ouiPasswordRule${this.$scope.$id}`;
+ }
+
+ $postLink () {
+ this.$timeout(() =>
+ this.$element
+ .addClass("oui-password-rule")
+ );
+
+ this.$scope.$watch(
+ () => this.password.model,
+ (value) => this.setValidity(value)
+ );
+ }
+}
diff --git a/packages/oui-password/src/rule/password-rule.html b/packages/oui-password/src/rule/password-rule.html
new file mode 100644
index 00000000..b05289b8
--- /dev/null
+++ b/packages/oui-password/src/rule/password-rule.html
@@ -0,0 +1,12 @@
+
+
+
diff --git a/packages/oui-password/src/strength/password-strength.component.js b/packages/oui-password/src/strength/password-strength.component.js
new file mode 100644
index 00000000..7eecc820
--- /dev/null
+++ b/packages/oui-password/src/strength/password-strength.component.js
@@ -0,0 +1,15 @@
+import controller from "./password-strength.controller";
+import template from "./password-strength.html";
+
+export default {
+ require: {
+ password: "^ouiPassword"
+ },
+ bindings: {
+ label: "@?",
+ score: ""
+ },
+ controller,
+ template,
+ transclude: true
+};
diff --git a/packages/oui-password/src/strength/password-strength.controller.js b/packages/oui-password/src/strength/password-strength.controller.js
new file mode 100644
index 00000000..f6c1a8ea
--- /dev/null
+++ b/packages/oui-password/src/strength/password-strength.controller.js
@@ -0,0 +1,68 @@
+export default class {
+ constructor ($attrs, $element, $scope, $timeout, ouiPasswordConfiguration) {
+ "ngInject";
+
+ this.$attrs = $attrs;
+ this.$element = $element;
+ this.$scope = $scope;
+ this.$timeout = $timeout;
+ this.translations = ouiPasswordConfiguration.translations;
+ }
+
+ updateStrength (score) {
+ // By default, use zxcvbn score scale (from 0 to 4)
+ // Convert input score to this scale for the result
+ let currentScore = !angular.isNumber(score) ? 0 : Math.round(score);
+ currentScore = Math.max(currentScore, this.minScore);
+ currentScore = Math.min(currentScore, this.maxScore);
+
+ switch (currentScore) {
+ case this.scale.strong:
+ this.classname = "oui-progress_success";
+ this.feedback = this.translations.strongPasswordLabel;
+ break;
+ case this.scale.good:
+ this.classname = "oui-progress_success";
+ this.feedback = this.translations.goodPasswordLabel;
+ break;
+ case this.scale.weak:
+ this.classname = "oui-progress_warning";
+ this.feedback = this.translations.weakPasswordLabel;
+ break;
+ case this.scale.bad:
+ this.classname = "oui-progress_error";
+ this.feedback = this.translations.badPasswordLabel;
+ break;
+ default:
+ this.classname = "oui-progress_error";
+ this.feedback = this.translations.riskyPasswordLabel;
+ break;
+ }
+ }
+
+ $onInit () {
+ this.scale = {
+ strong: 4,
+ good: 3,
+ weak: 2,
+ bad: 1
+ };
+ this.minScore = 0;
+ this.maxScore = Object.keys(this.scale).length;
+
+ // Avoid idle animation on Windows when undefined
+ this.score = this.score || this.minScore;
+ }
+
+ $postLink () {
+ this.$timeout(() =>
+ this.$element
+ .addClass("oui-password-strength")
+ );
+
+ this.$scope.$watch(
+ () => this.score,
+ (score) => this.updateStrength(score)
+ );
+ }
+}
diff --git a/packages/oui-password/src/strength/password-strength.html b/packages/oui-password/src/strength/password-strength.html
new file mode 100644
index 00000000..bd489a5f
--- /dev/null
+++ b/packages/oui-password/src/strength/password-strength.html
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/packages/oui-password/tests/index.js b/packages/oui-password/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-password/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-popover/tests/index.js b/packages/oui-popover/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-popover/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-progress/tests/index.js b/packages/oui-progress/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-progress/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-radio/tests/index.js b/packages/oui-radio/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-radio/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-search/README.md b/packages/oui-search/README.md
index bffe1916..5ff3ab4f 100644
--- a/packages/oui-search/README.md
+++ b/packages/oui-search/README.md
@@ -82,6 +82,9 @@ See [Autocomplete](#!/oui-angular/autocomplete) directive for more informations.
| `name` | string | @? | yes | n/a | n/a | name attribute of the button
| `placeholder` | string | @? | yes | n/a | n/a | placeholder text
| `aria-label` | string | @? | yes | n/a | n/a | accessibility label
+| `debounce` | number | | no | n/a | n/a | debounce of the model value
+| `minlength` | number | | no | n/a | n/a | min length of the model value
+| `maxlength` | number | | no | n/a | n/a | max length of the model value
| `disabled` | boolean | | no | `true`, `false` | `false` | disabled flag
| `on-change` | function | & | no | n/a | n/a | handler triggered when model has changed
| `on-reset` | function | & | no | n/a | n/a | handler triggered when form is reseted
diff --git a/packages/oui-search/src/index.spec.js b/packages/oui-search/src/index.spec.js
index 1d54b4ff..26a6a358 100644
--- a/packages/oui-search/src/index.spec.js
+++ b/packages/oui-search/src/index.spec.js
@@ -2,11 +2,7 @@ describe("ouiSearch", () => {
let $timeout;
let testUtils;
- const goodSearchText = "aa";
- const tooShortSearchText = "a";
-
beforeEach(angular.mock.module("oui.search"));
- beforeEach(angular.mock.module("oui.criteria-container"));
beforeEach(angular.mock.module("oui.test-utils"));
beforeEach(inject((_$timeout_, _TestUtils_) => {
@@ -63,6 +59,24 @@ describe("ouiSearch", () => {
expect(buttons.attr("disabled")).toBe("disabled");
});
+ it("should reset value on escape keypress", () => {
+ const escKeyCode = 27;
+ const fooKeyCode = 25;
+ const component = testUtils.compileTemplate('
', {
+ model: "foo"
+ });
+ const controller = component.controller("ouiSearch");
+ controller.onSearchReset = jasmine.createSpy("onSearchReset");
+
+ const $input = component.find("input");
+
+ $input.triggerHandler({ type: "keydown", which: fooKeyCode });
+ expect(controller.onSearchReset).not.toHaveBeenCalled();
+
+ $input.triggerHandler({ type: "keydown", which: escKeyCode });
+ expect(controller.onSearchReset).toHaveBeenCalled();
+ });
+
it("should call function of onChange attribute, when input value has changed, with the model value", () => {
const onChangeSpy = jasmine.createSpy("onChangeSpy");
const component = testUtils.compileTemplate('
', {
@@ -98,220 +112,4 @@ describe("ouiSearch", () => {
expect(onSubmitSpy).toHaveBeenCalledWith("foo");
});
});
-
- describe("With criteria container", () => {
- // Unfortunately, lodash prevent setTimeout to be mocked
- const debounceDelay = 800;
-
- describe("on submit", () => {
- it("should add criterion in criteria container", done => {
- const searchText = goodSearchText;
- const onChangeSpy = jasmine.createSpy();
- const element = testUtils.compileTemplate(`
-
-
-
- `, {
- searchText,
- onChangeSpy
- });
-
- element.find("form").triggerHandler("submit");
-
- setTimeout(() => {
- expect(onChangeSpy).toHaveBeenCalledWith([{
- title: searchText,
- property: null,
- operator: "contains",
- value: searchText
- }]);
- done();
- }, debounceDelay);
- });
-
- it("should not add criterion in criteria container if the text is too short", done => {
- const searchText = tooShortSearchText;
- const onChangeSpy = jasmine.createSpy();
- const element = testUtils.compileTemplate(`
-
-
-
- `, {
- searchText,
- onChangeSpy
- });
-
- element.find("form").triggerHandler("submit");
-
- setTimeout(() => {
- expect(onChangeSpy).not.toHaveBeenCalled();
- done();
- }, debounceDelay);
- });
- });
-
- describe("on change", () => {
- it("should add criterion in criteria container", done => {
- const onChangeSpy = jasmine.createSpy();
- const element = testUtils.compileTemplate(`
-
-
-
- `, {
- onChangeSpy
- });
-
- const input = element.find("input");
- input.val(goodSearchText);
- input.triggerHandler("input");
-
- setTimeout(() => {
- expect(onChangeSpy).toHaveBeenCalledWith([{
- title: goodSearchText,
- property: null,
- operator: "contains",
- value: goodSearchText,
- preview: true
- }]);
- done();
- }, debounceDelay);
- });
-
- it("should not add criterion in criteria container if text is too short", done => {
- const onChangeSpy = jasmine.createSpy();
- const element = testUtils.compileTemplate(`
-
-
-
- `, {
- onChangeSpy
- });
- const delay = 850;
-
- setTimeout(() => {
- const input = element.find("input");
- input.val(tooShortSearchText);
- input.triggerHandler("input");
- expect(onChangeSpy).not.toHaveBeenCalled();
- done();
- }, delay);
- });
-
- it("should delete preview criterion if search becomes too short", done => {
- const onChangeSpy = jasmine.createSpy();
- const element = testUtils.compileTemplate(`
-
-
-
- `, {
- onChangeSpy
- });
-
- const input = element.find("input");
-
- // Add preview criterion.
- input.val(goodSearchText);
- input.triggerHandler("input");
-
- setTimeout(() => {
- expect(onChangeSpy).toHaveBeenCalledWith([{
- title: goodSearchText,
- property: null,
- operator: "contains",
- value: goodSearchText,
- preview: true
- }]);
-
- input.val(tooShortSearchText);
- input.triggerHandler("input");
-
- setTimeout(() => {
- expect(onChangeSpy).toHaveBeenCalledWith([]);
- done();
- }, debounceDelay);
- }, debounceDelay);
- });
- });
-
- describe("on reset", () => {
- it("should delete preview criterion", done => {
- const onChangeSpy = jasmine.createSpy();
- const element = testUtils.compileTemplate(`
-
-
-
- `, {
- onChangeSpy
- });
-
- const input = element.find("input");
- const resetButton = element.find("button").eq(0);
-
- // Add preview criterion.
- input.val(goodSearchText);
- input.triggerHandler("input");
-
- setTimeout(() => {
- expect(onChangeSpy).toHaveBeenCalledWith([{
- title: goodSearchText,
- property: null,
- operator: "contains",
- value: goodSearchText,
- preview: true
- }]);
-
- resetButton.triggerHandler("click");
-
- setTimeout(() => {
- expect(onChangeSpy).toHaveBeenCalledWith([]);
- done();
- }, debounceDelay);
- }, debounceDelay);
- });
- });
-
- describe("on escape", () => {
- const escKeyCode = 27;
-
- it("should reset component on escape", () => {
- const element = testUtils.compileTemplate(`
-
-
-
- `);
-
- const controller = element.find("oui-search").controller("ouiSearch");
- const $input = element.find("input");
-
- controller.onSearchReset = jasmine.createSpy("onSearchReset");
-
- $input.triggerHandler({
- type: "keydown",
- keyCode: escKeyCode
- });
-
- expect(controller.onSearchReset).toHaveBeenCalled();
- });
-
- it("should not reset component if pressed is not escape", () => {
- const element = testUtils.compileTemplate(`
-
-
-
- `);
-
- const controller = element.find("oui-search").controller("ouiSearch");
- const $input = element.find("input");
-
- controller.onSearchReset = jasmine.createSpy("onSearchReset");
-
- $input.triggerHandler({
- type: "keydown",
- keyCode: 42
- });
-
- expect(controller.onSearchReset).not.toHaveBeenCalled();
- });
- });
- });
});
diff --git a/packages/oui-search/src/search.component.js b/packages/oui-search/src/search.component.js
index c1fe7f3e..9e656367 100644
--- a/packages/oui-search/src/search.component.js
+++ b/packages/oui-search/src/search.component.js
@@ -2,15 +2,16 @@ import controller from "./search.controller";
import template from "./search.html";
export default {
- require: {
- criteriaContainer: "?^^ouiCriteriaContainer"
- },
bindings: {
model: "=",
id: "@?",
name: "@?",
placeholder: "@?",
ariaLabel: "@?",
+
+ debounce: "",
+ maxlength: "",
+ minlength: "",
disabled: "",
onChange: "&",
diff --git a/packages/oui-search/src/search.controller.js b/packages/oui-search/src/search.controller.js
index 12b2b72a..2ad8bfb7 100644
--- a/packages/oui-search/src/search.controller.js
+++ b/packages/oui-search/src/search.controller.js
@@ -1,88 +1,34 @@
import { addBooleanParameter } from "@ovh-ui/common/component-utils";
-import debounce from "lodash/debounce";
const componentClass = "oui-search";
-// Min length needed to create a new criterion.
-const minLengthTrigger = 2;
-
-// Minimum delay before each criterion change.
-const criterionDebounceDelay = 800;
-
const escKeyCode = 27;
-export default class SearchController {
+export default class {
constructor ($attrs, $element, $timeout) {
"ngInject";
this.$attrs = $attrs;
this.$element = $element;
this.$timeout = $timeout;
-
- this.onCriterionChange = debounce(this.onCriterionChange.bind(this), criterionDebounceDelay);
- this.onCriterionSubmit = debounce(this.onCriterionSubmit.bind(this), criterionDebounceDelay);
- this.onCriterionReset = debounce(this.onCriterionReset.bind(this), criterionDebounceDelay);
- }
-
- $onInit () {
- // Support presence of attribute 'disabled'
- addBooleanParameter(this, "disabled");
- }
-
- $postLink () {
- // Sometimes the digest cycle is done before dom manipulation,
- // So we use $timeout to force the $apply
- this.$timeout(() =>
- this.$element
- .removeAttr("aria-label")
- .removeAttr("id")
- .removeAttr("name")
- .addClass(componentClass)
- );
}
onKeyDown (event) {
- if (event.keyCode === escKeyCode) {
+ if (event.which === escKeyCode) {
this.onSearchReset();
}
}
- $destroy () {
- this.$input.off("keypress");
- }
-
onSearchChange () {
const modelValue = this.model;
this.onChange({ modelValue });
-
- this.onCriterionChange();
- }
-
- onCriterionChange () {
- const modelValue = this.model;
-
- if (this.criteriaContainer) {
- if (modelValue && modelValue.length >= minLengthTrigger) {
- this.criteriaContainer.setPreviewCriterion(SearchController.getCriterion(modelValue), true);
- } else {
- this.criteriaContainer.deletePreviewCriterion();
- }
- }
}
onSearchSubmit (modelValue) {
this.model = undefined;
this.onSubmit({ modelValue });
-
- this.onCriterionSubmit(modelValue);
- }
-
- onCriterionSubmit (modelValue) {
- if (this.criteriaContainer && modelValue && modelValue.length >= minLengthTrigger) {
- this.criteriaContainer.add(SearchController.getCriterion(modelValue));
- }
}
onSearchReset () {
@@ -90,22 +36,22 @@ export default class SearchController {
this.model = undefined;
this.onReset();
-
- this.onCriterionReset();
}
- onCriterionReset () {
- if (this.criteriaContainer) {
- this.criteriaContainer.deletePreviewCriterion();
- }
+ $onInit () {
+ // Support presence of attribute 'disabled'
+ addBooleanParameter(this, "disabled");
}
- static getCriterion (modelValue) {
- return {
- title: modelValue,
- property: null, // any property
- operator: "contains",
- value: modelValue
- };
+ $postLink () {
+ // Sometimes the digest cycle is done before dom manipulation,
+ // So we use $timeout to force the $apply
+ this.$timeout(() =>
+ this.$element
+ .removeAttr("aria-label")
+ .removeAttr("id")
+ .removeAttr("name")
+ .addClass(componentClass)
+ );
}
}
diff --git a/packages/oui-search/src/search.html b/packages/oui-search/src/search.html
index 3f522cab..95f8e021 100644
--- a/packages/oui-search/src/search.html
+++ b/packages/oui-search/src/search.html
@@ -10,6 +10,9 @@
ng-attr-name="{{::$ctrl.name}}"
ng-attr-placeholder="{{::$ctrl.placeholder}}"
ng-model="$ctrl.model"
+ ng-model-options="{ debounce: $ctrl.debounce }"
+ ng-maxlength="$ctrl.maxlength"
+ ng-minlength="$ctrl.minlength"
ng-change="$ctrl.onSearchChange()"
ng-disabled="$ctrl.disabled"
ng-keydown="$ctrl.onKeyDown($event)" />
diff --git a/packages/oui-search/tests/index.js b/packages/oui-search/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-search/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-select-picker/tests/index.js b/packages/oui-select-picker/tests/index.js
new file mode 100644
index 00000000..ebd31bdb
--- /dev/null
+++ b/packages/oui-select-picker/tests/index.js
@@ -0,0 +1,7 @@
+import "@ovh-ui/common/test-utils";
+
+loadTests(require.context("../src/", true, /.*((\.spec)|(index))$/));
+
+function loadTests (context) {
+ context.keys().forEach(context);
+}
diff --git a/packages/oui-select/README.md b/packages/oui-select/README.md
index 766721fa..1a835996 100644
--- a/packages/oui-select/README.md
+++ b/packages/oui-select/README.md
@@ -19,7 +19,7 @@
+ match="country.name">
```
@@ -40,7 +40,20 @@
model="$ctrl.modelSearchable"
placeholder="Select a country..."
items="$ctrl.countries"
- match="name"
+ match="country.name"
+ searchable>
+
+```
+
+### Multiple
+
+```html:preview
+
```
@@ -52,9 +65,7 @@
model="$ctrl.modelDisabled"
placeholder="Select a country..."
items="$ctrl.countries"
- required
- match="name"
- data-align="start"
+ match="country.name"
disabled>
```
@@ -72,7 +83,7 @@
placeholder="Select a country..."
items="$ctrl.countries"
disable-items="$ctrl.disableItems($item)"
- match="name">
+ match="country.name">
```
@@ -84,7 +95,7 @@
placeholder="Select a country..."
items="$ctrl.countries"
group-by="$ctrl.groupByFirstLetter"
- match="name">
+ match="country.name">
```
@@ -101,10 +112,10 @@
placeholder="Select a country..."
items="$ctrl.countries"
group-by="$ctrl.groupByFirstLetter"
- match="name">
-
+ match="country.name">
+
- Code:
+ Code:
```
@@ -117,47 +128,46 @@
```html:preview
+
+
Last onChange value: {{ $ctrl.onChangeModelValue | json}}
+
onBlur counter: {{ $ctrl.onBlurCounter }}
+
onFocus counter: {{ $ctrl.onFocusCounter }}
+
-
+
Code:
-
-
Last onChange value: {{ $ctrl.onChangeModelValue | json}}
-
onBlur counter: {{ $ctrl.onBlurCounter }}
-
onFocus counter: {{ $ctrl.onFocusCounter }}
-
-
```
## API
-| Attribute | Type | Binding | One-time binding | Values | Default | Description
-| ---- | ---- | ---- | ---- | ---- | ---- | ----
-| `model` | object | = | no | n/a | n/a | model bound to component
-| `name` | string | @? | yes | n/a | n/a | name of the form component
-| `title` | string | @? | yes | n/a | n/a | title attribute of the component
-| `placeholder` | string | @? | yes | n/a | n/a | placeholder displayed when model is undefined
-| `match` | string | @? | no | n/a | n/a | property of item to show as selected item
-| `items` | array | < | no | n/a | n/a | array used to populate the list
-| `disable-items`| function | & | no | n/a | n/a | predicate to determine items to disable
-| `required` | boolean | | no | `true`, `false` | `false` | define if the field is required
-| `disabled` | boolean | | no | `true`, `false` | `false` | define if the field is disabled
-| `group-by` | function | | no | n/a | n/a | function taking an item as parameter and returning the group name as as string
-| `on-blur` | function | & | no | n/a | n/a | called focus is lost
-| `on-focus` | function | & | no | n/a | n/a | called on focus
-| `on-change` | function | & | no | n/a | n/a | handler triggered when value has changed
+| Attribute | Type | Binding | One-time binding | Values | Default | Description
+| ---- | ---- | ---- | ---- | ---- | ---- | ----
+| `model` | object | = | no | n/a | n/a | model bound to component
+| `name` | string | @? | yes | n/a | n/a | name of the form component
+| `title` | string | @? | yes | n/a | n/a | title of the form component
+| `placeholder` | string | @? | yes | n/a | n/a | placeholder displayed when model is undefined
+| `match` | string | @? | no | n/a | n/a | property of item to show as selected item
+| `items` | array | < | no | n/a | n/a | array used to populate the list
+| `disable-items` | function | & | no | n/a | n/a | predicate to determine items to disable
+| `required` | boolean | | no | `true`, `false` | `false` | define if the field is required
+| `disabled` | boolean | | no | `true`, `false` | `false` | define if the field is disabled
+| `multiple` | boolean | | yes | `true`, `false` | `false` | allow multiple selection
+| `group-by` | function | | no | n/a | n/a | function taking an item as parameter and returning the group name as as string
+| `on-blur` | function | & | no | n/a | n/a | called focus is lost
+| `on-focus` | function | & | no | n/a | n/a | called on focus
+| `on-change` | function | & | no | n/a | n/a | handler triggered when value has changed
#### Deprecated
diff --git a/packages/oui-select/src/index.spec.data.json b/packages/oui-select/src/index.spec.data.json
index e8df8918..3cb81ee3 100644
--- a/packages/oui-select/src/index.spec.data.json
+++ b/packages/oui-select/src/index.spec.data.json
@@ -1,245 +1,245 @@
[
-{"name": "Afghanistan", "code": "AF"},
-{"name": "Ã…land Islands", "code": "AX"},
-{"name": "Albania", "code": "AL"},
-{"name": "Algeria", "code": "DZ"},
-{"name": "American Samoa", "code": "AS"},
-{"name": "AndorrA", "code": "AD"},
-{"name": "Angola", "code": "AO"},
-{"name": "Anguilla", "code": "AI"},
-{"name": "Antarctica", "code": "AQ"},
-{"name": "Antigua and Barbuda", "code": "AG"},
-{"name": "Argentina", "code": "AR"},
-{"name": "Armenia", "code": "AM"},
-{"name": "Aruba", "code": "AW"},
-{"name": "Australia", "code": "AU"},
-{"name": "Austria", "code": "AT"},
-{"name": "Azerbaijan", "code": "AZ"},
-{"name": "Bahamas", "code": "BS"},
-{"name": "Bahrain", "code": "BH"},
-{"name": "Bangladesh", "code": "BD"},
-{"name": "Barbados", "code": "BB"},
-{"name": "Belarus", "code": "BY"},
-{"name": "Belgium", "code": "BE"},
-{"name": "Belize", "code": "BZ"},
-{"name": "Benin", "code": "BJ"},
-{"name": "Bermuda", "code": "BM"},
-{"name": "Bhutan", "code": "BT"},
-{"name": "Bolivia", "code": "BO"},
-{"name": "Bosnia and Herzegovina", "code": "BA"},
-{"name": "Botswana", "code": "BW"},
-{"name": "Bouvet Island", "code": "BV"},
-{"name": "Brazil", "code": "BR"},
-{"name": "British Indian Ocean Territory", "code": "IO"},
-{"name": "Brunei Darussalam", "code": "BN"},
-{"name": "Bulgaria", "code": "BG"},
-{"name": "Burkina Faso", "code": "BF"},
-{"name": "Burundi", "code": "BI"},
-{"name": "Cambodia", "code": "KH"},
-{"name": "Cameroon", "code": "CM"},
-{"name": "Canada", "code": "CA"},
-{"name": "Cape Verde", "code": "CV"},
-{"name": "Cayman Islands", "code": "KY"},
-{"name": "Central African Republic", "code": "CF"},
-{"name": "Chad", "code": "TD"},
-{"name": "Chile", "code": "CL"},
-{"name": "China", "code": "CN"},
-{"name": "Christmas Island", "code": "CX"},
-{"name": "Cocos (Keeling) Islands", "code": "CC"},
-{"name": "Colombia", "code": "CO"},
-{"name": "Comoros", "code": "KM"},
-{"name": "Congo", "code": "CG"},
-{"name": "Congo, The Democratic Republic of the", "code": "CD"},
-{"name": "Cook Islands", "code": "CK"},
-{"name": "Costa Rica", "code": "CR"},
-{"name": "Cote D'Ivoire", "code": "CI"},
-{"name": "Croatia", "code": "HR"},
-{"name": "Cuba", "code": "CU"},
-{"name": "Cyprus", "code": "CY"},
-{"name": "Czech Republic", "code": "CZ"},
-{"name": "Denmark", "code": "DK"},
-{"name": "Djibouti", "code": "DJ"},
-{"name": "Dominica", "code": "DM"},
-{"name": "Dominican Republic", "code": "DO"},
-{"name": "Ecuador", "code": "EC"},
-{"name": "Egypt", "code": "EG"},
-{"name": "El Salvador", "code": "SV"},
-{"name": "Equatorial Guinea", "code": "GQ"},
-{"name": "Eritrea", "code": "ER"},
-{"name": "Estonia", "code": "EE"},
-{"name": "Ethiopia", "code": "ET"},
-{"name": "Falkland Islands (Malvinas)", "code": "FK"},
-{"name": "Faroe Islands", "code": "FO"},
-{"name": "Fiji", "code": "FJ"},
-{"name": "Finland", "code": "FI"},
-{"name": "France", "code": "FR"},
-{"name": "French Guiana", "code": "GF"},
-{"name": "French Polynesia", "code": "PF"},
-{"name": "French Southern Territories", "code": "TF"},
-{"name": "Gabon", "code": "GA"},
-{"name": "Gambia", "code": "GM"},
-{"name": "Georgia", "code": "GE"},
-{"name": "Germany", "code": "DE"},
-{"name": "Ghana", "code": "GH"},
-{"name": "Gibraltar", "code": "GI"},
-{"name": "Greece", "code": "GR"},
-{"name": "Greenland", "code": "GL"},
-{"name": "Grenada", "code": "GD"},
-{"name": "Guadeloupe", "code": "GP"},
-{"name": "Guam", "code": "GU"},
-{"name": "Guatemala", "code": "GT"},
-{"name": "Guernsey", "code": "GG"},
-{"name": "Guinea", "code": "GN"},
-{"name": "Guinea-Bissau", "code": "GW"},
-{"name": "Guyana", "code": "GY"},
-{"name": "Haiti", "code": "HT"},
-{"name": "Heard Island and Mcdonald Islands", "code": "HM"},
-{"name": "Holy See (Vatican City State)", "code": "VA"},
-{"name": "Honduras", "code": "HN"},
-{"name": "Hong Kong", "code": "HK"},
-{"name": "Hungary", "code": "HU"},
-{"name": "Iceland", "code": "IS"},
-{"name": "India", "code": "IN"},
-{"name": "Indonesia", "code": "ID"},
-{"name": "Iran, Islamic Republic Of", "code": "IR"},
-{"name": "Iraq", "code": "IQ"},
-{"name": "Ireland", "code": "IE"},
-{"name": "Isle of Man", "code": "IM"},
-{"name": "Israel", "code": "IL"},
-{"name": "Italy", "code": "IT"},
-{"name": "Jamaica", "code": "JM"},
-{"name": "Japan", "code": "JP"},
-{"name": "Jersey", "code": "JE"},
-{"name": "Jordan", "code": "JO"},
-{"name": "Kazakhstan", "code": "KZ"},
-{"name": "Kenya", "code": "KE"},
-{"name": "Kiribati", "code": "KI"},
-{"name": "Korea, Democratic People'S Republic of", "code": "KP"},
-{"name": "Korea, Republic of", "code": "KR"},
-{"name": "Kuwait", "code": "KW"},
-{"name": "Kyrgyzstan", "code": "KG"},
-{"name": "Lao People'S Democratic Republic", "code": "LA"},
-{"name": "Latvia", "code": "LV"},
-{"name": "Lebanon", "code": "LB"},
-{"name": "Lesotho", "code": "LS"},
-{"name": "Liberia", "code": "LR"},
-{"name": "Libyan Arab Jamahiriya", "code": "LY"},
-{"name": "Liechtenstein", "code": "LI"},
-{"name": "Lithuania", "code": "LT"},
-{"name": "Luxembourg", "code": "LU"},
-{"name": "Macao", "code": "MO"},
-{"name": "Macedonia, The Former Yugoslav Republic of", "code": "MK"},
-{"name": "Madagascar", "code": "MG"},
-{"name": "Malawi", "code": "MW"},
-{"name": "Malaysia", "code": "MY"},
-{"name": "Maldives", "code": "MV"},
-{"name": "Mali", "code": "ML"},
-{"name": "Malta", "code": "MT"},
-{"name": "Marshall Islands", "code": "MH"},
-{"name": "Martinique", "code": "MQ"},
-{"name": "Mauritania", "code": "MR"},
-{"name": "Mauritius", "code": "MU"},
-{"name": "Mayotte", "code": "YT"},
-{"name": "Mexico", "code": "MX"},
-{"name": "Micronesia, Federated States of", "code": "FM"},
-{"name": "Moldova, Republic of", "code": "MD"},
-{"name": "Monaco", "code": "MC"},
-{"name": "Mongolia", "code": "MN"},
-{"name": "Montserrat", "code": "MS"},
-{"name": "Morocco", "code": "MA"},
-{"name": "Mozambique", "code": "MZ"},
-{"name": "Myanmar", "code": "MM"},
-{"name": "Namibia", "code": "NA"},
-{"name": "Nauru", "code": "NR"},
-{"name": "Nepal", "code": "NP"},
-{"name": "Netherlands", "code": "NL"},
-{"name": "Netherlands Antilles", "code": "AN"},
-{"name": "New Caledonia", "code": "NC"},
-{"name": "New Zealand", "code": "NZ"},
-{"name": "Nicaragua", "code": "NI"},
-{"name": "Niger", "code": "NE"},
-{"name": "Nigeria", "code": "NG"},
-{"name": "Niue", "code": "NU"},
-{"name": "Norfolk Island", "code": "NF"},
-{"name": "Northern Mariana Islands", "code": "MP"},
-{"name": "Norway", "code": "NO"},
-{"name": "Oman", "code": "OM"},
-{"name": "Pakistan", "code": "PK"},
-{"name": "Palau", "code": "PW"},
-{"name": "Palestinian Territory, Occupied", "code": "PS"},
-{"name": "Panama", "code": "PA"},
-{"name": "Papua New Guinea", "code": "PG"},
-{"name": "Paraguay", "code": "PY"},
-{"name": "Peru", "code": "PE"},
-{"name": "Philippines", "code": "PH"},
-{"name": "Pitcairn", "code": "PN"},
-{"name": "Poland", "code": "PL"},
-{"name": "Portugal", "code": "PT"},
-{"name": "Puerto Rico", "code": "PR"},
-{"name": "Qatar", "code": "QA"},
-{"name": "Reunion", "code": "RE"},
-{"name": "Romania", "code": "RO"},
-{"name": "Russian Federation", "code": "RU"},
-{"name": "RWANDA", "code": "RW"},
-{"name": "Saint Helena", "code": "SH"},
-{"name": "Saint Kitts and Nevis", "code": "KN"},
-{"name": "Saint Lucia", "code": "LC"},
-{"name": "Saint Pierre and Miquelon", "code": "PM"},
-{"name": "Saint Vincent and the Grenadines", "code": "VC"},
-{"name": "Samoa", "code": "WS"},
-{"name": "San Marino", "code": "SM"},
-{"name": "Sao Tome and Principe", "code": "ST"},
-{"name": "Saudi Arabia", "code": "SA"},
-{"name": "Senegal", "code": "SN"},
-{"name": "Serbia and Montenegro", "code": "CS"},
-{"name": "Seychelles", "code": "SC"},
-{"name": "Sierra Leone", "code": "SL"},
-{"name": "Singapore", "code": "SG"},
-{"name": "Slovakia", "code": "SK"},
-{"name": "Slovenia", "code": "SI"},
-{"name": "Solomon Islands", "code": "SB"},
-{"name": "Somalia", "code": "SO"},
-{"name": "South Africa", "code": "ZA"},
-{"name": "South Georgia and the South Sandwich Islands", "code": "GS"},
-{"name": "Spain", "code": "ES"},
-{"name": "Sri Lanka", "code": "LK"},
-{"name": "Sudan", "code": "SD"},
-{"name": "Suriname", "code": "SR"},
-{"name": "Svalbard and Jan Mayen", "code": "SJ"},
-{"name": "Swaziland", "code": "SZ"},
-{"name": "Sweden", "code": "SE"},
-{"name": "Switzerland", "code": "CH"},
-{"name": "Syrian Arab Republic", "code": "SY"},
-{"name": "Taiwan, Province of China", "code": "TW"},
-{"name": "Tajikistan", "code": "TJ"},
-{"name": "Tanzania, United Republic of", "code": "TZ"},
-{"name": "Thailand", "code": "TH"},
-{"name": "Timor-Leste", "code": "TL"},
-{"name": "Togo", "code": "TG"},
-{"name": "Tokelau", "code": "TK"},
-{"name": "Tonga", "code": "TO"},
-{"name": "Trinidad and Tobago", "code": "TT"},
-{"name": "Tunisia", "code": "TN"},
-{"name": "Turkey", "code": "TR"},
-{"name": "Turkmenistan", "code": "TM"},
-{"name": "Turks and Caicos Islands", "code": "TC"},
-{"name": "Tuvalu", "code": "TV"},
-{"name": "Uganda", "code": "UG"},
-{"name": "Ukraine", "code": "UA"},
-{"name": "United Arab Emirates", "code": "AE"},
-{"name": "United Kingdom", "code": "GB"},
-{"name": "United States", "code": "US"},
-{"name": "United States Minor Outlying Islands", "code": "UM"},
-{"name": "Uruguay", "code": "UY"},
-{"name": "Uzbekistan", "code": "UZ"},
-{"name": "Vanuatu", "code": "VU"},
-{"name": "Venezuela", "code": "VE"},
-{"name": "Viet Nam", "code": "VN"},
-{"name": "Virgin Islands, British", "code": "VG"},
-{"name": "Virgin Islands, U.S.", "code": "VI"},
-{"name": "Wallis and Futuna", "code": "WF"},
-{"name": "Western Sahara", "code": "EH"},
-{"name": "Yemen", "code": "YE"},
-{"name": "Zambia", "code": "ZM"},
-{"name": "Zimbabwe", "code": "ZW"}
+ {"country": {"name": "Afghanistan", "code": "AF"} },
+ {"country": {"name": "Ã…land Islands", "code": "AX"} },
+ {"country": {"name": "Albania", "code": "AL"} },
+ {"country": {"name": "Algeria", "code": "DZ"} },
+ {"country": {"name": "American Samoa", "code": "AS"} },
+ {"country": {"name": "AndorrA", "code": "AD"} },
+ {"country": {"name": "Angola", "code": "AO"} },
+ {"country": {"name": "Anguilla", "code": "AI"} },
+ {"country": {"name": "Antarctica", "code": "AQ"} },
+ {"country": {"name": "Antigua and Barbuda", "code": "AG"} },
+ {"country": {"name": "Argentina", "code": "AR"} },
+ {"country": {"name": "Armenia", "code": "AM"} },
+ {"country": {"name": "Aruba", "code": "AW"} },
+ {"country": {"name": "Australia", "code": "AU"} },
+ {"country": {"name": "Austria", "code": "AT"} },
+ {"country": {"name": "Azerbaijan", "code": "AZ"} },
+ {"country": {"name": "Bahamas", "code": "BS"} },
+ {"country": {"name": "Bahrain", "code": "BH"} },
+ {"country": {"name": "Bangladesh", "code": "BD"} },
+ {"country": {"name": "Barbados", "code": "BB"} },
+ {"country": {"name": "Belarus", "code": "BY"} },
+ {"country": {"name": "Belgium", "code": "BE"} },
+ {"country": {"name": "Belize", "code": "BZ"} },
+ {"country": {"name": "Benin", "code": "BJ"} },
+ {"country": {"name": "Bermuda", "code": "BM"} },
+ {"country": {"name": "Bhutan", "code": "BT"} },
+ {"country": {"name": "Bolivia", "code": "BO"} },
+ {"country": {"name": "Bosnia and Herzegovina", "code": "BA"} },
+ {"country": {"name": "Botswana", "code": "BW"} },
+ {"country": {"name": "Bouvet Island", "code": "BV"} },
+ {"country": {"name": "Brazil", "code": "BR"} },
+ {"country": {"name": "British Indian Ocean Territory", "code": "IO"} },
+ {"country": {"name": "Brunei Darussalam", "code": "BN"} },
+ {"country": {"name": "Bulgaria", "code": "BG"} },
+ {"country": {"name": "Burkina Faso", "code": "BF"} },
+ {"country": {"name": "Burundi", "code": "BI"} },
+ {"country": {"name": "Cambodia", "code": "KH"} },
+ {"country": {"name": "Cameroon", "code": "CM"} },
+ {"country": {"name": "Canada", "code": "CA"} },
+ {"country": {"name": "Cape Verde", "code": "CV"} },
+ {"country": {"name": "Cayman Islands", "code": "KY"} },
+ {"country": {"name": "Central African Republic", "code": "CF"} },
+ {"country": {"name": "Chad", "code": "TD"} },
+ {"country": {"name": "Chile", "code": "CL"} },
+ {"country": {"name": "China", "code": "CN"} },
+ {"country": {"name": "Christmas Island", "code": "CX"} },
+ {"country": {"name": "Cocos (Keeling) Islands", "code": "CC"} },
+ {"country": {"name": "Colombia", "code": "CO"} },
+ {"country": {"name": "Comoros", "code": "KM"} },
+ {"country": {"name": "Congo", "code": "CG"} },
+ {"country": {"name": "Congo, The Democratic Republic of the", "code": "CD"} },
+ {"country": {"name": "Cook Islands", "code": "CK"} },
+ {"country": {"name": "Costa Rica", "code": "CR"} },
+ {"country": {"name": "Cote D'Ivoire", "code": "CI"} },
+ {"country": {"name": "Croatia", "code": "HR"} },
+ {"country": {"name": "Cuba", "code": "CU"} },
+ {"country": {"name": "Cyprus", "code": "CY"} },
+ {"country": {"name": "Czech Republic", "code": "CZ"} },
+ {"country": {"name": "Denmark", "code": "DK"} },
+ {"country": {"name": "Djibouti", "code": "DJ"} },
+ {"country": {"name": "Dominica", "code": "DM"} },
+ {"country": {"name": "Dominican Republic", "code": "DO"} },
+ {"country": {"name": "Ecuador", "code": "EC"} },
+ {"country": {"name": "Egypt", "code": "EG"} },
+ {"country": {"name": "El Salvador", "code": "SV"} },
+ {"country": {"name": "Equatorial Guinea", "code": "GQ"} },
+ {"country": {"name": "Eritrea", "code": "ER"} },
+ {"country": {"name": "Estonia", "code": "EE"} },
+ {"country": {"name": "Ethiopia", "code": "ET"} },
+ {"country": {"name": "Falkland Islands (Malvinas)", "code": "FK"} },
+ {"country": {"name": "Faroe Islands", "code": "FO"} },
+ {"country": {"name": "Fiji", "code": "FJ"} },
+ {"country": {"name": "Finland", "code": "FI"} },
+ {"country": {"name": "France", "code": "FR"} },
+ {"country": {"name": "French Guiana", "code": "GF"} },
+ {"country": {"name": "French Polynesia", "code": "PF"} },
+ {"country": {"name": "French Southern Territories", "code": "TF"} },
+ {"country": {"name": "Gabon", "code": "GA"} },
+ {"country": {"name": "Gambia", "code": "GM"} },
+ {"country": {"name": "Georgia", "code": "GE"} },
+ {"country": {"name": "Germany", "code": "DE"} },
+ {"country": {"name": "Ghana", "code": "GH"} },
+ {"country": {"name": "Gibraltar", "code": "GI"} },
+ {"country": {"name": "Greece", "code": "GR"} },
+ {"country": {"name": "Greenland", "code": "GL"} },
+ {"country": {"name": "Grenada", "code": "GD"} },
+ {"country": {"name": "Guadeloupe", "code": "GP"} },
+ {"country": {"name": "Guam", "code": "GU"} },
+ {"country": {"name": "Guatemala", "code": "GT"} },
+ {"country": {"name": "Guernsey", "code": "GG"} },
+ {"country": {"name": "Guinea", "code": "GN"} },
+ {"country": {"name": "Guinea-Bissau", "code": "GW"} },
+ {"country": {"name": "Guyana", "code": "GY"} },
+ {"country": {"name": "Haiti", "code": "HT"} },
+ {"country": {"name": "Heard Island and Mcdonald Islands", "code": "HM"} },
+ {"country": {"name": "Holy See (Vatican City State)", "code": "VA"} },
+ {"country": {"name": "Honduras", "code": "HN"} },
+ {"country": {"name": "Hong Kong", "code": "HK"} },
+ {"country": {"name": "Hungary", "code": "HU"} },
+ {"country": {"name": "Iceland", "code": "IS"} },
+ {"country": {"name": "India", "code": "IN"} },
+ {"country": {"name": "Indonesia", "code": "ID"} },
+ {"country": {"name": "Iran, Islamic Republic Of", "code": "IR"} },
+ {"country": {"name": "Iraq", "code": "IQ"} },
+ {"country": {"name": "Ireland", "code": "IE"} },
+ {"country": {"name": "Isle of Man", "code": "IM"} },
+ {"country": {"name": "Israel", "code": "IL"} },
+ {"country": {"name": "Italy", "code": "IT"} },
+ {"country": {"name": "Jamaica", "code": "JM"} },
+ {"country": {"name": "Japan", "code": "JP"} },
+ {"country": {"name": "Jersey", "code": "JE"} },
+ {"country": {"name": "Jordan", "code": "JO"} },
+ {"country": {"name": "Kazakhstan", "code": "KZ"} },
+ {"country": {"name": "Kenya", "code": "KE"} },
+ {"country": {"name": "Kiribati", "code": "KI"} },
+ {"country": {"name": "Korea, Democratic People'S Republic of", "code": "KP"} },
+ {"country": {"name": "Korea, Republic of", "code": "KR"} },
+ {"country": {"name": "Kuwait", "code": "KW"} },
+ {"country": {"name": "Kyrgyzstan", "code": "KG"} },
+ {"country": {"name": "Lao People'S Democratic Republic", "code": "LA"} },
+ {"country": {"name": "Latvia", "code": "LV"} },
+ {"country": {"name": "Lebanon", "code": "LB"} },
+ {"country": {"name": "Lesotho", "code": "LS"} },
+ {"country": {"name": "Liberia", "code": "LR"} },
+ {"country": {"name": "Libyan Arab Jamahiriya", "code": "LY"} },
+ {"country": {"name": "Liechtenstein", "code": "LI"} },
+ {"country": {"name": "Lithuania", "code": "LT"} },
+ {"country": {"name": "Luxembourg", "code": "LU"} },
+ {"country": {"name": "Macao", "code": "MO"} },
+ {"country": {"name": "Macedonia, The Former Yugoslav Republic of", "code": "MK"} },
+ {"country": {"name": "Madagascar", "code": "MG"} },
+ {"country": {"name": "Malawi", "code": "MW"} },
+ {"country": {"name": "Malaysia", "code": "MY"} },
+ {"country": {"name": "Maldives", "code": "MV"} },
+ {"country": {"name": "Mali", "code": "ML"} },
+ {"country": {"name": "Malta", "code": "MT"} },
+ {"country": {"name": "Marshall Islands", "code": "MH"} },
+ {"country": {"name": "Martinique", "code": "MQ"} },
+ {"country": {"name": "Mauritania", "code": "MR"} },
+ {"country": {"name": "Mauritius", "code": "MU"} },
+ {"country": {"name": "Mayotte", "code": "YT"} },
+ {"country": {"name": "Mexico", "code": "MX"} },
+ {"country": {"name": "Micronesia, Federated States of", "code": "FM"} },
+ {"country": {"name": "Moldova, Republic of", "code": "MD"} },
+ {"country": {"name": "Monaco", "code": "MC"} },
+ {"country": {"name": "Mongolia", "code": "MN"} },
+ {"country": {"name": "Montserrat", "code": "MS"} },
+ {"country": {"name": "Morocco", "code": "MA"} },
+ {"country": {"name": "Mozambique", "code": "MZ"} },
+ {"country": {"name": "Myanmar", "code": "MM"} },
+ {"country": {"name": "Namibia", "code": "NA"} },
+ {"country": {"name": "Nauru", "code": "NR"} },
+ {"country": {"name": "Nepal", "code": "NP"} },
+ {"country": {"name": "Netherlands", "code": "NL"} },
+ {"country": {"name": "Netherlands Antilles", "code": "AN"} },
+ {"country": {"name": "New Caledonia", "code": "NC"} },
+ {"country": {"name": "New Zealand", "code": "NZ"} },
+ {"country": {"name": "Nicaragua", "code": "NI"} },
+ {"country": {"name": "Niger", "code": "NE"} },
+ {"country": {"name": "Nigeria", "code": "NG"} },
+ {"country": {"name": "Niue", "code": "NU"} },
+ {"country": {"name": "Norfolk Island", "code": "NF"} },
+ {"country": {"name": "Northern Mariana Islands", "code": "MP"} },
+ {"country": {"name": "Norway", "code": "NO"} },
+ {"country": {"name": "Oman", "code": "OM"} },
+ {"country": {"name": "Pakistan", "code": "PK"} },
+ {"country": {"name": "Palau", "code": "PW"} },
+ {"country": {"name": "Palestinian Territory, Occupied", "code": "PS"} },
+ {"country": {"name": "Panama", "code": "PA"} },
+ {"country": {"name": "Papua New Guinea", "code": "PG"} },
+ {"country": {"name": "Paraguay", "code": "PY"} },
+ {"country": {"name": "Peru", "code": "PE"} },
+ {"country": {"name": "Philippines", "code": "PH"} },
+ {"country": {"name": "Pitcairn", "code": "PN"} },
+ {"country": {"name": "Poland", "code": "PL"} },
+ {"country": {"name": "Portugal", "code": "PT"} },
+ {"country": {"name": "Puerto Rico", "code": "PR"} },
+ {"country": {"name": "Qatar", "code": "QA"} },
+ {"country": {"name": "Reunion", "code": "RE"} },
+ {"country": {"name": "Romania", "code": "RO"} },
+ {"country": {"name": "Russian Federation", "code": "RU"} },
+ {"country": {"name": "RWANDA", "code": "RW"} },
+ {"country": {"name": "Saint Helena", "code": "SH"} },
+ {"country": {"name": "Saint Kitts and Nevis", "code": "KN"} },
+ {"country": {"name": "Saint Lucia", "code": "LC"} },
+ {"country": {"name": "Saint Pierre and Miquelon", "code": "PM"} },
+ {"country": {"name": "Saint Vincent and the Grenadines", "code": "VC"} },
+ {"country": {"name": "Samoa", "code": "WS"} },
+ {"country": {"name": "San Marino", "code": "SM"} },
+ {"country": {"name": "Sao Tome and Principe", "code": "ST"} },
+ {"country": {"name": "Saudi Arabia", "code": "SA"} },
+ {"country": {"name": "Senegal", "code": "SN"} },
+ {"country": {"name": "Serbia and Montenegro", "code": "CS"} },
+ {"country": {"name": "Seychelles", "code": "SC"} },
+ {"country": {"name": "Sierra Leone", "code": "SL"} },
+ {"country": {"name": "Singapore", "code": "SG"} },
+ {"country": {"name": "Slovakia", "code": "SK"} },
+ {"country": {"name": "Slovenia", "code": "SI"} },
+ {"country": {"name": "Solomon Islands", "code": "SB"} },
+ {"country": {"name": "Somalia", "code": "SO"} },
+ {"country": {"name": "South Africa", "code": "ZA"} },
+ {"country": {"name": "South Georgia and the South Sandwich Islands", "code": "GS"} },
+ {"country": {"name": "Spain", "code": "ES"} },
+ {"country": {"name": "Sri Lanka", "code": "LK"} },
+ {"country": {"name": "Sudan", "code": "SD"} },
+ {"country": {"name": "Suriname", "code": "SR"} },
+ {"country": {"name": "Svalbard and Jan Mayen", "code": "SJ"} },
+ {"country": {"name": "Swaziland", "code": "SZ"} },
+ {"country": {"name": "Sweden", "code": "SE"} },
+ {"country": {"name": "Switzerland", "code": "CH"} },
+ {"country": {"name": "Syrian Arab Republic", "code": "SY"} },
+ {"country": {"name": "Taiwan, Province of China", "code": "TW"} },
+ {"country": {"name": "Tajikistan", "code": "TJ"} },
+ {"country": {"name": "Tanzania, United Republic of", "code": "TZ"} },
+ {"country": {"name": "Thailand", "code": "TH"} },
+ {"country": {"name": "Timor-Leste", "code": "TL"} },
+ {"country": {"name": "Togo", "code": "TG"} },
+ {"country": {"name": "Tokelau", "code": "TK"} },
+ {"country": {"name": "Tonga", "code": "TO"} },
+ {"country": {"name": "Trinidad and Tobago", "code": "TT"} },
+ {"country": {"name": "Tunisia", "code": "TN"} },
+ {"country": {"name": "Turkey", "code": "TR"} },
+ {"country": {"name": "Turkmenistan", "code": "TM"} },
+ {"country": {"name": "Turks and Caicos Islands", "code": "TC"} },
+ {"country": {"name": "Tuvalu", "code": "TV"} },
+ {"country": {"name": "Uganda", "code": "UG"} },
+ {"country": {"name": "Ukraine", "code": "UA"} },
+ {"country": {"name": "United Arab Emirates", "code": "AE"} },
+ {"country": {"name": "United Kingdom", "code": "GB"} },
+ {"country": {"name": "United States", "code": "US"} },
+ {"country": {"name": "United States Minor Outlying Islands", "code": "UM"} },
+ {"country": {"name": "Uruguay", "code": "UY"} },
+ {"country": {"name": "Uzbekistan", "code": "UZ"} },
+ {"country": {"name": "Vanuatu", "code": "VU"} },
+ {"country": {"name": "Venezuela", "code": "VE"} },
+ {"country": {"name": "Viet Nam", "code": "VN"} },
+ {"country": {"name": "Virgin Islands, British", "code": "VG"} },
+ {"country": {"name": "Virgin Islands, U.S.", "code": "VI"} },
+ {"country": {"name": "Wallis and Futuna", "code": "WF"} },
+ {"country": {"name": "Western Sahara", "code": "EH"} },
+ {"country": {"name": "Yemen", "code": "YE"} },
+ {"country": {"name": "Zambia", "code": "ZM"} },
+ {"country": {"name": "Zimbabwe", "code": "ZW"} }
]
diff --git a/packages/oui-select/src/index.spec.js b/packages/oui-select/src/index.spec.js
index 604f4ed2..9b40b6d8 100644
--- a/packages/oui-select/src/index.spec.js
+++ b/packages/oui-select/src/index.spec.js
@@ -19,6 +19,8 @@ describe("ouiSelect", () => {
const getContainer = element => element[0].querySelector(".ui-select-container");
const getDropdownButton = element => element[0].querySelector(".ui-select-match");
+ const getMultipleDropdownButton = element => element[0].querySelector(".ui-select-match-container");
+ const getMultipleMatchItem = element => element[0].querySelectorAll(".ui-select-match-item");
const getDropdown = element => element[0].querySelector(".ui-select-choices-content");
const getFocusser = element => element[0].querySelector(".ui-select-focusser");
const getItemsGroups = element => element[0].querySelectorAll(".ui-select-choices-group");
@@ -38,8 +40,7 @@ describe("ouiSelect", () => {
title="${title}"
placeholder="${placeholder}"
items="$ctrl.countries"
- match="name">
-
+ match="country.name">
`, {
countries: data
});
@@ -57,8 +58,7 @@ describe("ouiSelect", () => {
title="Select a country"
placeholder="Select a country..."
items="$ctrl.countries"
- match="name">
-
+ match="country.name">
`, {
countries: data
});
@@ -84,8 +84,8 @@ describe("ouiSelect", () => {
title="Select a country"
placeholder="Select a country..."
items="$ctrl.countries"
- match="name">
-
+ match="country.name">
+
`, {
@@ -111,8 +111,8 @@ describe("ouiSelect", () => {
title="Select a country"
placeholder="Select a country..."
items="$ctrl.countries"
- match="name">
-
`, {
countries: data
});
@@ -140,6 +140,73 @@ describe("ouiSelect", () => {
});
});
+ describe("Multiple select", () => {
+ it("should not close dropdown when an item is selected", () => {
+ const element = TestUtils.compileTemplate(`
+
`, {
+ countries: data
+ });
+
+ const $triggerButton = angular.element(getMultipleDropdownButton(element));
+
+ expect($triggerButton.attr("aria-expanded")).toBe("false");
+
+ // Open the dropdown
+ $triggerButton.triggerHandler("click");
+
+ // Select 5th element and check if it's highlighted.
+ const $itemButton = angular.element(getDropdownItem(element, 4)); // eslint-disable-line no-magic-numbers
+ $itemButton.triggerHandler("click");
+
+ // The dropdown should stay opened.
+ expect($triggerButton.attr("aria-expanded")).toBe("true");
+ });
+
+ it("should remove item selected", () => {
+ const element = TestUtils.compileTemplate(`
+
`, {
+ countries: data
+ });
+
+ const $triggerButton = angular.element(getMultipleDropdownButton(element));
+
+ expect($triggerButton.attr("aria-expanded")).toBe("false");
+
+ // Open the dropdown
+ $triggerButton.triggerHandler("click");
+
+ // Select 5th element and check if it's highlighted.
+ let $itemButton = angular.element(getDropdownItem(element, 4)); // eslint-disable-line no-magic-numbers
+ $itemButton.triggerHandler("click");
+
+ $itemButton = angular.element(getDropdownItem(element, 4)); // eslint-disable-line no-magic-numbers
+ $itemButton.triggerHandler("click");
+
+ // The dropdown should stay opened.
+ let matchItems = getMultipleMatchItem(element);
+ expect(matchItems.length).toBe(2); // eslint-disable-line no-magic-numbers
+
+ angular.element(matchItems[0]).triggerHandler("click");
+ matchItems = getMultipleMatchItem(element);
+
+ expect(matchItems.length).toBe(1); // eslint-disable-line no-magic-numbers
+ });
+ });
+
describe("Not grouped", () => {
it("should display all the choices (objectArray)", () => {
const element = TestUtils.compileTemplate(`
@@ -148,8 +215,7 @@ describe("ouiSelect", () => {
title="Select a country"
placeholder="Select a country..."
items="$ctrl.countries"
- match="name">
-
+ match="country.name">
`, {
countries: data
});
@@ -159,8 +225,8 @@ describe("ouiSelect", () => {
$triggerButton.triggerHandler("click");
expect(getDropdownItems(element).length).toEqual(data.length);
- expect(angular.element(getDropdownItem(element, 0)).text()).toContain(data[0].name);
- expect(angular.element(getDropdownItem(element, data.length - 1)).text()).toContain(data[data.length - 1].name);
+ expect(angular.element(getDropdownItem(element, 0)).text()).toContain(data[0].country.name);
+ expect(angular.element(getDropdownItem(element, data.length - 1)).text()).toContain(data[data.length - 1].country.name);
expect(getItemsGroups(element).length).toEqual(1);
});
@@ -171,7 +237,6 @@ describe("ouiSelect", () => {
title="Select a country"
model="$ctrl.country"
items="$ctrl.array">
-
`, {
array: stringArray
});
@@ -189,16 +254,15 @@ describe("ouiSelect", () => {
describe("Grouped", () => {
it("should display all the choices", () => {
- const groupByFirstLetter = (item) => item.name.substr(0, 1).toUpperCase();
+ const groupByFirstLetter = (item) => item.country.name.substr(0, 1).toUpperCase();
const element = TestUtils.compileTemplate(`
`, {
countries: data,
groupByFirstLetter
@@ -228,9 +292,8 @@ describe("ouiSelect", () => {
title="Select a country"
placeholder="Select a country..."
items="$ctrl.countries"
- match="name"
+ match="country.name"
on-blur="$ctrl.onBlur()">
-
`, {
onBlur
});
@@ -238,6 +301,10 @@ describe("ouiSelect", () => {
$timeout.flush();
angular.element(getFocusser(element)).triggerHandler("blur");
+
+ // Need to flush again for the callback
+ $timeout.flush();
+
expect(onBlur).toHaveBeenCalled();
});
});
@@ -251,9 +318,8 @@ describe("ouiSelect", () => {
title="Select a country"
placeholder="Select a country..."
items="$ctrl.countries"
- match="name"
+ match="country.name"
on-focus="$ctrl.onFocus()">
-
`, {
onFocus
});
@@ -261,6 +327,10 @@ describe("ouiSelect", () => {
$timeout.flush();
angular.element(getFocusser(element)).triggerHandler("focus");
+
+ // Need to flush again for the callback
+ $timeout.flush();
+
expect(onFocus).toHaveBeenCalled();
});
});
@@ -274,9 +344,8 @@ describe("ouiSelect", () => {
title="Select a country"
placeholder="Select a country..."
items="$ctrl.countries"
- match="name"
+ match="country.name"
on-change="$ctrl.onChange(modelValue)">
-
`, {
countries: data,
onChange
@@ -302,16 +371,15 @@ describe("ouiSelect", () => {
describe("Disable options", () => {
it("should disable corresponding items", () => {
- const disableCountry = (item) => item.name === data[3].name;
+ const disableCountry = (item) => item.country.name === data[3].country.name;
const element = TestUtils.compileTemplate(`
`, {
countries: data,
disableCountry
@@ -329,13 +397,12 @@ describe("ouiSelect", () => {
const disableCountry = (item) => item.code === "";
const element = TestUtils.compileTemplate(`
`, {
countries,
disableCountry
diff --git a/packages/oui-select/src/select.controller.js b/packages/oui-select/src/select.controller.js
index fbe33041..84f980f8 100644
--- a/packages/oui-select/src/select.controller.js
+++ b/packages/oui-select/src/select.controller.js
@@ -15,11 +15,17 @@ export default class {
$onInit () {
addBooleanParameter(this, "disabled");
addBooleanParameter(this, "required");
+ addBooleanParameter(this, "multiple");
addBooleanParameter(this, "searchable");
}
$postLink () {
const $htmlContent = angular.element(this.htmlContent);
+
+ if (this.multiple) {
+ $htmlContent.attr("multiple", true);
+ }
+
this.$compile($htmlContent)(this.$scope, (clone) => {
this.$element.append(clone);
});
@@ -29,11 +35,14 @@ export default class {
.removeAttr("name")
.removeAttr("title");
- this.$select.focusser
- .on("blur", () => this.onUiSelectBlur())
- .on("focus", () => this.onUiSelectFocus());
+ if (this.$select.focusInput) {
+ this.$select.focusInput
+ .on("blur", () => this.onUiSelectBlur())
+ .on("focus", () => this.onUiSelectFocus());
+ }
});
+ // For focus from oui-field label
this.unregisterFocus = this.$scope.$on("oui:focus", () => this.$select.setFocus());
}
@@ -43,25 +52,75 @@ export default class {
}
}
- onUiSelectBlur () {
- if (this.fieldCtrl) {
- this.fieldCtrl.hasFocus = false;
- this.fieldCtrl.checkControlErrors(this.$select.$element[0], this.name);
+ removeChoice (e, index, callback) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ // Call $selectMultiple.removeChoice of ui-select
+ callback(index, this);
+ }
+
+ onUiSelectClick () {
+ if (!this.$select.focus) {
+ this.$select.setFocus();
}
- this.onBlur();
+ // The plugin toggle open/close by itself
+ this.$select.activate();
}
- onUiSelectFocus () {
- if (this.fieldCtrl) {
- this.fieldCtrl.hasFocus = true;
- this.fieldCtrl.hideErrors(this.$select.$element[0], this.name);
+ onUiSelectChange (modelValue) {
+ this.onChange({ modelValue });
+
+ // Fix focus input (unfocus on select in multiple mode)
+ if (this.multiple) {
+ this.$select.setFocus();
}
+ }
+
+ onUiSelectBlur () {
+ // Fix focus property (no focusser in multiple mode)
+ this.$select.focus = false;
+
+ // Need $timeout to get the refreshed value of $select.open from UI Select
+ this.$timeout(() => {
+ // Since UI Select toggle focus between focusInput and searchInput
+ // We need to check if the blur event is the one we really need (only in single mode)
+ if (this.multiple || !this.$select.open) {
+ if (this.fieldCtrl) {
+ this.fieldCtrl.hasFocus = false;
+ this.fieldCtrl.checkControlErrors(this.$select.$element[0], this.name);
+ }
+
+ this.onBlur();
+ } else {
+ this.isOpen = true;
+ }
+ });
+ }
- this.onFocus();
+ onUiSelectFocus () {
+ // Fix focus property (no focusser in multiple mode)
+ this.$select.focus = true;
+
+ // Need $timeout to get the refreshed value of $select.open from UI Select
+ this.$timeout(() => {
+ // Since UI Select toggle focus between focusInput and searchInput
+ // We need to check if the focus event is the one we really need (only in single mode)
+ if (this.multiple || this.$select.open || (!this.$select.open && !this.isOpen)) {
+ if (this.fieldCtrl) {
+ this.fieldCtrl.hasFocus = true;
+ this.fieldCtrl.hideErrors(this.$select.$element[0], this.name);
+ }
+
+ this.onFocus();
+ } else {
+ this.isOpen = false;
+ }
+ });
}
getPropertyValue (item) {
- return get(item, this.match, null);
+ return get(item, this.match, item);
}
}
diff --git a/packages/oui-select/src/select.directive.js b/packages/oui-select/src/select.directive.js
index 20aa8d0e..5ffadfd3 100644
--- a/packages/oui-select/src/select.directive.js
+++ b/packages/oui-select/src/select.directive.js
@@ -20,6 +20,7 @@ export default () => ({
groupBy: "",
required: "",
disabled: "",
+ multiple: "",
searchable: "",
onBlur: "&",
onFocus: "&",
diff --git a/packages/oui-select/src/select.html b/packages/oui-select/src/select.html
index 6b388bf4..2aa14785 100644
--- a/packages/oui-select/src/select.html
+++ b/packages/oui-select/src/select.html
@@ -1,16 +1,19 @@
diff --git a/packages/oui-select/src/templates/match-multiple.html b/packages/oui-select/src/templates/match-multiple.html
index 04def96c..fcea1609 100644
--- a/packages/oui-select/src/templates/match-multiple.html
+++ b/packages/oui-select/src/templates/match-multiple.html
@@ -1,16 +1,21 @@
diff --git a/packages/oui-select/src/templates/match.html b/packages/oui-select/src/templates/match.html
index f0e561af..7f43eabc 100644
--- a/packages/oui-select/src/templates/match.html
+++ b/packages/oui-select/src/templates/match.html
@@ -1,10 +1,11 @@