[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) <aab@odoo.com>
This commit is contained in:
Mathieu Duckerts-Antoine
2022-02-08 19:11:21 +00:00
parent dc83963167
commit 96eb83c0a7
5 changed files with 443 additions and 409 deletions
@@ -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):
* <button data-tooltip="This is a tooltip">Do something</button>
*
* The ideal position of the tooltip can be specified thanks to the attribute
* "data-tooltip-position":
* <button data-tooltip="This is a tooltip" data-tooltip-position="left">Do something</button>
*
* 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:
* <t t-name="some_template">
* <ul>
* <li>info.x</li>
* <li>info.y</li>
* </ul>
* </t>
* This template can then be used in a tooltip as follows:
* <button data-tooltip-template="some_template" data-tooltip-info="info">Do something</button>
* 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 });
}
@@ -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:
* <button data-tooltip="This is a tooltip">Do something</button>
*
* The ideal position of the tooltip can be specified thanks to the attribute
* "data-tooltip-position":
* <button data-tooltip="This is a tooltip" data-tooltip-position="left">Do something</button>
*
* 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:
* <t t-name="some_template">
* <ul>
* <li>info.x</li>
* <li>info.y</li>
* </ul>
* </t>
* This template can then be used in a tooltip as follows:
* <button data-tooltip-template="some_template" data-tooltip-info="info">Do something</button>
* 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);
@@ -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() {
@@ -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<Component>}
*/
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`
<div>
<Child/>
<div>
<t t-foreach="Components" t-as="Component" t-key="Component[0]">
<t t-component="Component[1].Component" t-props="Component[1].props"/>
</t>
</div>
</div>`;
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`<button data-tooltip="hello">Action</button>`;
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`
<div>
<button t-if="state.visible" data-tooltip="hello">Action</button>
</div>`;
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`
<div>
<button class="button_1" data-tooltip="tooltip 1">Action 1</button>
<button class="button_2" data-tooltip="tooltip 2">Action 2</button>
</div>`;
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`
<div style="height: 400px; padding: 40px">
<button class="default" data-tooltip="default">Default</button>
<button class="top" data-tooltip="top" data-tooltip-position="top">Top</button>
<button class="right" data-tooltip="right" data-tooltip-position="right">Right</button>
<button class="bottom" data-tooltip="bottom" data-tooltip-position="bottom">Bottom</button>
<button class="left" data-tooltip="left" data-tooltip-position="left">Left</button>
</div>`;
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`
<button data-tooltip-template="my_tooltip_template">Action</button>
`;
const parent = await makeParent(MyComponent);
parent.env.qweb.addTemplate("my_tooltip_template", "<i>tooltip</i>");
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, "<i>tooltip</i>");
});
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`
<button
data-tooltip-template="my_tooltip_template"
t-att-data-tooltip-info="info">
Action
</button>
`;
const parent = await makeParent(MyComponent);
parent.env.qweb.addTemplate(
"my_tooltip_template",
`
<ul>
<li>X: <t t-esc="info.x"/></li>
<li>Y: <t t-esc="info.y"/></li>
</ul>
`
);
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,
"<ul><li>X: 3</li><li>Y: abc</li></ul>"
);
});
QUnit.test("destroy a component that called useTooltip", async (assert) => {
class MyComponent extends Component {}
MyComponent.template = xml`
<button data-tooltip="tooltip">Action</button>
`;
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"]);
});
});
@@ -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<Component>}
*/
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`
<div>
<Child/>
<div>
<t t-foreach="Components" t-as="Component" t-key="Component[0]">
<t t-component="Component[1].Component" t-props="Component[1].props"/>
</t>
</div>
</div>`;
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`<button data-tooltip="hello">Action</button>`;
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`<span data-tooltip="hello" class="our_span"><span class="our_span">Action</span></span>`;
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`
<div>
<button t-if="state.visible" data-tooltip="hello">Action</button>
</div>`;
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`
<div>
<button class="button_1" data-tooltip="tooltip 1">Action 1</button>
<button class="button_2" data-tooltip="tooltip 2">Action 2</button>
</div>`;
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`
<div style="height: 400px; padding: 40px">
<button class="default" data-tooltip="default">Default</button>
<button class="top" data-tooltip="top" data-tooltip-position="top">Top</button>
<button class="right" data-tooltip="right" data-tooltip-position="right">Right</button>
<button class="bottom" data-tooltip="bottom" data-tooltip-position="bottom">Bottom</button>
<button class="left" data-tooltip="left" data-tooltip-position="left">Left</button>
</div>`;
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`
<button data-tooltip-template="my_tooltip_template">Action</button>
`;
const parent = await makeParent(MyComponent);
parent.env.qweb.addTemplate("my_tooltip_template", "<i>tooltip</i>");
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, "<i>tooltip</i>");
});
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`
<button
data-tooltip-template="my_tooltip_template"
t-att-data-tooltip-info="info">
Action
</button>
`;
const parent = await makeParent(MyComponent);
parent.env.qweb.addTemplate(
"my_tooltip_template",
`
<ul>
<li>X: <t t-esc="info.x"/></li>
<li>Y: <t t-esc="info.y"/></li>
</ul>
`
);
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,
"<ul><li>X: 3</li><li>Y: abc</li></ul>"
);
});
}
);