[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:
@@ -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>"
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user