diff --git a/packages/oui-popover/README.md b/packages/oui-popover/README.md index 7a2a65a3..a9fec30b 100644 --- a/packages/oui-popover/README.md +++ b/packages/oui-popover/README.md @@ -1,10 +1,49 @@ # Popover - + ## Usage -### Simple case +### Using value of `oui-popover` attribute + +```html:preview + +``` + +### Using value of `title` attribute + +```html:preview + +``` + +### Using a template with `oui-popover-template` attribute + +```html:preview + + + +``` + +**Note**: This method use `ngInclude` to add the template in a popover. The content of your template will be compiled with a **new** scope. See [ngInclude](https://docs.angularjs.org/api/ng/directive/ngInclude). + +### Using `oui-popover` component ```html:preview @@ -15,83 +54,83 @@ ``` +**Note**: This use is **deprecated** and will be removed in the next major version + ### All directions ```html:preview - - - This is an awesome popover content. - - - - - This is an awesome popover content. - - - - - This is an awesome popover content. - - - - - This is an awesome popover content. - + + + + + + + ``` ### Alignments ```html:preview - - - This is an awesome popover content. - - - - - This is an awesome popover content. - + + + ``` ### Help Popover ```html:preview - - - This is an awesome popover content. - + ``` -## API - -### oui-popover - -The component for a popover. - -Availability: - - - Element - -| Attribute | Type | Binding | Values | Default | Description -| ---- | ---- | ---- | ---- | ---- | ---- -| `placement` | string | @? | See [Popper placements](https://popper.js.org/popper-documentation.html#Popper.placements) | `right` | modifier for alignment +### Dynamic popover text -For placement values, see Popper.JS documentation (https://popper.js.org/popper-documentation.html#Popper.placements) - -### oui-popover-trigger +```html:preview + +``` -The directive that triggers the popover apparition. +## API -Availability: +| Attribute | Type | Binding | One-time Binding | Values | Default | Description +| ---- | ---- | ---- | ---- | ---- | ---- | ---- +| `oui-popover` | string | @ | no | n/a | `title` attribute | popover content +| `oui-popover-template` | string | @? | no | n/a | n/a | popover content template +| `oui-popover-placement` | string | @? | yes | See [Popper placements](https://popper.js.org/popper-documentation.html#Popper.placements) | `right` | modifier for alignment - - Element - - Attribute +## Deprecated -### oui-popover-content +### Attributes -The directive that wrap the popover content. +* `placement`: Replaced by `oui-popover-placement` attribute -Availability: +### Components - - Element - - Attribute +* `oui-popover`: Replaced by `oui-popover` attribute +* `oui-popover-trigger`: Replaced by `oui-popover` attribute +* `oui-popover-content`: Replaced by `oui-popover` attribute diff --git a/packages/oui-popover/src/popover-content.directive.js b/packages/oui-popover/src/content/popover-content.directive.js similarity index 69% rename from packages/oui-popover/src/popover-content.directive.js rename to packages/oui-popover/src/content/popover-content.directive.js index c1873ac6..cd3952c8 100644 --- a/packages/oui-popover/src/popover-content.directive.js +++ b/packages/oui-popover/src/content/popover-content.directive.js @@ -1,5 +1,6 @@ import contentTemplate from "./popover-content.html"; +// Deprecated: Support only for old use export default () => { "ngInject"; @@ -13,6 +14,9 @@ export default () => { bindToController: true, scope: {}, template: contentTemplate, - transclude: true + transclude: true, + link: (scope, element) => { + element.addClass("oui-popover"); + } }; }; diff --git a/packages/oui-popover/src/content/popover-content.html b/packages/oui-popover/src/content/popover-content.html new file mode 100644 index 00000000..0f8066e4 --- /dev/null +++ b/packages/oui-popover/src/content/popover-content.html @@ -0,0 +1,5 @@ + +
+ diff --git a/packages/oui-popover/src/index.js b/packages/oui-popover/src/index.js index 97a7648c..f0afe3c0 100644 --- a/packages/oui-popover/src/index.js +++ b/packages/oui-popover/src/index.js @@ -1,10 +1,10 @@ -import Popover from "./popover.component.js"; -import PopoverContent from "./popover-content.directive"; -import PopoverTrigger from "./popover-trigger.directive"; +import Popover from "./popover.directive.js"; +import PopoverContent from "./content/popover-content.directive"; +import PopoverTrigger from "./trigger/popover-trigger.directive"; export default angular .module("oui.popover", []) - .component("ouiPopover", Popover) + .directive("ouiPopover", Popover) .directive("ouiPopoverContent", PopoverContent) .directive("ouiPopoverTrigger", PopoverTrigger) .name; diff --git a/packages/oui-popover/src/index.spec.js b/packages/oui-popover/src/index.spec.js index d286cd6c..0d12f7d2 100644 --- a/packages/oui-popover/src/index.spec.js +++ b/packages/oui-popover/src/index.spec.js @@ -1,81 +1,132 @@ describe("ouiPopover", () => { - let TestUtils; + let $timeout; + let testUtils; beforeEach(angular.mock.module("oui.popover")); beforeEach(angular.mock.module("oui.test-utils")); - beforeEach(inject((_TestUtils_) => { - TestUtils = _TestUtils_; + beforeEach(inject((_$timeout_, _TestUtils_) => { + $timeout = _$timeout_; + testUtils = _TestUtils_; })); - describe("Component", () => { - it("should display the trigger with correct class name", () => { - const element = TestUtils.compileTemplate(` - - - - Popover content - - ` - ); - - const trigger = element[0].querySelector("[oui-popover-trigger]"); - expect(angular.element(trigger).hasClass("oui-popover__trigger")).toBeTruthy(); - }); + describe("Directive", () => { + describe("oui-popover", () => { + it("should create a popover, next to the trigger, with the attribute value as text", () => { + const component = testUtils.compileTemplate('
'); - it("should display at right with arrow by default", () => { - const element = TestUtils.compileTemplate(` - - -
- Popover content -
-
` - ); - - const controller = element.controller("ouiPopover"); - controller.openPopover(); - - expect(controller.popper.options.placement).toEqual("right"); - expect(element[0].querySelector("[x-arrow]")).toBeDefined(); - }); + $timeout.flush(); + + const popover = angular.element(component[0].querySelector(".trigger")).next(); + + expect(popover.length).toBe(1); + expect(popover.hasClass("oui-popover")).toBe(true); + expect(popover.text().trim()).toBe("foo"); + }); + + it("should create a popover, next to the trigger, with the attribute value as text", () => { + const component = testUtils.compileTemplate('
'); + + $timeout.flush(); + + const popover = angular.element(component[0].querySelector(".trigger")).next(); + + expect(popover.length).toBe(1); + expect(popover.hasClass("oui-popover")).toBe(true); + expect(popover.text().trim()).toBe("foo"); + }); + + it("should position the popover with right direction when trigger is clicked, if there is no placement defined", () => { + const component = testUtils.compileTemplate('
'); + + $timeout.flush(); + + const trigger = angular.element(component[0].querySelector(".trigger")).triggerHandler("click"); + const popover = trigger.next(); + + expect(popover.attr("x-placement")).toBe("right"); + }); + + + it("should position the popover with placement attribute value, when trigger is clicked", () => { + const component = testUtils.compileTemplate('
'); + + $timeout.flush(); + + const trigger = angular.element(component[0].querySelector(".trigger")).triggerHandler("click"); + const popover = trigger.next(); + + expect(popover.attr("x-placement")).toBe("bottom-start"); + }); + + it("should create a popover, next to the trigger, with the content of the template", () => { + const component = testUtils.compileTemplate(`
+ + +
`); + + $timeout.flush(); + + const popover = angular.element(component[0].querySelector(".trigger")).next(); + + expect(popover.text().trim()).toBe("foo"); + }); + + it("should set aria-expanded when trigger is clicked", () => { + const component = testUtils.compileTemplate('
'); - it("should display the popover at bottom aligned the left border", () => { - const element = TestUtils.compileTemplate(` - - -
- Popover content -
-
` - ); + $timeout.flush(); - const controller = element.controller("ouiPopover"); - controller.openPopover(); + const trigger = angular.element(component[0].querySelector(".trigger")); + expect(trigger.attr("aria-expanded")).toBe("false"); - expect(controller.popper.options.placement).toEqual("bottom-start"); + trigger.triggerHandler("click"); + expect(trigger.attr("aria-expanded")).toBe("true"); + + trigger.triggerHandler("click"); + expect(trigger.attr("aria-expanded")).toBe("false"); + }); }); - describe("Events", () => { - it("should not be visible", () => { - const element = TestUtils.compileTemplate(` + describe("Deprecated support", () => { + it("should display the trigger with correct class name", () => { + const element = testUtils.compileTemplate(` - + + + Popover content + + ` + ); + + $timeout.flush(); + + const trigger = element[0].querySelector("[oui-popover-trigger]"); + expect(angular.element(trigger).hasClass("oui-popover__trigger")).toBeTruthy(); + }); + + it("should display at right with arrow by default", () => { + const element = testUtils.compileTemplate(` + +
Popover content
` ); - const popover = element[0].querySelector("[oui-popover-content]").parentNode; - const $popover = angular.element(popover); + $timeout.flush(); + + const controller = element.controller("ouiPopover"); + controller.openPopover(); - expect($popover.hasClass("oui-popover_active")).toBeFalsy(); + expect(controller.popper.options.placement).toEqual("right"); + expect(element[0].querySelector("[x-arrow]")).toBeDefined(); }); - it("should display and hide popover on click", () => { - const element = TestUtils.compileTemplate(` - + it("should display the popover at bottom aligned the left border", () => { + const element = testUtils.compileTemplate(` +
Popover content @@ -83,22 +134,34 @@ describe("ouiPopover", () => { ` ); - const rootElement = element[0].querySelector(".oui-popover"); - const $rootElement = angular.element(rootElement); - const trigger = element[0].querySelector("[oui-popover-trigger]"); - const $trigger = angular.element(trigger); - const closeButton = element[0].querySelector(".oui-popover__close-button"); - const $closeButton = angular.element(closeButton); - - expect($rootElement.hasClass("oui-popover_active")).toBeFalsy(); - $trigger.triggerHandler("click"); - expect($rootElement.hasClass("oui-popover_active")).toBeTruthy(); - $trigger.triggerHandler("click"); - expect($rootElement.hasClass("oui-popover_active")).toBeFalsy(); - $trigger.triggerHandler("click"); - expect($rootElement.hasClass("oui-popover_active")).toBeTruthy(); - $closeButton.triggerHandler("click"); - expect($rootElement.hasClass("oui-popover_active")).toBeFalsy(); + $timeout.flush(); + + const controller = element.controller("ouiPopover"); + controller.openPopover(); + + expect(controller.popper.options.placement).toEqual("bottom-start"); + }); + + it("should set aria-expanded when trigger is clicked", () => { + const element = testUtils.compileTemplate(` + + + + Popover content + + ` + ); + + $timeout.flush(); + + const trigger = angular.element(element[0].querySelector("[oui-popover-trigger]")); + expect(trigger.attr("aria-expanded")).toBe("false"); + + trigger.triggerHandler("click"); + expect(trigger.attr("aria-expanded")).toBe("true"); + + trigger.triggerHandler("click"); + expect(trigger.attr("aria-expanded")).toBe("false"); }); }); }); diff --git a/packages/oui-popover/src/popover-content.html b/packages/oui-popover/src/popover-content.html deleted file mode 100644 index 6e784b4e..00000000 --- a/packages/oui-popover/src/popover-content.html +++ /dev/null @@ -1,7 +0,0 @@ -
- -
- -
diff --git a/packages/oui-popover/src/popover-trigger.directive.js b/packages/oui-popover/src/popover-trigger.directive.js deleted file mode 100644 index 485713fd..00000000 --- a/packages/oui-popover/src/popover-trigger.directive.js +++ /dev/null @@ -1,41 +0,0 @@ -const popoverTriggerClass = "oui-popover__trigger"; - -export default () => { - "ngInject"; - - return { - restrict: "AE", - require: "^ouiPopover", - scope: {}, - link: (scope, element, attrs, ctrl) => { - const triggerElement = element; - - triggerElement.addClass(popoverTriggerClass); - - triggerElement.attr("id", ctrl.id); - triggerElement.attr({ "aria-haspopup": true, "aria-expanded": false }); - - triggerElement.on("click", () => ctrl.onTriggerClick()); - - scope.$on("oui:popover:afterOpen", (e, id) => { - if (id !== ctrl.id) { - return; - } - - triggerElement.attr("aria-expanded", true); - }); - - scope.$on("oui:popover:afterClose", (e, id) => { - if (id !== ctrl.id) { - return; - } - - triggerElement.attr("aria-expanded", false); - }); - - scope.$on("$destroy", () => { - triggerElement.off("click"); - }); - } - }; -}; diff --git a/packages/oui-popover/src/popover.component.js b/packages/oui-popover/src/popover.component.js deleted file mode 100644 index 6531221c..00000000 --- a/packages/oui-popover/src/popover.component.js +++ /dev/null @@ -1,11 +0,0 @@ -import controller from "./popover.controller"; -import template from "./popover.html"; - -export default { - template, - controller, - bindings: { - placement: "@?" - }, - transclude: true -}; diff --git a/packages/oui-popover/src/popover.controller.js b/packages/oui-popover/src/popover.controller.js index b22df046..c7624775 100644 --- a/packages/oui-popover/src/popover.controller.js +++ b/packages/oui-popover/src/popover.controller.js @@ -1,39 +1,91 @@ +import { addDefaultParameter } from "@ovh-ui/common/component-utils"; import Popper from "popper.js"; +import template from "./popover.html"; const KEY_ESCAPE = 27; export default class PopoverController { - constructor ($scope, $element, $attrs, $document, $timeout) { + constructor ($attrs, $compile, $document, $element, $scope, $timeout) { "ngInject"; - this.$scope = $scope; - this.$element = $element; this.$attrs = $attrs; + this.$compile = $compile; this.$document = $document; + this.$element = $element; + this.$scope = $scope; this.$timeout = $timeout; } $onInit () { - this.isPopoverOpen = false; + // Deprecated: Support for component `oui-popover` + // Check if directive is an attribute or a component + this.isComponent = angular.isUndefined(this.$attrs.ouiPopover); + + // Deprecated: Support for `placement` attribute + this.placement = this.placement || this.$attrs.placement; - // Use internal id to map trigger this.id = `ouiPopover${this.$scope.$id}`; + this.isPopoverOpen = false; - if (angular.isUndefined(this.placement)) { - this.placement = "right"; - } + addDefaultParameter(this, "placement", "right"); } $postLink () { - this.triggerElement = this.$element[0].querySelector(".oui-popover__trigger"); - this.popperElement = this.$element[0].querySelector(".oui-popover__content"); - this.arrowElement = this.$element[0].querySelector(".oui-popover__arrow"); + this.setPopover(); + this.setTrigger(); } $destroy () { this.closePopover(); } + setPopover () { + this.$timeout(() => { + // Deprecated: Support for component `oui-popover-content` + if (this.isComponent) { + this.popperElement = this.$element[0].querySelector(".oui-popover"); + this.arrowElement = this.$element[0].querySelector(".oui-popover__arrow"); + return; + } + + // Support for attribute `oui-popover` + // Create a new scope to compile the popover next to the trigger + const popoverScope = angular.extend(this.$scope.$new(true), { $popoverCtrl: this }); + const popoverTemplate = this.$compile(template)(popoverScope); + + // Add compiled template after $element + this.$element + .removeAttr("title") // Remove title to avoid native tooltip + .after(popoverTemplate); + + this.popperElement = this.$element.next()[0]; + this.arrowElement = this.popperElement.querySelector(".oui-popover__arrow"); + }); + } + + setTrigger () { + this.$timeout(() => { + // Deprecated: Support for component `oui-popover-trigger` + if (this.isComponent) { + this.triggerElement = this.$element[0].querySelector(".oui-popover__trigger"); + this.$triggerElement = angular.element(this.triggerElement); + return; + } + + // Support for attribute `oui-popover` + this.triggerElement = this.$element[0]; + this.$triggerElement = angular.element(this.triggerElement); + + this.$triggerElement + .addClass("oui-popover__trigger") + .attr({ + "aria-haspopup": true, + "aria-expanded": false + }) + .on("click", () => this.onTriggerClick()); + }); + } + onTriggerClick () { if (!this.isPopoverOpen) { this.openPopover(); @@ -52,20 +104,33 @@ export default class PopoverController { openPopover () { this.isPopoverOpen = true; - angular.element(this.$element.children()[0]).addClass("oui-popover_active"); this.updatePopper(); this.$document.on("keydown", evt => this.triggerKeyHandler(evt)); - this.$scope.$broadcast("oui:popover:afterOpen", this.id); + + // Deprecated: Support for component `oui-popover-trigger` + if (this.isComponent) { + this.$triggerElement.attr("aria-expanded", true); + return; + } + + // Support for attribute `oui-popover` + this.$element.attr("aria-expanded", true); } closePopover () { this.isPopoverOpen = false; - angular.element(this.$element.children()[0]).removeClass("oui-popover_active"); - this.destroyPopper(); this.$document.off("keydown", evt => this.triggerKeyHandler(evt)); - this.$scope.$broadcast("oui:popover:afterClose", this.id); + + // Deprecated: Support for component `oui-popover-trigger` + if (this.isComponent) { + this.$triggerElement.attr("aria-expanded", false); + return; + } + + // Support for attribute `oui-popover` + this.$element.attr("aria-expanded", false); } createPopper () { diff --git a/packages/oui-popover/src/popover.directive.js b/packages/oui-popover/src/popover.directive.js new file mode 100644 index 00000000..b388e7d5 --- /dev/null +++ b/packages/oui-popover/src/popover.directive.js @@ -0,0 +1,17 @@ +import controller from "./popover.controller"; + +export default () => { + "ngInject"; + + return { + restrict: "AE", + bindToController: { + text: "@ouiPopover", + title: "@?", + placement: "@?ouiPopoverPlacement", + template: "@?ouiPopoverTemplate" + }, + controller, + controllerAs: "$popoverCtrl" + }; +}; diff --git a/packages/oui-popover/src/popover.html b/packages/oui-popover/src/popover.html index ecfd3b09..460f5661 100644 --- a/packages/oui-popover/src/popover.html +++ b/packages/oui-popover/src/popover.html @@ -1,3 +1,18 @@ -
+
+ +
+
+
+
+
diff --git a/packages/oui-popover/src/trigger/popover-trigger.controller.js b/packages/oui-popover/src/trigger/popover-trigger.controller.js new file mode 100644 index 00000000..9f664c2d --- /dev/null +++ b/packages/oui-popover/src/trigger/popover-trigger.controller.js @@ -0,0 +1,26 @@ +// Deprecated: Support only for old use +export default class { + constructor ($element, $scope, $timeout) { + "ngInject"; + + this.$element = $element; + this.$scope = $scope; + this.$timeout = $timeout; + } + + $postLink () { + this.$timeout(() => + this.$element + .addClass("oui-popover__trigger") + .attr({ + "aria-haspopup": true, + "aria-expanded": false + }) + .on("click", () => this.popover.onTriggerClick()) + ); + } + + $onDestroy () { + this.$element.off("click"); + } +} diff --git a/packages/oui-popover/src/trigger/popover-trigger.directive.js b/packages/oui-popover/src/trigger/popover-trigger.directive.js new file mode 100644 index 00000000..78be2ab2 --- /dev/null +++ b/packages/oui-popover/src/trigger/popover-trigger.directive.js @@ -0,0 +1,16 @@ +import controller from "./popover-trigger.controller"; + +// Deprecated: Support only for old use +export default () => { + "ngInject"; + + return { + restrict: "AE", + require: { + popover: "^ouiPopover" + }, + controller, + bindToController: true, + scope: {} + }; +}; diff --git a/packages/oui-tooltip/src/tooltip.controller.js b/packages/oui-tooltip/src/tooltip.controller.js index 8f308029..93d9b64a 100644 --- a/packages/oui-tooltip/src/tooltip.controller.js +++ b/packages/oui-tooltip/src/tooltip.controller.js @@ -17,8 +17,11 @@ export default class { addDefaultParameter(this, "placement", "top"); } - $postLink () { + $onDestroy () { + this.destroyPopper(); + } + $postLink () { this.$timeout(() => { if (this.title) { addDefaultParameter(this, "text", this.title); @@ -49,4 +52,13 @@ export default class { placement: this.placement }); } + + destroyPopper () { + if (!this.popper) { + return; + } + + this.popper.destroy(); + this.popper = null; + } }