From 96eb83c0a76024b87e8129b1641b1fa000af6a45 Mon Sep 17 00:00:00 2001 From: Mathieu Duckerts-Antoine Date: Tue, 8 Feb 2022 11:16:18 +0000 Subject: [PATCH] [REF] web: tooltip service replaces useTooltip hook In preparation for OWL-2, we introduce a new tooltip service that replaces the hook useTooltip used by the webclient. The main reason for that is that the implementation of useTooltip had to access el on the components calling it. Since this won't be possible anymore in OWL-2 (el is ill-defined in presence of multi root components) we had to find something else. Another reason is that the useTooltip hook had no reason at all to be a hook (as attested by the fact that only the webclient needs to call it to have tooltip on every dom elements). closes odoo/odoo#84160 Related: odoo/enterprise#24152 Signed-off-by: Aaron Bohy (aab) --- .../static/src/core/tooltip/tooltip_hook.js | 120 ------- .../src/core/tooltip/tooltip_service.js | 125 +++++++ addons/web/static/src/webclient/webclient.js | 2 - .../tests/core/tooltip/tooltip_hook_tests.js | 287 ---------------- .../core/tooltip/tooltip_service_tests.js | 318 ++++++++++++++++++ 5 files changed, 443 insertions(+), 409 deletions(-) delete mode 100644 addons/web/static/src/core/tooltip/tooltip_hook.js create mode 100644 addons/web/static/src/core/tooltip/tooltip_service.js delete mode 100644 addons/web/static/tests/core/tooltip/tooltip_hook_tests.js create mode 100644 addons/web/static/tests/core/tooltip/tooltip_service_tests.js diff --git a/addons/web/static/src/core/tooltip/tooltip_hook.js b/addons/web/static/src/core/tooltip/tooltip_hook.js deleted file mode 100644 index 24d9d05de06..00000000000 --- a/addons/web/static/src/core/tooltip/tooltip_hook.js +++ /dev/null @@ -1,120 +0,0 @@ -/** @odoo-module **/ - -import { browser } from "@web/core/browser/browser"; -import { useEffect, useListener } from "@web/core/utils/hooks"; -import { usePopover } from "@web/core/popover/popover_hook"; -import { Tooltip } from "./tooltip"; - -/** - * The useTooltip hook allows to display custom tooltips on every elements with - * a "data-tooltip" attribute inside the Component using the hook. This attribute - * can be set on elements for which we prefer a custom tooltip instead of the - * native one displaying the value of the "title" attribute. - * - * Usage (with a parent somewhere using the useTooltip hook): - * - * - * The ideal position of the tooltip can be specified thanks to the attribute - * "data-tooltip-position": - * - * - * For advanced tooltips containing dynamic and/or html content, the - * "data-tooltip-template" and "data-tooltip-info" attributes can be used. - * For example, let's suppose the following qweb template: - * - *
    - *
  • info.x
  • - *
  • info.y
  • - *
