diff --git a/addons/web/static/src/core/tooltip/tooltip.scss b/addons/web/static/src/core/tooltip/tooltip.scss
index 98933b8ad56..faacce47bf4 100644
--- a/addons/web/static/src/core/tooltip/tooltip.scss
+++ b/addons/web/static/src/core/tooltip/tooltip.scss
@@ -1,5 +1,5 @@
.o-tooltip {
- font-size: smaller;
+ font-size: small;
.o-tooltip--string {
background-color: $o-tooltip-title-background-color;
@@ -8,18 +8,21 @@
}
.o-tooltip--help {
+ font-size: smaller;
white-space: pre-line;
padding: 8px;
margin-bottom: 0px;
}
.o-tooltip--technical {
+ font-size: smaller;
padding: 8px;
margin: 0 0 0 15px;
list-style-type: circle;
.o-tooltip--technical--title {
font-weight: bold;
+ margin-right: 4px;
}
}
}
diff --git a/addons/web/static/src/core/tooltip/tooltip_service.js b/addons/web/static/src/core/tooltip/tooltip_service.js
index bc591385fc8..1aa80452a63 100644
--- a/addons/web/static/src/core/tooltip/tooltip_service.js
+++ b/addons/web/static/src/core/tooltip/tooltip_service.js
@@ -19,6 +19,9 @@ const { whenReady } = owl;
* "data-tooltip-position":
*
*
+ * The opening delay can be modified with the "data-tooltip-delay" attribute (default: 400):
+ *
+ *
* 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:
@@ -42,7 +45,6 @@ export const tooltipService = {
let openTooltipTimeout;
let closeTooltip;
let target = null;
-
let positionX;
let positionY;
@@ -56,6 +58,29 @@ export const tooltipService = {
}
}
+ /**
+ * Checks that the target is in the DOM and we're hovering the target.
+ * @returns {boolean}
+ */
+ function shouldCleanup() {
+ 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 true; // mouse is no longer hovering the target
+ }
+ return false;
+ }
+
/**
* Opens the tooltip after a delay, if the event's target has a data-tooltip
* attribute.
@@ -76,35 +101,39 @@ export const tooltipService = {
info = dataset.tooltipInfo ? JSON.parse(dataset.tooltipInfo) : null;
}
cleanup();
+ if (tooltip === "" && !template) {
+ return;
+ }
+
+ // register mouse position first in case the target
+ // contains disabled elements which would prevent
+ // to track mousemove. The position is needed to check
+ // whether the element is hovered or not
+ positionX = ev.x;
+ positionY = ev.y;
+
+ const delay = dataset.tooltipDelay ? JSON.parse(dataset.tooltipDelay) : OPEN_DELAY;
openTooltipTimeout = browser.setTimeout(() => {
- const position = dataset.tooltipPosition;
- closeTooltip = popover.add(
- target,
- Tooltip,
- { tooltip, template, info },
- { position }
- );
- }, OPEN_DELAY);
+ if (shouldCleanup()) {
+ cleanup();
+ } else {
+ const position = dataset.tooltipPosition;
+ closeTooltip = popover.add(
+ target,
+ Tooltip,
+ { tooltip, template, info },
+ { position }
+ );
+ }
+ }, 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
+ if (shouldCleanup()) {
+ cleanup();
}
}, CLOSE_DELAY);
diff --git a/addons/web/static/tests/core/tooltip/tooltip_service_tests.js b/addons/web/static/tests/core/tooltip/tooltip_service_tests.js
index 463d20084bd..247ce04c321 100644
--- a/addons/web/static/tests/core/tooltip/tooltip_service_tests.js
+++ b/addons/web/static/tests/core/tooltip/tooltip_service_tests.js
@@ -8,6 +8,7 @@ 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";
+import { makeFakeLocalizationService } from "../../helpers/mock_services";
const { App, Component, useState, xml } = owl;
@@ -28,7 +29,7 @@ const mainComponents = registry.category("main_components");
* @param {{[templateName:string]: string}} [options.templates] additional templates
* @returns {Promise}
*/
-async function makeParent(Child, options = {}) {
+export async function makeParent(Child, options = {}) {
const target = getFixture();
// add the popover service to the registry -> will add the PopoverContainer
@@ -44,6 +45,7 @@ async function makeParent(Child, options = {}) {
registry.category("services").add("popover", popoverService);
registry.category("services").add("tooltip", tooltipService);
+ registry.category("services").add("localization", makeFakeLocalizationService());
const env = await makeTestEnv();
patchWithCleanup(env.services.popover, {
@@ -341,4 +343,36 @@ QUnit.module("Tooltip service", (hooks) => {
""
);
});
+
+ QUnit.test("empty tooltip, no template", async (assert) => {
+ class MyComponent extends Component {
+ get tooltip() {
+ return "";
+ }
+ }
+ MyComponent.template = xml``;
+ let simulateTimeout = () => {};
+ const mockSetTimeout = (fn) => {
+ simulateTimeout = fn;
+ };
+ await makeParent(MyComponent, { mockSetTimeout });
+
+ assert.containsNone(target, ".o_popover_container .o_popover");
+ target.querySelector("button").dispatchEvent(new Event("mouseenter"));
+ await nextTick();
+ simulateTimeout();
+ await nextTick();
+ assert.containsNone(target, ".o_popover_container .o_popover");
+ });
+
+ QUnit.test("tooltip with a delay", async (assert) => {
+ class MyComponent extends Component {}
+ MyComponent.template = xml``;
+ const mockSetTimeout = (fn, delay) => {
+ assert.strictEqual(delay, 2000);
+ };
+ await makeParent(MyComponent, { mockSetTimeout });
+ target.querySelector("button").dispatchEvent(new Event("mouseenter"));
+ await nextTick();
+ });
});