[IMP] web: tooltip: style, delay and empty tooltips

This commit is threefold:
 1) it improves the style of tooltips (mostly, a larger font size)
 2) it adds an option, `data-tooltip-delay` that allows to specify
    the delay before the tooltip opens
 3) it handles the case where the tooltip is empty (no tooltip
    displayed)

Part-of: odoo/odoo#92475
This commit is contained in:
Aaron Bohy
2022-06-30 09:29:43 +02:00
parent 34c2610e9f
commit 8697a136c3
3 changed files with 91 additions and 25 deletions
@@ -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;
}
}
}
@@ -19,6 +19,9 @@ const { whenReady } = owl;
* "data-tooltip-position":
* <button data-tooltip="This is a tooltip" data-tooltip-position="left">Do something</button>
*
* The opening delay can be modified with the "data-tooltip-delay" attribute (default: 400):
* <button data-tooltip="This is a tooltip" data-tooltip-delay="0">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:
@@ -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);
@@ -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<Component>}
*/
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) => {
"<ul><li>X: 3</li><li>Y: abc</li></ul>"
);
});
QUnit.test("empty tooltip, no template", async (assert) => {
class MyComponent extends Component {
get tooltip() {
return "";
}
}
MyComponent.template = xml`<button t-att-data-tooltip="tooltip">Action</button>`;
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`<button data-tooltip="'helpful tooltip'" data-tooltip-delay="2000">Action</button>`;
const mockSetTimeout = (fn, delay) => {
assert.strictEqual(delay, 2000);
};
await makeParent(MyComponent, { mockSetTimeout });
target.querySelector("button").dispatchEvent(new Event("mouseenter"));
await nextTick();
});
});