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