[IMP] web: Tooltip UX on touch-enabled devices
Port of legacy's BasicRenderer for WOWL views. In a nutshell: Tooltips are problematic in touch devices as they are meant to appear on hover... which doesn't happen with touch events. This commit implements a "tap-to-show" behavior which let the user see the Tooltip by pressing the element, and letting it disappear when the pressure ends. Part-of: odoo/odoo#95924
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Tooltip } from "./tooltip";
|
||||
import { hasTouch } from "@web/core/browser/feature_detection";
|
||||
|
||||
const { whenReady } = owl;
|
||||
|
||||
@@ -47,6 +48,7 @@ export const tooltipService = {
|
||||
let target = null;
|
||||
let positionX;
|
||||
let positionY;
|
||||
let touchPressed;
|
||||
const elementsWithTooltips = new Map();
|
||||
|
||||
/**
|
||||
@@ -70,6 +72,9 @@ export const tooltipService = {
|
||||
if (!document.body.contains(target)) {
|
||||
return true; // target is no longer in the DOM
|
||||
}
|
||||
if (hasTouch()) {
|
||||
return !touchPressed;
|
||||
}
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
if (
|
||||
positionX < targetRect.left ||
|
||||
@@ -120,18 +125,13 @@ export const tooltipService = {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether there is a tooltip registered on the event target, and
|
||||
* Checks whether there is a tooltip registered on the element, and
|
||||
* if there is, creates a timeout to open the corresponding tooltip
|
||||
* after a delay.
|
||||
*
|
||||
* @param {MouseEvent} ev a "mouseenter" event
|
||||
* @param {HTMLElement} el
|
||||
*/
|
||||
function onMouseenter(ev) {
|
||||
// set mouse position in case the target contains disabled elements which won't trigger mousemove
|
||||
positionX = ev.x;
|
||||
positionY = ev.y;
|
||||
/** @type {HTMLElement} */
|
||||
const el = ev.target;
|
||||
function openElementsTooltip(el) {
|
||||
if (elementsWithTooltips.has(el)) {
|
||||
openTooltip(el, elementsWithTooltips.get(el));
|
||||
} else if (el.matches("[data-tooltip], [data-tooltip-template]")) {
|
||||
@@ -148,6 +148,32 @@ export const tooltipService = {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether there is a tooltip registered on the event target, and
|
||||
* if there is, creates a timeout to open the corresponding tooltip
|
||||
* after a delay.
|
||||
*
|
||||
* @param {MouseEvent} ev a "mouseenter" event
|
||||
*/
|
||||
function onMouseenter(ev) {
|
||||
// set mouse position in case the target contains disabled elements which won't trigger mousemove
|
||||
positionX = ev.x;
|
||||
positionY = ev.y;
|
||||
openElementsTooltip(ev.target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether there is a tooltip registered on the event target, and
|
||||
* if there is, creates a timeout to open the corresponding tooltip
|
||||
* after a delay.
|
||||
*
|
||||
* @param {TouchEvent} ev a "touchstart" event
|
||||
*/
|
||||
function onTouchStart(ev) {
|
||||
touchPressed = true;
|
||||
openElementsTooltip(ev.target);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -157,6 +183,20 @@ export const tooltipService = {
|
||||
}
|
||||
}, CLOSE_DELAY);
|
||||
|
||||
if (hasTouch()) {
|
||||
document.body.addEventListener("touchstart", onTouchStart);
|
||||
|
||||
document.body.addEventListener("touchend", () => {
|
||||
touchPressed = false;
|
||||
});
|
||||
|
||||
document.body.addEventListener("touchcancel", () => {
|
||||
touchPressed = false;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 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) => {
|
||||
|
||||
@@ -41,6 +41,7 @@ export async function makeParent(Child, options = {}) {
|
||||
clearTimeout: options.mockClearTimeout || (() => {}),
|
||||
setInterval: options.mockSetInterval || ((fn) => fn()),
|
||||
clearInterval: options.mockClearInterval || (() => {}),
|
||||
ontouchstart: options.mockOnTouchStart || undefined,
|
||||
});
|
||||
|
||||
registry.category("services").add("popover", popoverService);
|
||||
@@ -375,4 +376,38 @@ QUnit.module("Tooltip service", (hooks) => {
|
||||
target.querySelector("button").dispatchEvent(new Event("mouseenter"));
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
QUnit.test("touch 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 mockOnTouchStart = () => {};
|
||||
await makeParent(MyComponent, { mockSetTimeout, mockSetInterval, mockOnTouchStart });
|
||||
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
await triggerEvent(target, "button", "touchstart");
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
|
||||
simulateTimeout();
|
||||
await nextTick();
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_popover_container .o_popover").innerText,
|
||||
"hello"
|
||||
);
|
||||
|
||||
await triggerEvent(target, "button", "touchend");
|
||||
assert.containsOnce(target, ".o_popover_container .o_popover");
|
||||
simulateInterval();
|
||||
await nextTick();
|
||||
assert.containsNone(target, ".o_popover_container .o_popover");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user