- *
- * This template can then be used in a tooltip as follows: - * - * with "info" being a stringified object with two keys "x" and "y". - */ -const OPEN_DELAY = 400; -const CLOSE_DELAY = 200; -export function useTooltip() { - const popover = usePopover(); - let openTooltipTimeout; - let closeTooltip; - let target = null; - - /** - * Closes the currently opened tooltip if any, or prevent it from opening. - */ - function cleanup() { - browser.clearTimeout(openTooltipTimeout); - if (closeTooltip) { - closeTooltip(); - } - } - - /** - * Opens the tooltip after a delay, if the event's target has a data-tooltip - * attribute. - * - * @param {MouseEvent} ev a "mouseenter" event - */ - function onMouseEnter(ev) { - const dataset = ev.target.dataset; - const tooltip = dataset.tooltip; - let template, info; - if ("tooltipTemplate" in dataset) { - template = dataset.tooltipTemplate; - info = dataset.tooltipInfo ? JSON.parse(dataset.tooltipInfo) : null; - } - if (tooltip || template) { - cleanup(); - target = ev.target; - openTooltipTimeout = browser.setTimeout(() => { - const position = dataset.tooltipPosition; - closeTooltip = popover.add( - target, - Tooltip, - { tooltip, template, info }, - { position } - ); - }, OPEN_DELAY); - } - } - - // Track mouse position to be able to detect that we are no longer hovering - // the target, thus that we should close the tooltip - let positionX; - let positionY; - useListener("mousemove", (ev) => { - positionX = ev.x; - positionY = ev.y; - }); - - // Regularly check that the target is still in the DOM and we're still - // hovering it, because if not, we have to close the tooltip - useEffect( - () => { - const interval = browser.setInterval(() => { - if (!target) { - return; - } - if (!document.body.contains(target)) { - return cleanup(); // target is no longer in the DOM - } - const targetRect = target.getBoundingClientRect(); - if ( - positionX < targetRect.left || - positionX > targetRect.right || - positionY < targetRect.top || - positionY > targetRect.bottom - ) { - return cleanup(); // mouse is no longer hovering the target - } - }, CLOSE_DELAY); - return () => browser.clearInterval(interval); - }, - () => [] - ); - - // Listen (using event delegation) to "mouseenter" events on all nodes with - // the "data-tooltip" or "data-tooltip-template" attribute, to open the tooltip. - const selector = "[data-tooltip], [data-tooltip-template]"; - useListener("mouseenter", selector, onMouseEnter, { capture: true }); -} diff --git a/addons/web/static/src/core/tooltip/tooltip_service.js b/addons/web/static/src/core/tooltip/tooltip_service.js new file mode 100644 index 00000000000..bc591385fc8 --- /dev/null +++ b/addons/web/static/src/core/tooltip/tooltip_service.js @@ -0,0 +1,125 @@ +/** @odoo-module **/ + +import { browser } from "@web/core/browser/browser"; +import { registry } from "@web/core/registry"; +import { Tooltip } from "./tooltip"; + +const { whenReady } = owl; + +/** + * The tooltip service allows to display custom tooltips on every elements with + * a "data-tooltip" attribute. This attribute can be set on elements for which + * we prefer a custom tooltip instead of the native one displaying the value of + * the "title" attribute. + * + * Usage: + * + * + * The ideal position of the tooltip can be specified thanks to the attribute + * "data-tooltip-position": + * + * + * For advanced tooltips containing dynamic and/or html content, the + * "data-tooltip-template" and "data-tooltip-info" attributes can be used. + * For example, let's suppose the following qweb template: + * + *
    + *
  • info.x
  • + *
  • info.y
  • + *
+ *
+ * This template can then be used in a tooltip as follows: + * + * with "info" being a stringified object with two keys "x" and "y". + */ + +const OPEN_DELAY = 400; +const CLOSE_DELAY = 200; + +export const tooltipService = { + dependencies: ["popover"], + start(env, { popover }) { + let openTooltipTimeout; + let closeTooltip; + let target = null; + + let positionX; + let positionY; + + /** + * Closes the currently opened tooltip if any, or prevent it from opening. + */ + function cleanup() { + browser.clearTimeout(openTooltipTimeout); + if (closeTooltip) { + closeTooltip(); + } + } + + /** + * Opens the tooltip after a delay, if the event's target has a data-tooltip + * attribute. + * + * @param {MouseEvent} ev a "mouseenter" event + */ + function onMouseenter(ev) { + let el = ev.target; + if (!el.matches("[data-tooltip], [data-tooltip-template]")) { + return; + } + target = el; + const dataset = el.dataset; + const tooltip = dataset.tooltip; + let template, info; + if ("tooltipTemplate" in dataset) { + template = dataset.tooltipTemplate; + info = dataset.tooltipInfo ? JSON.parse(dataset.tooltipInfo) : null; + } + cleanup(); + openTooltipTimeout = browser.setTimeout(() => { + const position = dataset.tooltipPosition; + closeTooltip = popover.add( + target, + Tooltip, + { tooltip, template, info }, + { position } + ); + }, OPEN_DELAY); + } + + whenReady(() => { + // Regularly check that the target is still in the DOM and we're still + // hovering it, because if not, we have to close the tooltipd + browser.setInterval(() => { + if (!target) { + return; + } + if (!document.body.contains(target)) { + return cleanup(); // target is no longer in the DOM + } + const targetRect = target.getBoundingClientRect(); + if ( + positionX < targetRect.left || + positionX > targetRect.right || + positionY < targetRect.top || + positionY > targetRect.bottom + ) { + return cleanup(); // mouse is no longer hovering the target + } + }, CLOSE_DELAY); + + // Track mouse position to be able to detect that we are no longer hovering + // the target, thus that we should close the tooltip + document.body.addEventListener("mousemove", (ev) => { + positionX = ev.x; + positionY = ev.y; + }); + + // Listen (using event delegation) to "mouseenter" events on all nodes with + // the "data-tooltip" or "data-tooltip-template" attribute, to open the tooltip. + document.body.addEventListener("mouseenter", onMouseenter, { capture: true }); + }); + }, +}; + +registry.category("services").add("tooltip", tooltipService); diff --git a/addons/web/static/src/webclient/webclient.js b/addons/web/static/src/webclient/webclient.js index 8efe92e4a6e..cf0260f1dd7 100644 --- a/addons/web/static/src/webclient/webclient.js +++ b/addons/web/static/src/webclient/webclient.js @@ -3,7 +3,6 @@ import { ActionContainer } from "./actions/action_container"; import { NavBar } from "./navbar/navbar"; import { useBus, useEffect, useService } from "@web/core/utils/hooks"; -import { useTooltip } from "@web/core/tooltip/tooltip_hook"; import { NotUpdatable } from "../core/utils/components"; import { MainComponentsContainer } from "../core/main_components_container"; import { useOwnDebugContext } from "../core/debug/debug_context"; @@ -46,7 +45,6 @@ export class WebClient extends Component { () => [] ); useExternalListener(window, "click", this.onGlobalClick, { capture: true }); - useTooltip(); } mounted() { diff --git a/addons/web/static/tests/core/tooltip/tooltip_hook_tests.js b/addons/web/static/tests/core/tooltip/tooltip_hook_tests.js deleted file mode 100644 index feb25e7217d..00000000000 --- a/addons/web/static/tests/core/tooltip/tooltip_hook_tests.js +++ /dev/null @@ -1,287 +0,0 @@ -/** @odoo-module **/ - -import { browser } from "@web/core/browser/browser"; -import { PopoverContainer } from "@web/core/popover/popover_container"; -import { popoverService } from "@web/core/popover/popover_service"; -import { useTooltip } from "@web/core/tooltip/tooltip_hook"; -import { registry } from "@web/core/registry"; -import { registerCleanup } from "../../helpers/cleanup"; -import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env"; -import { getFixture, nextTick, patchWithCleanup, triggerEvent } from "../../helpers/utils"; - -const { Component, mount, useState, xml } = owl; - -const mainComponents = registry.category("main_components"); - -/** - * Creates and mounts a parent component that use the "useTooltip" hook. - * - * @param {Component} Child a child Component that contains nodes with "data-tooltip" attribute - * @param {Object} [options] - * @param {function} options.mockSetTimeout the mocked setTimeout to use (by default, calls the - * callback directly) - * @param {function} options.mockSetInterval the mocked setInterval to use (by default, calls the - * callback directly) - * @param {function} options.mockClearTimeout the mocked clearTimeout to use (by default, does nothing) - * @param {function} options.mockClearInterval the mocked clearInterval to use (by default, does nothing) - * @returns {Promise} - */ -async function makeParent(Child, options = {}) { - const fixture = getFixture(); - - // add the popover service to the registry -> will add the PopoverContainer - // to the mainComponentRegistry - clearRegistryWithCleanup(mainComponents); - registry.category("services").add("popover", popoverService); - const env = await makeTestEnv(); - - class Parent extends Component { - setup() { - this.Components = mainComponents.getEntries(); - useTooltip(); - } - } - Parent.template = xml` -
- -
- - - -
-
`; - Parent.components = { PopoverContainer, Child }; - - patchWithCleanup(browser, { - setTimeout: options.mockSetTimeout || ((fn) => fn()), - clearTimeout: options.mockClearTimeout || (() => {}), - setInterval: options.mockSetInterval || ((fn) => fn()), - clearInterval: options.mockClearInterval || (() => {}), - }); - - const comp = await mount(Parent, { env, target: fixture }); - registerCleanup(() => comp.destroy()); - return comp; -} - -QUnit.module("Tooltip hook", () => { - QUnit.test("basic rendering", async (assert) => { - class MyComponent extends Component {} - MyComponent.template = xml``; - let simulateTimeout; - const mockSetTimeout = (fn) => { - simulateTimeout = fn; - }; - let simulateInterval; - const mockSetInterval = (fn) => { - simulateInterval = fn; - }; - const parent = await makeParent(MyComponent, { mockSetTimeout, mockSetInterval }); - - assert.containsNone(parent, ".o_popover_container .o_popover"); - parent.el.querySelector("button").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsNone(parent, ".o_popover_container .o_popover"); - - simulateTimeout(); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - assert.strictEqual( - parent.el.querySelector(".o_popover_container .o_popover").innerText, - "hello" - ); - - const buttonRect = parent.el.querySelector("button").getBoundingClientRect(); - const x = buttonRect.right + 10; - const y = buttonRect.bottom + 10; - await triggerEvent(parent.el, "button", "mousemove", { - pageX: x, - layerX: x, - screenX: x, - pageY: y, - layerY: y, - screenY: y, - }); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - simulateInterval(); - await nextTick(); - assert.containsNone(parent, ".o_popover_container .o_popover"); - }); - - QUnit.test("remove element with opened tooltip", async (assert) => { - let compState; - class MyComponent extends Component { - setup() { - this.state = useState({ visible: true }); - compState = this.state; - } - } - MyComponent.template = xml` -
- -
`; - let simulateInterval; - const mockSetInterval = (fn) => { - simulateInterval = fn; - }; - const parent = await makeParent(MyComponent, { mockSetInterval }); - - assert.containsOnce(parent, "button"); - assert.containsNone(parent, ".o_popover_container .o_popover"); - parent.el.querySelector("button").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - - compState.visible = false; - await nextTick(); - assert.containsNone(parent, "button"); - simulateInterval(); - await nextTick(); - assert.containsNone(parent, ".o_popover_container .o_popover"); - }); - - QUnit.test("rendering with several tooltips", async (assert) => { - class MyComponent extends Component {} - MyComponent.template = xml` -
- - -
`; - const parent = await makeParent(MyComponent); - - assert.containsNone(parent, ".o_popover_container .o_popover"); - parent.el.querySelector("button.button_1").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "tooltip 1"); - parent.el.querySelector("button.button_1").dispatchEvent(new Event("mouseleave")); - parent.el.querySelector("button.button_2").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "tooltip 2"); - }); - - QUnit.test("positionning", async (assert) => { - class MyComponent extends Component {} - MyComponent.template = xml` -
- - - - - -
`; - const parent = await makeParent(MyComponent); - - // default - parent.el.querySelector("button.default").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "default"); - assert.hasClass(parent.el.querySelector(".o_popover"), "o-popper-position--bm"); - - // top - parent.el.querySelector("button.top").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "top"); - assert.hasClass(parent.el.querySelector(".o_popover"), "o-popper-position--tm"); - - // right - parent.el.querySelector("button.right").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "right"); - assert.hasClass(parent.el.querySelector(".o_popover"), "o-popper-position--rm"); - - // bottom - parent.el.querySelector("button.bottom").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "bottom"); - assert.hasClass(parent.el.querySelector(".o_popover"), "o-popper-position--bm"); - - // left - parent.el.querySelector("button.left").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o_popover"); - assert.strictEqual(parent.el.querySelector(".o_popover").innerText, "left"); - assert.hasClass(parent.el.querySelector(".o_popover"), "o-popper-position--lm"); - }); - - QUnit.test("tooltip with a template, no info", async (assert) => { - class MyComponent extends Component {} - MyComponent.template = xml` - - `; - const parent = await makeParent(MyComponent); - parent.env.qweb.addTemplate("my_tooltip_template", "tooltip"); - - assert.containsNone(parent, ".o_popover_container .o-tooltip"); - parent.el.querySelector("button").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o-tooltip"); - assert.strictEqual(parent.el.querySelector(".o-tooltip").innerHTML, "tooltip"); - }); - - QUnit.test("tooltip with a template and info", async (assert) => { - class MyComponent extends Component { - get info() { - return JSON.stringify({ x: 3, y: "abc" }); - } - } - MyComponent.template = xml` - - `; - const parent = await makeParent(MyComponent); - parent.env.qweb.addTemplate( - "my_tooltip_template", - ` -
    -
  • X:
  • -
  • Y:
  • -
- ` - ); - - assert.containsNone(parent, ".o_popover_container .o-tooltip"); - parent.el.querySelector("button").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o-tooltip"); - assert.strictEqual( - parent.el.querySelector(".o-tooltip").innerHTML, - "
  • X: 3
  • Y: abc
" - ); - }); - - QUnit.test("destroy a component that called useTooltip", async (assert) => { - class MyComponent extends Component {} - MyComponent.template = xml` - - `; - const parent = await makeParent(MyComponent, { - mockSetInterval() { - assert.step("setInterval"); - }, - mockClearInterval() { - assert.step("clearInterval"); - }, - }); - - assert.verifySteps(["setInterval"]); - assert.containsNone(parent, ".o_popover_container .o-tooltip"); - - parent.el.querySelector("button").dispatchEvent(new Event("mouseenter")); - await nextTick(); - assert.containsOnce(parent, ".o_popover_container .o-tooltip"); - assert.strictEqual(parent.el.querySelector(".o-tooltip").innerText, "tooltip"); - - parent.destroy(); - - assert.verifySteps(["clearInterval"]); - }); -}); diff --git a/addons/web/static/tests/core/tooltip/tooltip_service_tests.js b/addons/web/static/tests/core/tooltip/tooltip_service_tests.js new file mode 100644 index 00000000000..d7756b910c1 --- /dev/null +++ b/addons/web/static/tests/core/tooltip/tooltip_service_tests.js @@ -0,0 +1,318 @@ +/** @odoo-module **/ + +import { browser } from "@web/core/browser/browser"; +import { PopoverContainer } from "@web/core/popover/popover_container"; +import { popoverService } from "@web/core/popover/popover_service"; +import { tooltipService } from "@web/core/tooltip/tooltip_service"; +import { registry } from "@web/core/registry"; +import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env"; +import { getFixture, nextTick, patchWithCleanup, triggerEvent } from "../../helpers/utils"; +import { registerCleanup } from "../../helpers/cleanup"; + +const { Component, mount, useState, xml } = owl; + +const mainComponents = registry.category("main_components"); + +/** + * Creates and mounts a parent component that use the "useTooltip" hook. + * + * @param {Component} Child a child Component that contains nodes with "data-tooltip" attribute + * @param {Object} [options] + * @param {function} [options.mockSetTimeout] the mocked setTimeout to use (by default, calls the + * callback directly) + * @param {function} [options.mockSetInterval] the mocked setInterval to use (by default, calls the + * callback directly) + * @param {function} [options.mockClearTimeout] the mocked clearTimeout to use (by default, does nothing) + * @param {function} [options.mockClearInterval] the mocked clearInterval to use (by default, does nothing) + * @param {{[templateName:string]: string}} [options.templates] additional templates + * @returns {Promise} + */ +async function makeParent(Child, options = {}) { + const target = getFixture(); + + // add the popover service to the registry -> will add the PopoverContainer + // to the mainComponentRegistry + clearRegistryWithCleanup(mainComponents); + + patchWithCleanup(browser, { + setTimeout: options.mockSetTimeout || ((fn) => fn()), + clearTimeout: options.mockClearTimeout || (() => {}), + setInterval: options.mockSetInterval || ((fn) => fn()), + clearInterval: options.mockClearInterval || (() => {}), + }); + + registry.category("services").add("popover", popoverService); + registry.category("services").add("tooltip", tooltipService); + const env = await makeTestEnv(); + + class Parent extends Component { + setup() { + this.Components = mainComponents.getEntries(); + } + } + Parent.template = xml` +
+ +
+ + + +
+
`; + Parent.components = { PopoverContainer, Child }; + + const parent = await mount(Parent, { env, target }); + registerCleanup(() => parent.destroy()); + return parent; +} + +let target; +QUnit.module( + "Tooltip service", + { + beforeEach() { + target = getFixture(); + }, + }, + () => { + QUnit.test("basic rendering", async (assert) => { + class MyComponent extends Component {} + MyComponent.template = xml``; + let simulateTimeout; + const mockSetTimeout = (fn) => { + simulateTimeout = fn; + }; + let simulateInterval; + const mockSetInterval = (fn) => { + simulateInterval = fn; + }; + const parent = await makeParent(MyComponent, { mockSetTimeout, mockSetInterval }); + + assert.containsNone(parent, ".o_popover_container .o_popover"); + target.querySelector("button").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsNone(parent, ".o_popover_container .o_popover"); + + simulateTimeout(); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + assert.strictEqual( + target.querySelector(".o_popover_container .o_popover").innerText, + "hello" + ); + + const buttonRect = target.querySelector("button").getBoundingClientRect(); + const x = buttonRect.right + 10; + const y = buttonRect.bottom + 10; + await triggerEvent(target, "button", "mousemove", { + pageX: x, + layerX: x, + screenX: x, + pageY: y, + layerY: y, + screenY: y, + }); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + simulateInterval(); + await nextTick(); + assert.containsNone(parent, ".o_popover_container .o_popover"); + }); + + QUnit.test("basic rendering 2", async (assert) => { + class MyComponent extends Component {} + MyComponent.template = xml`Action`; + let simulateTimeout; + const mockSetTimeout = (fn) => { + simulateTimeout = fn; + }; + let simulateInterval; + const mockSetInterval = (fn) => { + simulateInterval = fn; + }; + const parent = await makeParent(MyComponent, { mockSetTimeout, mockSetInterval }); + + assert.containsNone(parent, ".o_popover_container .o_popover"); + let [outerSpan, innerSpan] = target.querySelectorAll("span.our_span"); + outerSpan.dispatchEvent(new Event("mouseenter")); + innerSpan.dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsNone(parent, ".o_popover_container .o_popover"); + + simulateTimeout(); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + assert.strictEqual( + target.querySelector(".o_popover_container .o_popover").innerText, + "hello" + ); + + const outerSpanRect = outerSpan.getBoundingClientRect(); + const x = outerSpanRect.right + 10; + const y = outerSpanRect.bottom + 10; + await triggerEvent(target, 'span[data-tooltip="hello"]', "mousemove", { + pageX: x, + layerX: x, + screenX: x, + pageY: y, + layerY: y, + screenY: y, + }); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + simulateInterval(); + await nextTick(); + assert.containsNone(parent, ".o_popover_container .o_popover"); + }); + + QUnit.test("remove element with opened tooltip", async (assert) => { + let compState; + class MyComponent extends Component { + setup() { + this.state = useState({ visible: true }); + compState = this.state; + } + } + MyComponent.template = xml` +
+ +
`; + let simulateInterval; + const mockSetInterval = (fn) => { + simulateInterval = fn; + }; + const parent = await makeParent(MyComponent, { mockSetInterval }); + + assert.containsOnce(parent, "button"); + assert.containsNone(parent, ".o_popover_container .o_popover"); + target.querySelector("button").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + + compState.visible = false; + await nextTick(); + assert.containsNone(parent, "button"); + simulateInterval(); + await nextTick(); + assert.containsNone(parent, ".o_popover_container .o_popover"); + }); + + QUnit.test("rendering with several tooltips", async (assert) => { + class MyComponent extends Component {} + MyComponent.template = xml` +
+ + +
`; + const parent = await makeParent(MyComponent); + + assert.containsNone(parent, ".o_popover_container .o_popover"); + target.querySelector("button.button_1").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + assert.strictEqual(target.querySelector(".o_popover").innerText, "tooltip 1"); + target.querySelector("button.button_1").dispatchEvent(new Event("mouseleave")); + target.querySelector("button.button_2").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + assert.strictEqual(target.querySelector(".o_popover").innerText, "tooltip 2"); + }); + + QUnit.test("positionning", async (assert) => { + class MyComponent extends Component {} + MyComponent.template = xml` +
+ + + + + +
`; + const parent = await makeParent(MyComponent); + + // default + target.querySelector("button.default").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + assert.strictEqual(target.querySelector(".o_popover").innerText, "default"); + assert.hasClass(target.querySelector(".o_popover"), "o-popper-position--bm"); + + // top + target.querySelector("button.top").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + assert.strictEqual(target.querySelector(".o_popover").innerText, "top"); + assert.hasClass(target.querySelector(".o_popover"), "o-popper-position--tm"); + + // right + target.querySelector("button.right").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + assert.strictEqual(target.querySelector(".o_popover").innerText, "right"); + assert.hasClass(target.querySelector(".o_popover"), "o-popper-position--rm"); + + // bottom + target.querySelector("button.bottom").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + assert.strictEqual(target.querySelector(".o_popover").innerText, "bottom"); + assert.hasClass(target.querySelector(".o_popover"), "o-popper-position--bm"); + + // left + target.querySelector("button.left").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o_popover"); + assert.strictEqual(target.querySelector(".o_popover").innerText, "left"); + assert.hasClass(target.querySelector(".o_popover"), "o-popper-position--lm"); + }); + + QUnit.test("tooltip with a template, no info", async (assert) => { + class MyComponent extends Component {} + MyComponent.template = xml` + + `; + + const parent = await makeParent(MyComponent); + parent.env.qweb.addTemplate("my_tooltip_template", "tooltip"); + + assert.containsNone(parent, ".o_popover_container .o-tooltip"); + target.querySelector("button").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o-tooltip"); + assert.strictEqual(target.querySelector(".o-tooltip").innerHTML, "tooltip"); + }); + + QUnit.test("tooltip with a template and info", async (assert) => { + class MyComponent extends Component { + get info() { + return JSON.stringify({ x: 3, y: "abc" }); + } + } + MyComponent.template = xml` + + `; + + const parent = await makeParent(MyComponent); + parent.env.qweb.addTemplate( + "my_tooltip_template", + ` +
    +
  • X:
  • +
  • Y:
  • +
+ ` + ); + + assert.containsNone(parent, ".o_popover_container .o-tooltip"); + target.querySelector("button").dispatchEvent(new Event("mouseenter")); + await nextTick(); + assert.containsOnce(parent, ".o_popover_container .o-tooltip"); + assert.strictEqual( + target.querySelector(".o-tooltip").innerHTML, + "
  • X: 3
  • Y: abc
" + ); + }); + } +);