diff --git a/packages/oui-angular/src/index.js b/packages/oui-angular/src/index.js index 2b5f6d77..c32f6756 100644 --- a/packages/oui-angular/src/index.js +++ b/packages/oui-angular/src/index.js @@ -14,6 +14,7 @@ import Field from "@ovh-ui/oui-field"; import FormActions from "@ovh-ui/oui-form-actions"; import GuideMenu from "@ovh-ui/oui-guide-menu"; import HeaderTabs from "@ovh-ui/oui-header-tabs"; +import InlineAdder from "@ovh-ui/oui-inline-adder"; import Message from "@ovh-ui/oui-message"; import Modal from "@ovh-ui/oui-modal"; import Navbar from "@ovh-ui/oui-navbar"; @@ -53,6 +54,7 @@ export default angular FormActions, GuideMenu, HeaderTabs, + InlineAdder, Message, Modal, Navbar, diff --git a/packages/oui-angular/src/index.spec.js b/packages/oui-angular/src/index.spec.js index ed8ea126..67b46a72 100644 --- a/packages/oui-angular/src/index.spec.js +++ b/packages/oui-angular/src/index.spec.js @@ -16,6 +16,7 @@ loadTests(require.context("../../oui-field/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))$/)); diff --git a/packages/oui-calendar/README.md b/packages/oui-calendar/README.md index c223aac9..0d54e3cd 100644 --- a/packages/oui-calendar/README.md +++ b/packages/oui-calendar/README.md @@ -61,6 +61,17 @@ ``` +### Enabling time + +Use `enable-time` to show time selection after a date is selected. + +```html:preview + + +``` + +**Note**: See [Flatpickr documentation](https://flatpickr.js.org/examples/#time-picker) for more information. + ### Disabling dates Use `disable-date` to make certain dates unavailable for selection. @@ -168,7 +179,7 @@ Use `mode` to set a different selection mode for the calendar | `min-date` | object | ``` +### Access row index + +```html:preview + + + {{$rowIndex}} + + + {{$row.firstName}} {{$row.lastName}} + + + {{$value}} + + + + {{$value | date:shortDate}} + + +``` + ### Remote data ```html diff --git a/packages/oui-datagrid/src/cell/cell.controller.js b/packages/oui-datagrid/src/cell/cell.controller.js index d17d3cdd..ca62b284 100644 --- a/packages/oui-datagrid/src/cell/cell.controller.js +++ b/packages/oui-datagrid/src/cell/cell.controller.js @@ -40,6 +40,7 @@ export default class { this.cellScope.$row = this.row; this.cellScope.$column = this.column; this.cellScope.$value = this.row[this.column.name]; + this.cellScope.$rowIndex = this.index; if (this.column.compiledTemplate) { this.column.compiledTemplate(this.cellScope, clone => { diff --git a/packages/oui-datagrid/src/index.spec.js b/packages/oui-datagrid/src/index.spec.js index 0af9c61f..1871e269 100644 --- a/packages/oui-datagrid/src/index.spec.js +++ b/packages/oui-datagrid/src/index.spec.js @@ -1076,6 +1076,38 @@ describe("ouiDatagrid", () => { expect(actualCellHtml).toBe(`test: ${fakeData[0].lastName}`); }); + it("should support row index data binding inside cell", () => { + const element = TestUtils.compileTemplate(` + + + + test: {{ $rowIndex }} + + + `, { + rows: fakeData.slice(0, 5) + } + ); + + const $firstRow = getRow(element, 0); + expect( + getCell($firstRow, 1).children().children().html() + .trim()) + .toBe("test: 0"); + + const $middleRow = getRow(element, 2); + expect( + getCell($middleRow, 1).children().children().html() + .trim()) + .toBe("test: 2"); + + const $lastRow = getRow(element, 4); + expect( + getCell($lastRow, 1).children().children().html() + .trim()) + .toBe("test: 4"); + }); + it("should support parent binding inside cell", () => { const element = TestUtils.compileTemplate(` diff --git a/packages/oui-dropdown/src/index.spec.js b/packages/oui-dropdown/src/index.spec.js index f2fa14d1..0be80e1c 100644 --- a/packages/oui-dropdown/src/index.spec.js +++ b/packages/oui-dropdown/src/index.spec.js @@ -212,6 +212,32 @@ describe("ouiDropdown", () => { expect(link.attr("target")).toBe("_blank"); expect(link.attr("rel")).toBe("noopener"); }); + + it("should call click callback", () => { + const onLinkClickSpy = jasmine.createSpy("onLinkClickSpy"); + const onButtonClickSpy = jasmine.createSpy("onButtonClickSpy"); + const element = TestUtils.compileTemplate(` + + + + + + + + + `, { + onLinkClick: onLinkClickSpy, + onButtonClick: onButtonClickSpy + }); + + $timeout.flush(); + const items = angular.element(element[0].querySelectorAll("oui-dropdown-item")).children(); + angular.element(items[0]).triggerHandler("click"); + angular.element(items[1]).triggerHandler("click"); + + expect(onLinkClickSpy).toHaveBeenCalled(); + expect(onButtonClickSpy).toHaveBeenCalled(); + }); }); describe("Group", () => { diff --git a/packages/oui-dropdown/src/item/dropdown-item.html b/packages/oui-dropdown/src/item/dropdown-item.html index a7996605..4741b3df 100644 --- a/packages/oui-dropdown/src/item/dropdown-item.html +++ b/packages/oui-dropdown/src/item/dropdown-item.html @@ -12,7 +12,8 @@ ng-if="::!!$ctrl.href" ng-href="{{::$ctrl.href}}" ng-attr-target="{{::$ctrl.linkTarget}}" - ng-attr-rel="{{::$ctrl.linkRel}}"> + ng-attr-rel="{{::$ctrl.linkRel}}" + ng-click="$ctrl.onClick()"> {{::$ctrl.text}} {{::$ctrl.text}} diff --git a/packages/oui-inline-adder/README.md b/packages/oui-inline-adder/README.md new file mode 100644 index 00000000..76292d0f --- /dev/null +++ b/packages/oui-inline-adder/README.md @@ -0,0 +1,168 @@ +# Inline adder + + + +## Usage + +### Basic + +```html:preview + + + + + + + + + + + + + + +``` + +### Multiple rows + +```html:preview + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### Adaptive fields + +**Note**: Fields with `adaptive` attribute will adapt their size to their content. + +```html:preview + + + + + + + + + + + + + + + + + + + +``` + +### Events + +#### `on-add` + +**Note**: If you want to access the form inside `on-add` callback, you need to use the `form` variable as below. + +```html:preview + + + + + + + + + +
+

On Add

+
{{$ctrl.addedForm | json}}
+
+``` + +#### `on-remove` + +**Note**: If you want to access the form inside `on-remove` callback, you need to use the `form` variable as below. + +```html:preview + + + + + + + + + +
+

On Remove

+
{{$ctrl.removedForm | json}}
+
+``` + +#### `on-change` + +**Note**: If you want to access the forms array inside `on-change` callback, you need to use the `forms` variable as below. + +```html:preview + + + + + + + + + +
+

On Change

+
{{$ctrl.changedForms | json}}
+
+``` + +## API + +### oui-inline-adder + +| Attribute | Type | Binding | One-time binding | Values | Default | Description +| ---- | ---- | ---- | ---- | ---- | ---- | ---- +| `on-add` | function | & | no | n/a | n/a | handler triggered when a row is added +| `on-remove` | function | & | no | n/a | n/a | handler triggered when a row is removed +| `on-change` | function | & | no | n/a | n/a | handler triggered when rows have changed + +### oui-inline-adder-field + +| Attribute | Type | Binding | One-time binding | Values | Default | Description +| ---- | ---- | ---- | ---- | ---- | ---- | ---- +| `adaptive` | boolean | { + this.$element.addClass("oui-inline-adder__field"); + + if (this.adaptive) { + this.$element.addClass("oui-inline-adder__field_adaptive"); + } + }); + } +} diff --git a/packages/oui-inline-adder/src/index.js b/packages/oui-inline-adder/src/index.js new file mode 100644 index 00000000..d2b791a2 --- /dev/null +++ b/packages/oui-inline-adder/src/index.js @@ -0,0 +1,12 @@ +import InlineAdder from "./inline-adder.component"; +import InlineAdderField from "./field/inline-adder-field.component"; +import InlineAdderProvider from "./inline-adder.provider"; +import InlineAdderRow from "./row/inline-adder-row.component"; + +export default angular + .module("oui.inline-adder", []) + .component("ouiInlineAdder", InlineAdder) + .component("ouiInlineAdderField", InlineAdderField) + .component("ouiInlineAdderRow", InlineAdderRow) + .provider("ouiInlineAdderConfiguration", InlineAdderProvider) + .name; diff --git a/packages/oui-inline-adder/src/index.spec.js b/packages/oui-inline-adder/src/index.spec.js new file mode 100644 index 00000000..f3a16ac3 --- /dev/null +++ b/packages/oui-inline-adder/src/index.spec.js @@ -0,0 +1,176 @@ +describe("ouiInlineAdder", () => { + let TestUtils; + let $timeout; + let configuration; + + beforeEach(angular.mock.module("oui.inline-adder")); + beforeEach(angular.mock.module("oui.inline-adder.configuration")); + beforeEach(angular.mock.module("oui.test-utils")); + + beforeEach(inject((_TestUtils_, _$timeout_) => { + TestUtils = _TestUtils_; + $timeout = _$timeout_; + })); + + describe("Provider", () => { + angular.module("oui.inline-adder.configuration", [ + "oui.inline-adder" + ]).config(ouiInlineAdderConfigurationProvider => { + ouiInlineAdderConfigurationProvider.setTranslations({ + foo: "bar" + }); + }); + + beforeEach(inject(_ouiInlineAdderConfiguration_ => { + configuration = _ouiInlineAdderConfiguration_; + })); + + it("should have custom options", () => { + expect(configuration.translations.foo).toEqual("bar"); + }); + }); + + describe("Component", () => { + describe("oui-inline-adder", () => { + it("should have a default classname", () => { + const element = TestUtils.compileTemplate(` + + + + + + + + + `); + + $timeout.flush(); + + expect(element.hasClass("oui-inline-adder")).toBeTruthy(); + }); + + it("should have a form with a default id/name", () => { + const element = TestUtils.compileTemplate(` + + + + + + + + + `); + + const form = element.find("form"); + const id = `${element.controller("ouiInlineAdder").id}_0`; + const name = `${element.controller("ouiInlineAdder").name}_0`; + + expect(form.length).toBe(1); + expect(form.attr("id")).toBe(id); + expect(form.attr("name")).toBe(name); + }); + + it("should create a new form when submitted", () => { + const element = TestUtils.compileTemplate(` + + + + + + + + + `); + + let length = element.find("form").length; + + // Will have 2 rows + angular.element(element.find("form")[length - 1]).triggerHandler("submit"); + expect(element.find("form").length).toBe(length += 1); + + // Will have 3 rows + angular.element(element.find("form")[length - 1]).triggerHandler("submit"); + expect(element.find("form").length).toBe(length += 1); + }); + + it("should hide a form when removed", () => { + const element = TestUtils.compileTemplate(` + + + + + + + + + `); + + let length = element.find("form").length; + const form = angular.element(element.find("form")[0]); + + // Will have 2 rows + form.triggerHandler("submit"); + expect(element.find("form").length).toBe(length += 1); + + // Will have 1 row hidden + form.find("button").triggerHandler("click"); + expect(form.hasClass("ng-hide")).toBeTruthy(); + }); + }); + + describe("oui-inline-adder-row", () => { + it("should have a default classname", () => { + const element = TestUtils.compileTemplate(` + + + + + + + + + `); + + $timeout.flush(); + + expect(element.find("oui-inline-adder-row").hasClass("oui-inline-adder__row")).toBeTruthy(); + }); + }); + + describe("oui-inline-adder-field", () => { + it("should have a default classname", () => { + const element = TestUtils.compileTemplate(` + + + + + + + + + `); + + $timeout.flush(); + + expect(element.find("oui-inline-adder-field").hasClass("oui-inline-adder__field")).toBeTruthy(); + }); + + it("should have a variant classname", () => { + const element = TestUtils.compileTemplate(` + + + + + + + + + `); + + $timeout.flush(); + + expect(element.find("oui-inline-adder-field").hasClass("oui-inline-adder__field_adaptive")).toBeTruthy(); + }); + }); + }); +}); diff --git a/packages/oui-inline-adder/src/inline-adder.component.js b/packages/oui-inline-adder/src/inline-adder.component.js new file mode 100644 index 00000000..0a60a478 --- /dev/null +++ b/packages/oui-inline-adder/src/inline-adder.component.js @@ -0,0 +1,15 @@ +import controller from "./inline-adder.controller.js"; +import template from "./inline-adder.html"; + +export default { + bindings: { + id: "@?", + name: "@?", + onAdd: "&", + onChange: "&", + onRemove: "&" + }, + controller, + template, + transclude: true +}; diff --git a/packages/oui-inline-adder/src/inline-adder.controller.js b/packages/oui-inline-adder/src/inline-adder.controller.js new file mode 100644 index 00000000..e8bf4b53 --- /dev/null +++ b/packages/oui-inline-adder/src/inline-adder.controller.js @@ -0,0 +1,57 @@ +import { addDefaultParameter } from "@ovh-ui/common/component-utils"; +import filter from "lodash/filter"; + +export default class { + constructor ($attrs, $element, $scope, $timeout, ouiInlineAdderConfiguration) { + "ngInject"; + + this.$attrs = $attrs; + this.$element = $element; + this.$scope = $scope; + this.$timeout = $timeout; + this.translations = ouiInlineAdderConfiguration.translations; + } + + $onInit () { + this.forms = [true]; + this.isDisabled = [false]; + + addDefaultParameter(this, "id", `ouiInlineAdderForm${this.$scope.$id}`); + addDefaultParameter(this, "name", `ouiInlineAdderForm${this.$scope.$id}`); + } + + $postLink () { + this.$timeout(() => + this.$element.addClass("oui-inline-adder") + ); + } + + onFormsChange () { + // Filter boolean values used for ngShow + const forms = filter(this.forms, (item) => angular.isObject(item)); + this.onChange({ forms }); + } + + onFormSubmit (form, index) { + if (form.$valid) { + this.forms[index] = form; + + // Create new instance of form + this.isDisabled[index] = true; + this.forms.push(true); + + // Callbacks + this.onAdd({ form }); + this.onFormsChange(); + } + } + + onFormRemove (form, index) { + // Hide removed form to avoid refreshing ngRepeat + this.forms[index] = false; + + // Callback + this.onRemove({ form }); + this.onFormsChange(); + } +} diff --git a/packages/oui-inline-adder/src/inline-adder.html b/packages/oui-inline-adder/src/inline-adder.html new file mode 100644 index 00000000..9502e156 --- /dev/null +++ b/packages/oui-inline-adder/src/inline-adder.html @@ -0,0 +1,24 @@ +
+
+
+
+ + +
+
diff --git a/packages/oui-inline-adder/src/inline-adder.provider.js b/packages/oui-inline-adder/src/inline-adder.provider.js new file mode 100644 index 00000000..c2810a0f --- /dev/null +++ b/packages/oui-inline-adder/src/inline-adder.provider.js @@ -0,0 +1,25 @@ +import { merge } from "lodash"; + +export default class { + constructor () { + this.translations = { + ariaAddItem: "Add Item", + ariaRemoveItem: "Remove Item" + }; + } + + /** + * 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-inline-adder/src/row/inline-adder-row.component.js b/packages/oui-inline-adder/src/row/inline-adder-row.component.js new file mode 100644 index 00000000..4a62ecae --- /dev/null +++ b/packages/oui-inline-adder/src/row/inline-adder-row.component.js @@ -0,0 +1,16 @@ +export default { + controller: class { + constructor ($element, $timeout) { + "ngInject"; + + this.$element = $element; + this.$timeout = $timeout; + } + + $postLink () { + this.$timeout(() => + this.$element.addClass("oui-inline-adder__row") + ); + } + } +}; diff --git a/packages/oui-modal/README.md b/packages/oui-modal/README.md index 6cc3d121..ab008655 100644 --- a/packages/oui-modal/README.md +++ b/packages/oui-modal/README.md @@ -44,11 +44,10 @@
@@ -61,17 +60,29 @@ ### Warning modal ```html:preview -
Modal content -
+``` + +### Disabled buttons + +```html:preview + + You shall not pass! + ``` ## API @@ -83,8 +94,10 @@ | `loading` | boolean | { expect($secondaryButton.attr("disabled")).toBe("disabled"); }); + it("should disable buttons when the conditions are met", () => { + const primaryDisabled = true; + const secondaryDisabled = true; + + const element = TestUtils.compileTemplate(` + + + `, { + primaryLabel, + secondaryLabel, + primaryDisabled, + secondaryDisabled + }); + + const $footer = getFooter(element); + const $primaryButton = getPrimaryButton($footer); + const $secondaryButton = getSecondaryButton($footer); + + expect($primaryButton).toBeDefined(); + expect($primaryButton.attr("disabled")).toBe("disabled"); + expect($secondaryButton).toBeDefined(); + expect($secondaryButton.attr("disabled")).toBe("disabled"); + }); + it("should trigger secondary action", () => { const secondarySpy = jasmine.createSpy("secondaryClick"); const element = TestUtils.compileTemplate(` diff --git a/packages/oui-modal/src/modal.component.js b/packages/oui-modal/src/modal.component.js index 5a656647..2576c362 100644 --- a/packages/oui-modal/src/modal.component.js +++ b/packages/oui-modal/src/modal.component.js @@ -11,8 +11,10 @@ export default { loading: " + ng-disabled="$ctrl.loading || $ctrl.secondaryDisabled">
diff --git a/packages/oui-navbar/README.md b/packages/oui-navbar/README.md index 86558664..26d6dca1 100644 --- a/packages/oui-navbar/README.md +++ b/packages/oui-navbar/README.md @@ -250,6 +250,7 @@ This property is only available for root links of `main-links`. "title": String, "url": String, "isPrimary": Boolean, + "click": Function, "subLinks": Array[{ "label": String, "title": String, @@ -348,6 +349,7 @@ It defines the menu in reponsive mode. It will be visible only for screen resolu This property is only available for root links of `aside-links`. - `iconClass`: define `class` of the menu item icon. +- `iconAnimated`: define if the menu item icon should be animated ```json [{ @@ -356,6 +358,7 @@ This property is only available for root links of `aside-links`. "label": String, "title": String, "iconClass": String, + "iconAnimated": Boolean, "subLinks": Array[{ "label": String, "title": String, @@ -378,6 +381,7 @@ This property is only available for root links of `aside-links`. acknowledged: '!true' }).length" icon-class="{{asideLink.iconClass}}" + icon-animated="asideLink.name === 'notifications'" on-click="asideLink.onClick" ng-repeat="asideLink in $ctrl.asideLinks track by $index" ng-class="asideLink.class" @@ -547,6 +551,7 @@ The property `name` **must be** `"user"`. | `text` | string | @ | yes | n/a | n/a | text of the button | `aria-label` | string | @? | yes | n/a | n/a | accessibility label of the button | `icon-class` | string | @? | yes | n/a | n/a | classname of the button icon +| `icon-animated` | boolean | -