diff --git a/.htmlhintrc b/.htmlhintrc new file mode 100644 index 00000000..5d3fb3d9 --- /dev/null +++ b/.htmlhintrc @@ -0,0 +1,25 @@ +{ + "alt-require": true, + "attr-lowercase": true, + "attr-no-duplication": true, + "attr-unsafe-chars": true, + "attr-value-double-quotes": true, + "attr-value-not-empty": false, + "doctype-first": false, + "doctype-html5": true, + "head-script-disabled": true, + "href-abs-or-rel": false, + "id-class-ad-disabled": true, + "id-class-value": false, + "id-unique": true, + "inline-script-disabled": true, + "inline-style-disabled": false, + "space-tab-mixed-disabled": "space", + "spec-char-escape": true, + "src-not-empty": true, + "style-disabled": true, + "tag-pair": true, + "tag-self-close": false, + "tagname-lowercase": true, + "title-require": true +} diff --git a/assets/svg/.gitkeep b/assets/svg/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/assets/svg/checkbox-facade.svg b/assets/svg/checkbox-facade.svg deleted file mode 100644 index 2d2a2b02..00000000 --- a/assets/svg/checkbox-facade.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/assets/svg/close-icon.svg b/assets/svg/close-icon.svg deleted file mode 100644 index a56bf0f9..00000000 --- a/assets/svg/close-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/svg/error-icon_circle.svg b/assets/svg/error-icon_circle.svg deleted file mode 100644 index 2358d18a..00000000 --- a/assets/svg/error-icon_circle.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/svg/info-icon_circle.svg b/assets/svg/info-icon_circle.svg deleted file mode 100644 index 535273d6..00000000 --- a/assets/svg/info-icon_circle.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/svg/success-icon_circle.svg b/assets/svg/success-icon_circle.svg deleted file mode 100644 index f5c67761..00000000 --- a/assets/svg/success-icon_circle.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/svg/warning-icon_circle.svg b/assets/svg/warning-icon_circle.svg deleted file mode 100644 index b0c1fa83..00000000 --- a/assets/svg/warning-icon_circle.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/build/karma.conf.js b/build/karma.conf.js index 101b97c7..f4b1d029 100644 --- a/build/karma.conf.js +++ b/build/karma.conf.js @@ -11,6 +11,7 @@ module.exports = function (config) { // 1. install corresponding karma launcher // http://karma-runner.github.io/0.13/config/browsers.html // 2. add it to the `browsers` array below. + basePath: "../", browsers: ["PhantomJS"], frameworks: ["jasmine"], client: { @@ -18,13 +19,12 @@ module.exports = function (config) { includeStack: true } }, - reporters: ["nyan"], files: [ require.resolve("angular"), // eslint-disable-line no-undef require.resolve("angular-mocks"), // eslint-disable-line no-undef require.resolve("angular-aria"), // eslint-disable-line no-undef require.resolve("angular-sanitize"), // eslint-disable-line no-undef - "../packages/oui-angular/src/index.spec.js" + "packages/**/tests/index.js" ], preprocessors: { // eslint-disable-next-line no-undef @@ -33,7 +33,7 @@ module.exports = function (config) { [require.resolve("angular-mocks")]: ["webpack", "sourcemap"], [require.resolve("angular-aria")]: ["webpack", "sourcemap"], [require.resolve("angular-sanitize")]: ["webpack", "sourcemap"], - "../packages/oui-angular/src/index.spec.js": ["webpack", "sourcemap"] + "packages/**/tests/index.js": ["webpack", "sourcemap"] }, webpack: webpackConfig, webpackMiddleware: { @@ -43,7 +43,7 @@ module.exports = function (config) { } }, coverageReporter: { - dir: "../coverage/", + dir: "coverage/", reporters: [ { type: "text" }, { type: "lcov", subdir: "report-lcov" }, diff --git a/index.html b/index.html deleted file mode 100644 index fef47ab0..00000000 --- a/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - Preview - - - - - - - - diff --git a/package.json b/package.json index bd5a7c6a..842a3758 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,8 @@ "build:watch": "webpack --progress --colors --config build/webpack.dist.config.js --watch", "eslint": "eslint ./packages", "eslint:fix": "eslint --fix ./packages", - "test": "npm run unit:ci", - "unit": "cross-env NODE_ENV=test babel-node node_modules/karma/bin/karma start build/karma.conf.js --reporters nyan,coverage --single-run", - "unit:ci": "cross-env NODE_ENV=test babel-node node_modules/karma/bin/karma start build/karma.conf.js --reporters spec,coverage --single-run", - "unit:watch": "cross-env NODE_ENV=test BABEL_ENV=test node -r babel-register node_modules/karma/bin/karma start build/karma.conf.js --watch" + "test": "cross-env NODE_ENV=test babel-node ./node_modules/karma/bin/karma start build/karma.conf.js --reporters spec,coverage --single-run", + "test:watch": "cross-env NODE_ENV=test BABEL_ENV=test node -r babel-register ./node_modules/karma/bin/karma start build/karma.conf.js --watch" }, "config": { "commitizen": { @@ -74,11 +72,8 @@ "istanbul-instrumenter-loader": "^3.0.1", "jasmine-core": "^3.2.1", "karma": "^3.0.0", - "karma-chrome-launcher": "^2.2.0", "karma-coverage": "^1.1.2", "karma-jasmine": "^1.1.2", - "karma-junit-reporter": "^1.2.0", - "karma-nyan-reporter": "^0.2.5", "karma-phantomjs-launcher": "^1.0.4", "karma-sourcemap-loader": "^0.3.7", "karma-spec-reporter": "^0.0.32", diff --git a/packages/common/component-utils.js b/packages/common/component-utils.js index bfa58020..91067e3a 100644 --- a/packages/common/component-utils.js +++ b/packages/common/component-utils.js @@ -9,9 +9,10 @@ */ export function addBooleanParameter (controller, parameterName) { const ctrl = controller; - if (angular.isDefined(ctrl.$attrs[parameterName]) && - ctrl.$attrs[parameterName] === "") { - ctrl[parameterName] = true; + if (ctrl.$attrs) { + if (angular.isDefined(ctrl.$attrs[parameterName]) && ctrl.$attrs[parameterName] === "") { + ctrl[parameterName] = true; + } } } @@ -27,9 +28,11 @@ export function addBooleanParameter (controller, parameterName) { */ export function addDefaultParameter (controller, parameterName, defaultValue) { const ctrl = controller; - if (!angular.isDefined(ctrl.$attrs[parameterName]) || - (angular.isDefined(ctrl.$attrs[parameterName]) && ctrl.$attrs[parameterName].trim() === "")) { - ctrl[parameterName] = defaultValue; + if (ctrl.$attrs) { + if (!angular.isDefined(ctrl.$attrs[parameterName]) || + (angular.isDefined(ctrl.$attrs[parameterName]) && ctrl.$attrs[parameterName].trim() === "")) { + ctrl[parameterName] = defaultValue; + } } } diff --git a/packages/oui-action-menu/tests/index.js b/packages/oui-action-menu/tests/index.js new file mode 100644 index 00000000..ebd31bdb --- /dev/null +++ b/packages/oui-action-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-angular/package.json b/packages/oui-angular/package.json index fbf5e08b..a227fcd9 100644 --- a/packages/oui-angular/package.json +++ b/packages/oui-angular/package.json @@ -19,8 +19,7 @@ "@ovh-ui/oui-chips": "^1.0.0", "@ovh-ui/oui-clipboard": "^1.0.0", "@ovh-ui/oui-collapsible": "^1.0.0", - "@ovh-ui/oui-criteria-adder": "^1.0.0", - "@ovh-ui/oui-criteria-container": "^1.0.0", + "@ovh-ui/oui-criteria": "^1.0.0", "@ovh-ui/oui-datagrid": "^1.0.0", "@ovh-ui/oui-dropdown": "^1.0.0", "@ovh-ui/oui-field": "^1.0.0", diff --git a/packages/oui-angular/src/index.js b/packages/oui-angular/src/index.js index 62b6ec9d..675e886c 100644 --- a/packages/oui-angular/src/index.js +++ b/packages/oui-angular/src/index.js @@ -7,6 +7,7 @@ import Checkbox from "@ovh-ui/oui-checkbox"; import Chips from "@ovh-ui/oui-chips"; import Clipboard from "@ovh-ui/oui-clipboard"; import Collapsible from "@ovh-ui/oui-collapsible"; +import Criteria from "@ovh-ui/oui-criteria"; import CriteriaAdder from "@ovh-ui/oui-criteria-adder"; import CriteriaContainer from "@ovh-ui/oui-criteria-container"; import Datagrid from "@ovh-ui/oui-datagrid"; @@ -24,6 +25,7 @@ import Navbar from "@ovh-ui/oui-navbar"; import Numeric from "@ovh-ui/oui-numeric"; import PageHeader from "@ovh-ui/oui-page-header"; import Pagination from "@ovh-ui/oui-pagination"; +import Password from "@ovh-ui/oui-password"; import Popover from "@ovh-ui/oui-popover"; import Progress from "@ovh-ui/oui-progress"; import Radio from "@ovh-ui/oui-radio"; @@ -38,6 +40,7 @@ import Switch from "@ovh-ui/oui-switch"; import Tabs from "@ovh-ui/oui-tabs"; import Textarea from "@ovh-ui/oui-textarea"; import Tile from "@ovh-ui/oui-tile"; +import Timepicker from "@ovh-ui/oui-timepicker"; import Tooltip from "@ovh-ui/oui-tooltip"; export default angular @@ -51,8 +54,9 @@ export default angular Chips, Clipboard, Collapsible, - CriteriaAdder, - CriteriaContainer, + Criteria, + CriteriaAdder, // Deprecated: Suppport only for old use + CriteriaContainer, // Deprecated: Suppport only for old use Datagrid, Dropdown, DualList, @@ -68,6 +72,7 @@ export default angular Numeric, PageHeader, Pagination, + Password, Popover, Progress, Radio, @@ -82,6 +87,7 @@ export default angular Tabs, Textarea, Tile, + Timepicker, Tooltip ]) .name; diff --git a/packages/oui-angular/src/index.spec.js b/packages/oui-angular/src/index.spec.js deleted file mode 100644 index d62f271c..00000000 --- a/packages/oui-angular/src/index.spec.js +++ /dev/null @@ -1,47 +0,0 @@ -import "@ovh-ui/common/test-utils"; - -loadTests(require.context("../../oui-action-menu/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-autocomplete/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-back-button/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-button/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-calendar/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-checkbox/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-chips/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-clipboard/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-collapsible/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-criteria-adder/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-criteria-container/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-datagrid/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-dropdown/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-dual-list/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-field/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-file/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-form-actions/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-inline-adder/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-message/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-modal/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-navbar/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-numeric/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-page-header/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-pagination/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-popover/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-progress/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-radio/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-search/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-select/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-select-picker/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-skeleton/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-slideshow/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-spinner/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-stepper/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-switch/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-tabs/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-tile/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-textarea/src/", true, /.*((\.spec)|(index))$/)); -loadTests(require.context("../../oui-tooltip/src/", true, /.*((\.spec)|(index))$/)); - -function loadTests (context) { - context.keys().forEach(context); -} diff --git a/packages/oui-autocomplete/tests/index.js b/packages/oui-autocomplete/tests/index.js new file mode 100644 index 00000000..ebd31bdb --- /dev/null +++ b/packages/oui-autocomplete/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-back-button/tests/index.js b/packages/oui-back-button/tests/index.js new file mode 100644 index 00000000..ebd31bdb --- /dev/null +++ b/packages/oui-back-button/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-button/tests/index.js b/packages/oui-button/tests/index.js new file mode 100644 index 00000000..ebd31bdb --- /dev/null +++ b/packages/oui-button/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-calendar/README.md b/packages/oui-calendar/README.md index 0d54e3cd..29e3a1c2 100644 --- a/packages/oui-calendar/README.md +++ b/packages/oui-calendar/README.md @@ -160,6 +160,16 @@ Use `mode` to set a different selection mode for the calendar * `selectedDates` returns an array of Date objects selected by the user. When there are no dates selected, the array is empty. * `dateStr` returns a string representation of the latest selected Date object by the user. The string is formatted as per the `dateFormat` option. +## Variants + +### Timepicker + +See Action menu component. + +```html:preview + +``` + ## API | Attribute | Type | Binding | One-time Binding | Values | Default | Description @@ -168,19 +178,19 @@ Use `mode` to set a different selection mode for the calendar | `id` | string | @? | yes | n/a | n/a | id attribute of the field | `name` | string | @? | yes | n/a | n/a | name attribute of the field | `placeholder` | string | @? | yes | n/a | n/a | placeholder text -| `inline` | boolean | { - this.options[hook] = (selectedDates, dateStr) => { + this.config[hook] = (selectedDates, dateStr) => { this.model = dateStr; this.$timeout(this[hook]({ selectedDates, dateStr })); }; @@ -28,11 +30,15 @@ export default class { setOptionsProperty (property, value) { if (angular.isDefined(value)) { - this.options[property] = value; + this.config[property] = value; } } initCalendarInstance () { + if (this.options) { + this.config = merge(this.config, this.options); + } + // Set options from attributes this.setOptionsProperty("appendTo", this.appendTo); this.setOptionsProperty("defaultDate", this.model); @@ -50,7 +56,7 @@ export default class { this.setOptionsProperty("dateFormat", this.format); if (angular.isDefined(this.altFormat)) { - this.setOptionsProperty("altInput", true); + this.setOptionsProperty("altInput", !this.disabled); this.setOptionsProperty("altFormat", this.altFormat); } @@ -76,7 +82,7 @@ export default class { }); // Init the flatpickr instance - this.flatpickr = new Flatpickr(this.$element.find("input")[0], this.options); + this.flatpickr = new Flatpickr(this.$element.find("input")[0], this.config); } $onInit () { @@ -84,10 +90,14 @@ export default class { addBooleanParameter(this, "disabled"); addBooleanParameter(this, "enableTime"); addBooleanParameter(this, "inline"); + addBooleanParameter(this, "noCalendar"); addBooleanParameter(this, "required"); addBooleanParameter(this, "static"); addBooleanParameter(this, "weekNumbers"); + addDefaultParameter(this, "id", `ouiCalendar${this.$id}`); + addDefaultParameter(this, "name", `ouiCalendar${this.$id}`); + this.initCalendarInstance(); } @@ -96,16 +106,20 @@ export default class { } $postLink () { - // Avoid $element DOM unsync for jqLite methods this.$timeout(() => { + const controls = angular.element(this.$element[0].querySelectorAll(".oui-calendar__control")); + this.$element .addClass("oui-calendar") .removeAttr("id") .removeAttr("name"); - // Add class for `inline` + // Avoid 'alt-input' to take bad value of placeholder + controls.attr("placeholder", this.placeholder); + if (this.inline) { this.$element.addClass("oui-calendar_inline"); + controls.attr("type", "hidden"); } }); } diff --git a/packages/oui-calendar/src/calendar.html b/packages/oui-calendar/src/calendar.html index a422784f..67b32f10 100644 --- a/packages/oui-calendar/src/calendar.html +++ b/packages/oui-calendar/src/calendar.html @@ -3,20 +3,7 @@ autocomplete="off" ng-attr-id="{{::$ctrl.id}}" ng-attr-name="{{::$ctrl.name}}" - ng-attr-placeholder="{{::$ctrl.placeholder}}" ng-model="$ctrl.model" ng-disabled="$ctrl.disabled" ng-required="$ctrl.required" /> - diff --git a/packages/oui-calendar/src/index.spec.js b/packages/oui-calendar/src/index.spec.js index e35fedf1..d0bf51a4 100644 --- a/packages/oui-calendar/src/index.spec.js +++ b/packages/oui-calendar/src/index.spec.js @@ -65,7 +65,7 @@ describe("ouiCalendar", () => { $timeout.flush(); - expect(controller.options.inline).toBe(true); + expect(controller.config.inline).toBe(true); expect(component.hasClass("oui-calendar_inline")).toBe(true); }); @@ -76,7 +76,7 @@ describe("ouiCalendar", () => { $timeout.flush(); - expect(controller.options.appendTo).toBeUndefined(); + expect(controller.config.appendTo).toBeUndefined(); expect(calendar).toBeNull(); }); @@ -98,6 +98,8 @@ describe("ouiCalendar", () => { const component = testUtils.compileTemplate(''); const input = component.find("input"); + $timeout.flush(); + expect(input.attr("placeholder")).toBe("foo"); }); @@ -106,7 +108,7 @@ describe("ouiCalendar", () => { const ctrl = component.controller("ouiCalendar"); ctrl.setOptionsProperty("foo", "bar"); - expect(ctrl.options.foo).toBe("bar"); + expect(ctrl.config.foo).toBe("bar"); }); it("should change the value formatting of the model and the input", () => { @@ -124,9 +126,9 @@ describe("ouiCalendar", () => { const input = component[0].querySelector(".oui-calendar__control"); const altInput = component[0].querySelector(".oui-calendar__control_alt"); - expect(ctrl.options.dateFormat).toBe(format); - expect(ctrl.options.altInput).toBe(true); - expect(ctrl.options.altFormat).toBe(altFormat); + expect(ctrl.config.dateFormat).toBe(format); + expect(ctrl.config.altInput).toBe(true); + expect(ctrl.config.altFormat).toBe(altFormat); expect(input.value).toBe(formatDate); expect(altInput.value).toBe(altFormatDate); }); @@ -136,7 +138,7 @@ describe("ouiCalendar", () => { const ctrl = component.controller("ouiCalendar"); ctrl.setEventHooks(["foo"]); - expect(typeof ctrl.options.foo).toBe("function"); + expect(typeof ctrl.config.foo).toBe("function"); }); it("should call function of events attributes", () => { @@ -153,7 +155,7 @@ describe("ouiCalendar", () => { onOpenSpy }); const ctrl = component.controller("ouiCalendar"); - const today = ctrl.flatpickr.parseDate("today", ctrl.options.dateFormat); + const today = ctrl.flatpickr.parseDate("today", ctrl.config.dateFormat); ctrl.setModelValue(today); expect(onChangeSpy).toHaveBeenCalledWith([today], ctrl.model); @@ -162,26 +164,5 @@ describe("ouiCalendar", () => { ctrl.flatpickr.close(); expect(onCloseSpy).toHaveBeenCalledWith([today], ctrl.model); }); - - // it("should set the value to today's date when 'today' button is clicked", () => { - // const component = testUtils.compileTemplate(''); - // const ctrl = component.controller("ouiCalendar"); - // const button = component.find("button").eq(0); - // const today = ctrl.flatpickr.formatDate(new Date(), ctrl.options.dateFormat); - - // button.triggerHandler("click"); - // expect(ctrl.model).toBe(today); - // }); - - // it("should reset the value when 'reset' button is clicked", () => { - // const component = testUtils.compileTemplate('', { - // model: "today" - // }); - // const ctrl = component.controller("ouiCalendar"); - // const button = component.find("button").eq(1); - - // button.triggerHandler("click"); - // expect(ctrl.model).toBe(""); - // }); }); }); diff --git a/packages/oui-calendar/tests/index.js b/packages/oui-calendar/tests/index.js new file mode 100644 index 00000000..ebd31bdb --- /dev/null +++ b/packages/oui-calendar/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-checkbox/tests/index.js b/packages/oui-checkbox/tests/index.js new file mode 100644 index 00000000..ebd31bdb --- /dev/null +++ b/packages/oui-checkbox/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-chips/src/chips.component.js b/packages/oui-chips/src/chips.component.js index 5f5df162..d380142e 100644 --- a/packages/oui-chips/src/chips.component.js +++ b/packages/oui-chips/src/chips.component.js @@ -2,9 +2,6 @@ import controller from "./chips.controller"; import template from "./chips.html"; export default { - require: { - criteriaContainer: "?^^ouiCriteriaContainer" - }, template, controller, bindings: { diff --git a/packages/oui-chips/src/chips.controller.js b/packages/oui-chips/src/chips.controller.js index 4494d5d8..b7ab5f6d 100644 --- a/packages/oui-chips/src/chips.controller.js +++ b/packages/oui-chips/src/chips.controller.js @@ -27,9 +27,5 @@ export default class { const removed = angular.copy(this.items.splice(index, 1)[0]); const items = angular.copy(this.items); this.onRemove({ items, removed }); - - if (this.criteriaContainer) { - this.criteriaContainer.remove(removed); - } } } diff --git a/packages/oui-chips/src/index.spec.js b/packages/oui-chips/src/index.spec.js index 804c167b..40fe4f5f 100644 --- a/packages/oui-chips/src/index.spec.js +++ b/packages/oui-chips/src/index.spec.js @@ -1,4 +1,3 @@ -import cloneDeep from "lodash/cloneDeep"; import mockData from "./index.spec.data.json"; describe("ouiChips", () => { @@ -6,7 +5,6 @@ describe("ouiChips", () => { let testUtils; beforeEach(angular.mock.module("oui.chips")); - beforeEach(angular.mock.module("oui.criteria-container")); beforeEach(angular.mock.module("oui.test-utils")); beforeEach(inject((_$timeout_, _TestUtils_) => { @@ -65,27 +63,5 @@ describe("ouiChips", () => { const controllerItems = angular.copy(controller.items); expect(onRemoveSpy).toHaveBeenCalledWith(controllerItems, firstChip); }); - - describe("With criteria container", () => { - it("should remove criterion in criteria container", () => { - const onChangeSpy = jasmine.createSpy("onChangeSpy"); - component = testUtils.compileTemplate(` - - - - - `, { - items: mockData.criteria, - onChangeSpy - }); - - const criteriaContainerController = component.controller("ouiCriteriaContainer"); - criteriaContainerController.criteria = cloneDeep(mockData.criteria); - - component.find("button").eq(0).triggerHandler("click"); - expect(onChangeSpy).toHaveBeenCalledWith(mockData.criteria.slice(1)); - }); - }); }); }); diff --git a/packages/oui-chips/tests/index.js b/packages/oui-chips/tests/index.js new file mode 100644 index 00000000..ebd31bdb --- /dev/null +++ b/packages/oui-chips/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-clipboard/tests/index.js b/packages/oui-clipboard/tests/index.js new file mode 100644 index 00000000..ebd31bdb --- /dev/null +++ b/packages/oui-clipboard/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-collapsible/tests/index.js b/packages/oui-collapsible/tests/index.js new file mode 100644 index 00000000..ebd31bdb --- /dev/null +++ b/packages/oui-collapsible/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-criteria-adder/src/index.js b/packages/oui-criteria-adder/src/index.js index f51b5754..53d182a1 100644 --- a/packages/oui-criteria-adder/src/index.js +++ b/packages/oui-criteria-adder/src/index.js @@ -1,8 +1,7 @@ -import CriteriaAdder from "./criteria-adder.component"; -import CriteriaAdderProvider from "./criteria-adder.provider"; +import Criteria from "@ovh-ui/oui-criteria"; +// Deprecated: Support only for old use +// Has been moved to 'oui.criteria' export default angular - .module("oui.criteria-adder", []) - .component("ouiCriteriaAdder", CriteriaAdder) - .provider("ouiCriteriaAdderConfiguration", CriteriaAdderProvider) + .module("oui.criteria-adder", [Criteria]) .name; diff --git a/packages/oui-criteria-container/src/criteria-container.component.js b/packages/oui-criteria-container/src/criteria-container.component.js deleted file mode 100644 index 3559b1e1..00000000 --- a/packages/oui-criteria-container/src/criteria-container.component.js +++ /dev/null @@ -1,10 +0,0 @@ -import controller from "./criteria-container.controller"; - -export default { - template: "", - transclude: true, - controller, - bindings: { - onChange: "&" - } -}; diff --git a/packages/oui-criteria-container/src/criteria-container.controller.js b/packages/oui-criteria-container/src/criteria-container.controller.js deleted file mode 100644 index 37fa3ce6..00000000 --- a/packages/oui-criteria-container/src/criteria-container.controller.js +++ /dev/null @@ -1,75 +0,0 @@ -import findIndex from "lodash/findIndex"; - -export default class CriteriaController { - $onInit () { - this.criteria = []; - } - - triggerChange () { - if (this.onChange) { - this.onChange({ modelValue: this.criteria }); - } - } - - indexOfCriterion (criterion) { - let criterionIndex = this.criteria.length - 1; - while (criterionIndex >= 0 && !angular.equals(this.criteria[criterionIndex], criterion)) { - --criterionIndex; - } - return criterionIndex; - } - - setPreviewCriterion (previewCriterion) { - const criterionIndex = findIndex(this.criteria, ["preview", true]); - previewCriterion.preview = true; - if (criterionIndex > -1) { - this.criteria[criterionIndex] = previewCriterion; - } else { - this.criteria.push(previewCriterion); - } - this.triggerChange(); - } - - deletePreviewCriterion () { - const previewCriterionIndex = findIndex(this.criteria, ["preview", true]); - if (previewCriterionIndex > -1) { - this.criteria.splice(previewCriterionIndex, 1); - this.triggerChange(); - } - } - - add (criterion) { - // Delete same preview criterion if it exists. - const previewCriterion = angular.copy(criterion); - previewCriterion.preview = true; - - const previewCriterionIndex = this.indexOfCriterion(previewCriterion); - if (previewCriterionIndex > -1) { - this.criteria.splice(previewCriterionIndex, 1); - } - - // Add the criterion if it does not exist. - if (this.indexOfCriterion(criterion) < 0) { - this.criteria.push(criterion); - this.triggerChange(); - } - } - - remove (criterion) { - const criterionIndex = this.indexOfCriterion(criterion); - if (criterionIndex > -1) { - this.criteria.splice(criterionIndex, 1); - this.triggerChange(); - } - } - - set (criteria) { - this.criteria = criteria; - this.triggerChange(); - } - - clear () { - this.criteria = []; - this.triggerChange(); - } -} diff --git a/packages/oui-criteria-container/src/index.js b/packages/oui-criteria-container/src/index.js index e6d31978..6cb37a39 100644 --- a/packages/oui-criteria-container/src/index.js +++ b/packages/oui-criteria-container/src/index.js @@ -1,6 +1,7 @@ -import CriteriaContainer from "./criteria-container.component"; +import Criteria from "@ovh-ui/oui-criteria"; +// Deprecated: Support only for old use +// Has been moved to 'oui.criteria' export default angular - .module("oui.criteria-container", []) - .component("ouiCriteriaContainer", CriteriaContainer) + .module("oui.criteria-container", [Criteria]) .name; diff --git a/packages/oui-criteria-container/src/index.spec.js b/packages/oui-criteria-container/src/index.spec.js deleted file mode 100644 index 887fa7ef..00000000 --- a/packages/oui-criteria-container/src/index.spec.js +++ /dev/null @@ -1,143 +0,0 @@ -import CriteriaContainerController from "./criteria-container.controller"; - -describe("ouiCriteriaContainer", () => { - const criterion = { - property: "column1", - operator: "equal", - value: "test" - }; - - const criterion2 = { - property: "column2", - operator: "equal", - value: "test" - }; - - const previewCriterion = { - property: "column3", - operator: "equal", - value: "test", - preview: true - }; - - const previewCriterion2 = { - property: "column3", - operator: "equal", - value: "anotherTest", - preview: true - }; - - beforeEach(angular.mock.module("oui.criteria-container")); - beforeEach(angular.mock.module("oui.test-utils")); - - describe("Controller", () => { - let controller; - - beforeEach(() => { - controller = new CriteriaContainerController(); - controller.onChange = jasmine.createSpy("onChange"); - controller.$onInit(); - }); - - it("should init the criteria if not defined", () => { - expect(controller.criteria).toEqual([]); - }); - - it("should add a criteria", () => { - expect(controller.criteria.length).toEqual(0); - - controller.add(criterion); - expect(controller.criteria.length).toEqual(1); - expect(controller.criteria[0]).toEqual(criterion); - - expect(controller.onChange).toHaveBeenCalledWith({ modelValue: [criterion] }); - }); - - it("should not add an existing criteria", () => { - controller.criteria.push(criterion); - expect(controller.criteria.length).toEqual(1); - - controller.add(criterion); - expect(controller.criteria.length).toEqual(1); - - expect(controller.onChange).not.toHaveBeenCalled(); - }); - - it("should delete a criteria", () => { - controller.criteria.push(criterion); - controller.criteria.push(criterion2); - const expectedLength = 2; - expect(controller.criteria.length).toEqual(expectedLength); - - controller.remove(criterion); - expect(controller.criteria.length).toEqual(1); - expect(controller.criteria[0]).toEqual(criterion2); - - expect(controller.onChange).toHaveBeenCalledWith({ modelValue: [criterion2] }); - }); - - it("should not delete a nonexistent criteria", () => { - controller.criteria.push(criterion); - - expect(() => controller.remove(criterion2)).not.toThrow(); - expect(controller.onChange).not.toHaveBeenCalled(); - }); - - it("should set all criteria", () => { - const criteria = [criterion, criterion2]; - expect(controller.criteria.length).toEqual(0); - - controller.set(criteria); - const expectedLength = 2; - expect(controller.criteria.length).toEqual(expectedLength); - }); - - it("should delete all criteria", () => { - controller.criteria.push(criterion); - controller.criteria.push(criterion2); - const expectedLength = 2; - expect(controller.criteria.length).toEqual(expectedLength); - - controller.clear(); - expect(controller.criteria.length).toEqual(0); - }); - - describe("Preview criterion", () => { - it("should be added if nonexistent", () => { - expect(controller.criteria.length).toEqual(0); - - controller.setPreviewCriterion(previewCriterion); - expect(controller.criteria.length).toEqual(1); - }); - - it("should be removed", () => { - controller.criteria.push(previewCriterion); - expect(controller.criteria.length).toEqual(1); - - controller.deletePreviewCriterion(); - expect(controller.criteria.length).toEqual(0); - }); - - it("should replace previous preview criterion", () => { - controller.criteria.push(previewCriterion); - - controller.setPreviewCriterion(previewCriterion2); - expect(controller.criteria.length).toEqual(1); - expect(controller.criteria[0]).toEqual(previewCriterion2); - }); - - it("should be deleted if an equivalent non-preview criterion is added", () => { - const nonPreviewCriterion = Object.assign({}, previewCriterion); - nonPreviewCriterion.preview = false; - - // Initial state - controller.criteria.push(previewCriterion); - - controller.add(nonPreviewCriterion); - - expect(controller.criteria.length).toEqual(1); - expect(controller.criteria[0]).toEqual(nonPreviewCriterion); - }); - }); - }); -}); diff --git a/packages/oui-criteria-adder/README.md b/packages/oui-criteria/README.md similarity index 61% rename from packages/oui-criteria-adder/README.md rename to packages/oui-criteria/README.md index a7781ef2..55b0b5b3 100644 --- a/packages/oui-criteria-adder/README.md +++ b/packages/oui-criteria/README.md @@ -1,32 +1,63 @@ -# Criteria adder +# Criteria ## Usage -### Example +### Basic ```html:preview - + +``` + +### With search field + +```html:preview + + +``` + +### Events + +```html:preview + +
-

Input

+

Input

{{$ctrl.inputValue | json}}
-

Output

+

Output

{{$ctrl.outputValue | json}}
``` ## API +### oui-criteria + +| Attribute | Type | Binding | One-time Binding | Values | Default | Description +| ---- | ---- | ---- | ---- | ---- | ---- | ---- +| `model` | object | = | no | n/a | n/a | model bound to component +| `properties` | array | { ouiCriteriaAdderConfigurationProvider.setOperatorsByType({ // default operatorsByType - "boolean": [ - "is", - "isNot" - ], - date: [ - "is", - "isAfter", - "isBefore" - ], - number: [ - "is", - "smaller", - "bigger" - ], - options: [ - "is", - "isNot" - ], - string: [ - "contains", - "containsNot", - "startsWith", - "endsWith", - "is", - "isNot" - ] + "boolean": ["is", "isNot"], + date: ["is", "isAfter", "isBefore"], + number: ["is", "smaller", "bigger"], + options: ["is", "isNot"], + string: ["contains", "containsNot", "startsWith", "endsWith", "is", "isNot"] }); ouiCriteriaAdderConfigurationProvider.setTranslations({ // default translations column_label: "Column", diff --git a/packages/oui-criteria/package.json b/packages/oui-criteria/package.json new file mode 100644 index 00000000..17672957 --- /dev/null +++ b/packages/oui-criteria/package.json @@ -0,0 +1,14 @@ +{ + "name": "@ovh-ui/oui-criteria", + "version": "1.0.0", + "main": "./src/index.js", + "license": "BSD-3-Clause", + "author": "OVH SAS", + "dependencies": { + "@ovh-ui/oui-chips": "^1.0.0", + "@ovh-ui/oui-dropdown": "^1.0.0", + "@ovh-ui/oui-field": "^1.0.0", + "@ovh-ui/oui-search": "^1.0.0", + "@ovh-ui/oui-select": "^1.0.0" + } +} diff --git a/packages/oui-criteria-adder/src/criteria-adder.component.js b/packages/oui-criteria/src/adder/criteria-adder.component.js similarity index 75% rename from packages/oui-criteria-adder/src/criteria-adder.component.js rename to packages/oui-criteria/src/adder/criteria-adder.component.js index 10fa784b..88967f0d 100644 --- a/packages/oui-criteria-adder/src/criteria-adder.component.js +++ b/packages/oui-criteria/src/adder/criteria-adder.component.js @@ -3,12 +3,12 @@ import template from "./criteria-adder.html"; export default { require: { - criteriaContainer: "?^^ouiCriteriaContainer" + criteriaContainer: "?^^ouiCriteria" }, bindings: { id: "@?", - name: "@", - align: "@?", + name: "@?", + placement: "@?", properties: "<", disabled: " { - this.dropdownContent = this.$element[0]; - }); - - // Auto select first column - if (this.properties) { - this.columnModel = this.properties[0]; - } - - this.selectableOperators = this.filterSelectableOperators(); - this.operatorModel = this.selectableOperators[0]; - - this.resetValueModel(); - } - - $postLink () { - // Sometimes the digest cycle is done before dom manipulation, - // So we use $timeout to force the $apply - this.$timeout(() => { - this.$element - .addClass("oui-criteria-adder") - .removeAttr("id") - .removeAttr("name"); - }); - } - getOperatorsByType (type) { const operators = this.operators[type] || []; return operators.map((operator) => ({ @@ -157,4 +125,38 @@ export default class { title: this.translations[`operator_${type}_${operator}`] })); } + + $onInit () { + // Deprecated: Support for `align` attribute + // Will become addDefaultParameter(this, "placement", "center"); + this.placement = this.placement || this.$attrs.align || "center"; + + addDefaultParameter(this, "id", `ouiCriteriaAdder${this.$scope.$id}`); + addDefaultParameter(this, "name", `ouiCriteriaAdder${this.$scope.$id}`); + + this.$timeout(() => { + this.dropdownContent = this.$element[0]; + }); + + // Auto select first column + if (this.properties) { + this.columnModel = this.properties[0]; + } + + this.selectableOperators = this.filterSelectableOperators(); + this.operatorModel = this.selectableOperators[0]; + + this.resetValueModel(); + } + + $postLink () { + // Sometimes the digest cycle is done before dom manipulation, + // So we use $timeout to force the $apply + this.$timeout(() => { + this.$element + .addClass("oui-criteria-adder") + .removeAttr("id") + .removeAttr("name"); + }); + } } diff --git a/packages/oui-criteria-adder/src/criteria-adder.html b/packages/oui-criteria/src/adder/criteria-adder.html similarity index 100% rename from packages/oui-criteria-adder/src/criteria-adder.html rename to packages/oui-criteria/src/adder/criteria-adder.html diff --git a/packages/oui-criteria-adder/src/criteria-adder.provider.js b/packages/oui-criteria/src/adder/criteria-adder.provider.js similarity index 100% rename from packages/oui-criteria-adder/src/criteria-adder.provider.js rename to packages/oui-criteria/src/adder/criteria-adder.provider.js diff --git a/packages/oui-criteria-adder/src/index.spec.data.json b/packages/oui-criteria/src/adder/criteria-adder.spec.data.json similarity index 100% rename from packages/oui-criteria-adder/src/index.spec.data.json rename to packages/oui-criteria/src/adder/criteria-adder.spec.data.json diff --git a/packages/oui-criteria-adder/src/index.spec.js b/packages/oui-criteria/src/adder/criteria-adder.spec.js similarity index 96% rename from packages/oui-criteria-adder/src/index.spec.js rename to packages/oui-criteria/src/adder/criteria-adder.spec.js index ad693d49..b1150100 100644 --- a/packages/oui-criteria-adder/src/index.spec.js +++ b/packages/oui-criteria/src/adder/criteria-adder.spec.js @@ -1,5 +1,5 @@ import find from "lodash/find"; -import mockData from "./index.spec.data.json"; +import mockData from "./criteria-adder.spec.data.json"; const getValueComponent = $element => $element[0].querySelector("[name=barValue]"); @@ -7,11 +7,7 @@ describe("ouiCriteriaAdder", () => { let $timeout; let testUtils; - beforeEach(angular.mock.module("oui.criteria-adder")); - beforeEach(angular.mock.module("oui.dropdown")); - beforeEach(angular.mock.module("oui.field")); - beforeEach(angular.mock.module("oui.select")); - beforeEach(angular.mock.module("oui.criteria-container")); + beforeEach(angular.mock.module("oui.criteria")); beforeEach(angular.mock.module("oui.test-utils")); beforeEach(angular.mock.module("test.configuration")); @@ -24,7 +20,7 @@ describe("ouiCriteriaAdder", () => { let configuration; angular.module("test.configuration", [ - "oui.criteria-adder" + "oui.criteria" ]).config(ouiCriteriaAdderConfigurationProvider => { const operatorsByType = ouiCriteriaAdderConfigurationProvider.operatorsByType; operatorsByType.foo = ["bar"]; @@ -318,14 +314,15 @@ describe("ouiCriteriaAdder", () => { it("should add criterion in criteria container", () => { const onChangeSpy = jasmine.createSpy(); component = testUtils.compileTemplate(` - + - + `, { + model: [], properties: mockData.properties, onChangeSpy }); diff --git a/packages/oui-criteria/src/criteria.component.js b/packages/oui-criteria/src/criteria.component.js new file mode 100644 index 00000000..ae567a5e --- /dev/null +++ b/packages/oui-criteria/src/criteria.component.js @@ -0,0 +1,14 @@ +import controller from "./criteria.controller"; +import template from "./criteria.html"; + +export default { + bindings: { + model: "=", + properties: " !criterion.preview); + } + } + + indexOfCriterion (criterion) { + let criterionIndex = this.model.length - 1; + while (criterionIndex >= 0 && !angular.equals(this.model[criterionIndex], criterion)) { + --criterionIndex; + } + return criterionIndex; + } + + setPreviewCriterion (previewCriterion) { + const criterionIndex = findIndex(this.model, ["preview", true]); + previewCriterion.preview = true; + if (criterionIndex > -1) { + this.model[criterionIndex] = previewCriterion; + } else { + this.model.push(previewCriterion); + } + this.triggerChange(); + } + + deletePreviewCriterion () { + const previewCriterionIndex = findIndex(this.model, ["preview", true]); + if (previewCriterionIndex > -1) { + this.model.splice(previewCriterionIndex, 1); + this.triggerChange(); + } + } + + + static getCriterion (modelValue) { + return { + title: modelValue, + property: null, // any property + operator: "contains", + value: modelValue + }; + } + + add (criterion) { + // Delete same preview criterion if it exists. + const previewCriterion = angular.copy(criterion); + previewCriterion.preview = true; + + const previewCriterionIndex = this.indexOfCriterion(previewCriterion); + if (previewCriterionIndex > -1) { + this.model.splice(previewCriterionIndex, 1); + } + + // Add the criterion if it does not exist. + if (this.indexOfCriterion(criterion) < 0) { + this.model.push(criterion); + this.triggerChange(); + } + } + + remove (criterion) { + const criterionIndex = this.indexOfCriterion(criterion); + if (criterionIndex > -1) { + this.model.splice(criterionIndex, 1); + } + this.triggerChange(); + } + + set (criteria) { + this.model = criteria; + this.triggerChange(); + } + + clear () { + this.model = []; + this.triggerChange(); + } + + onCriterionChange (modelValue) { + if (modelValue && modelValue.length >= this.minLength) { + this.setPreviewCriterion(this.constructor.getCriterion(modelValue), true); + } else { + this.deletePreviewCriterion(); + } + } + + onCriterionReset () { + this.deletePreviewCriterion(); + } + + onCriterionSubmit (modelValue) { + if (modelValue && modelValue.length >= this.minLength) { + this.add(this.constructor.getCriterion(modelValue)); + this.deletePreviewCriterion(); + } + } + + $onInit () { + addBooleanParameter(this, "searchable"); + + this.model = this.model || []; + } +} diff --git a/packages/oui-criteria/src/criteria.html b/packages/oui-criteria/src/criteria.html new file mode 100644 index 00000000..ac7257d7 --- /dev/null +++ b/packages/oui-criteria/src/criteria.html @@ -0,0 +1,25 @@ +
+
+
+ + + + +
+ + diff --git a/packages/oui-criteria/src/index.js b/packages/oui-criteria/src/index.js new file mode 100644 index 00000000..ac674cfe --- /dev/null +++ b/packages/oui-criteria/src/index.js @@ -0,0 +1,21 @@ +import Chips from "@ovh-ui/oui-chips"; +import Criteria from "./criteria.component"; +import CriteriaAdder from "./adder/criteria-adder.component"; +import CriteriaAdderProvider from "./adder/criteria-adder.provider"; +import Dropdown from "@ovh-ui/oui-dropdown"; +import Field from "@ovh-ui/oui-field"; +import Search from "@ovh-ui/oui-search"; +import Select from "@ovh-ui/oui-select"; + +export default angular + .module("oui.criteria", [ + Chips, + Dropdown, + Field, + Search, + Select + ]) + .component("ouiCriteria", Criteria) + .component("ouiCriteriaAdder", CriteriaAdder) + .provider("ouiCriteriaAdderConfiguration", CriteriaAdderProvider) + .name; diff --git a/packages/oui-criteria/src/index.spec.js b/packages/oui-criteria/src/index.spec.js new file mode 100644 index 00000000..5929ec13 --- /dev/null +++ b/packages/oui-criteria/src/index.spec.js @@ -0,0 +1,336 @@ +describe("ouiCriteria", () => { + let testUtils; + let $timeout; + + const goodSearchText = "aa"; + const tooShortSearchText = "a"; + + const criterion = { + property: "column1", + operator: "equal", + value: "test" + }; + + const criterion2 = { + property: "column2", + operator: "equal", + value: "test" + }; + + const previewCriterion = { + property: "column3", + operator: "equal", + value: "test", + preview: true + }; + + const previewCriterion2 = { + property: "column3", + operator: "equal", + value: "anotherTest", + preview: true + }; + + beforeEach(angular.mock.module("oui.criteria")); + beforeEach(angular.mock.module("oui.test-utils")); + + beforeEach(inject((_$timeout_, _TestUtils_) => { + $timeout = _$timeout_; + testUtils = _TestUtils_; + })); + + describe("Controller", () => { + let component; + let controller; + + beforeEach(() => { + component = testUtils.compileTemplate('', { + model: [] + }); + + controller = component.controller("ouiCriteria"); + controller.onChange = jasmine.createSpy("onChange"); + }); + + it("should init the criteria if not defined", () => { + expect(controller.model).toEqual([]); + }); + + it("should add a criteria", () => { + expect(controller.model.length).toEqual(0); + + controller.add(criterion); + expect(controller.model.length).toEqual(1); + expect(controller.model[0]).toEqual(criterion); + + expect(controller.onChange).toHaveBeenCalledWith({ modelValue: [criterion] }); + }); + + it("should not add an existing criteria", () => { + controller.model.push(criterion); + expect(controller.model.length).toEqual(1); + + controller.add(criterion); + expect(controller.model.length).toEqual(1); + + expect(controller.onChange).not.toHaveBeenCalled(); + }); + + it("should delete a criteria", () => { + controller.model.push(criterion); + controller.model.push(criterion2); + const expectedLength = 2; + expect(controller.model.length).toEqual(expectedLength); + + controller.remove(criterion); + expect(controller.model.length).toEqual(1); + expect(controller.model[0]).toEqual(criterion2); + + expect(controller.onChange).toHaveBeenCalledWith({ modelValue: [criterion2] }); + }); + + it("should not delete a nonexistent criteria", () => { + controller.model.push(criterion); + + expect(() => controller.remove(criterion2)).not.toThrow(); + expect(controller.onChange).toHaveBeenCalled(); + }); + + it("should set all criteria", () => { + const criteria = [criterion, criterion2]; + expect(controller.model.length).toEqual(0); + + controller.set(criteria); + const expectedLength = 2; + expect(controller.model.length).toEqual(expectedLength); + }); + + it("should delete all criteria", () => { + controller.model.push(criterion); + controller.model.push(criterion2); + const expectedLength = 2; + expect(controller.model.length).toEqual(expectedLength); + + controller.clear(); + expect(controller.model.length).toEqual(0); + }); + + describe("Preview criterion", () => { + it("should be added if nonexistent", () => { + expect(controller.model.length).toEqual(0); + + controller.setPreviewCriterion(previewCriterion); + expect(controller.model.length).toEqual(1); + }); + + it("should be removed", () => { + controller.model.push(previewCriterion); + expect(controller.model.length).toEqual(1); + + controller.deletePreviewCriterion(); + expect(controller.model.length).toEqual(0); + }); + + it("should replace previous preview criterion", () => { + controller.model.push(previewCriterion); + + controller.setPreviewCriterion(previewCriterion2); + expect(controller.model.length).toEqual(1); + expect(controller.model[0]).toEqual(previewCriterion2); + }); + + it("should be deleted if an equivalent non-preview criterion is added", () => { + const nonPreviewCriterion = Object.assign({}, previewCriterion); + nonPreviewCriterion.preview = false; + + // Initial state + controller.model.push(previewCriterion); + + controller.add(nonPreviewCriterion); + + expect(controller.model.length).toEqual(1); + expect(controller.model[0]).toEqual(nonPreviewCriterion); + }); + }); + }); + + 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(` + + `, { + onChangeSpy + }); + + const input = element.find("input"); + input.val(searchText).triggerHandler("input"); + + // Need to flush for the debounce + $timeout.flush(); + + 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(` + + `, { + onChangeSpy + }); + + const input = element.find("input"); + input.val(searchText).triggerHandler("input"); + + // Need to flush for the debounce + $timeout.flush(); + + element.find("form").triggerHandler("submit"); + + setTimeout(() => { + expect(onChangeSpy).not.toHaveBeenCalled(); + done(); + }, debounceDelay); + }); + }); + + describe("on change", () => { + it("should add criterion in criteria container", done => { + const searchText = goodSearchText; + const onChangeSpy = jasmine.createSpy(); + const element = testUtils.compileTemplate(` + + `, { + onChangeSpy + }); + + const input = element.find("input"); + input.val(searchText).triggerHandler("input"); + + // Need to flush for the debounce + $timeout.flush(); + + 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).triggerHandler("input"); + expect(onChangeSpy).not.toHaveBeenCalled(); + done(); + }, delay); + }); + + it("should delete preview criterion if search becomes too short", done => { + const searchText = goodSearchText; + const onChangeSpy = jasmine.createSpy(); + const element = testUtils.compileTemplate(` + + `, { + onChangeSpy + }); + + const input = element.find("input"); + input.val(searchText).triggerHandler("input"); + + // Need to flush for the debounce + $timeout.flush(); + + setTimeout(() => { + expect(onChangeSpy).toHaveBeenCalledWith([{ + title: goodSearchText, + property: null, + operator: "contains", + value: goodSearchText, + preview: true + }]); + + input.val(tooShortSearchText); + input.triggerHandler("input"); + + // Need to flush for the debounce + $timeout.flush(); + + 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).triggerHandler("input"); + + // Need to flush for the debounce + $timeout.flush(); + + setTimeout(() => { + expect(onChangeSpy).toHaveBeenCalledWith([{ + title: goodSearchText, + property: null, + operator: "contains", + value: goodSearchText, + preview: true + }]); + + resetButton.triggerHandler("click"); + + setTimeout(() => { + expect(onChangeSpy).toHaveBeenCalledWith([]); + done(); + }, debounceDelay); + }, debounceDelay); + }); + }); + }); +}); diff --git a/packages/oui-criteria/tests/index.js b/packages/oui-criteria/tests/index.js new file mode 100644 index 00000000..ebd31bdb --- /dev/null +++ b/packages/oui-criteria/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-datagrid/package.json b/packages/oui-datagrid/package.json index e560790f..fbdb75e6 100644 --- a/packages/oui-datagrid/package.json +++ b/packages/oui-datagrid/package.json @@ -5,6 +5,10 @@ "license": "BSD-3-Clause", "author": "OVH SAS", "dependencies": { + "@ovh-ui/oui-checkbox": "^1.0.0", + "@ovh-ui/oui-criteria": "^1.0.0", + "@ovh-ui/oui-pagination": "^1.0.0", + "@ovh-ui/oui-spinner": "^1.0.0", "escape-string-regexp": "^1.0.5" } } diff --git a/packages/oui-datagrid/src/datagrid.controller.js b/packages/oui-datagrid/src/datagrid.controller.js index 91e90a67..c344bd78 100644 --- a/packages/oui-datagrid/src/datagrid.controller.js +++ b/packages/oui-datagrid/src/datagrid.controller.js @@ -241,9 +241,8 @@ export default class DatagridController { } onCriteriaChange (criteria) { - this.criteria = criteria; // with preview criteria - this.appliedCriteria = this.criteria - .filter(criterion => !criterion.preview); + // Preview criteria are visually filtered by oui-criteria + this.criteria = criteria; this.refreshData(() => { this.paging.setOffset(1); this.paging.setCriteria(criteria); diff --git a/packages/oui-datagrid/src/datagrid.html b/packages/oui-datagrid/src/datagrid.html index f66b8acf..8624c676 100644 --- a/packages/oui-datagrid/src/datagrid.html +++ b/packages/oui-datagrid/src/datagrid.html @@ -1,119 +1,108 @@ -
- -
- - - -
- -
-
+ + + +
+
-
-
- - - - - - - - - - - - - - - - - - - - - - -
- - - - - - -
- - - - - - - - - - -
- - -
-
+
+ + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + +
+ + + + + + + + + + +
+ + +
-
- - -
- +
+ + +
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 +
+ + + + + Must contain between 8 and 30 characters + + + Have at least one number + + + Have at least capital letter + + + +
+``` + +#### 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 | { + 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(` +
+ + + Must contain between 8 and 30 characters + + + Have at least one number + + + Have at least capital letter + + +
`, { + 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: " + 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: " + 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 | { 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: " - 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 | { 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"> - + match="country.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(` - + model="$ctrl.country" + title="Select a country" + placeholder="Select a country..." + items="$ctrl.countries" + disable-items="$ctrl.disableCountry($item)" + match="country.name"> `, { countries: data, disableCountry @@ -329,13 +397,12 @@ describe("ouiSelect", () => { const disableCountry = (item) => item.code === ""; const element = TestUtils.compileTemplate(` - + model="$ctrl.country" + title="Select a country" + placeholder="Select a country..." + items="$ctrl.countries" + disable-items="$ctrl.disableCountry($item)" + match="country.name"> `, { 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: " - + ng-attr-name="{{::$ctrl.name}}" + ng-attr-title="{{::$ctrl.title}}" + ng-init="$ctrl.$select = $select" + ng-model="$ctrl.model" + ng-required="$ctrl.required && !$select.open" + ng-disabled="$ctrl.disabled" + close-on-select="{{!$ctrl.multiple}}" + on-select="$ctrl.onUiSelectChange($model)" + search-enabled="{{::!!$ctrl.searchable}}" + theme="oui-ui-select"> + + + - + 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 @@