diff --git a/addons/web/static/src/core/tooltip/tooltip_service.js b/addons/web/static/src/core/tooltip/tooltip_service.js index 90ca17af804..0aedee156d5 100644 --- a/addons/web/static/src/core/tooltip/tooltip_service.js +++ b/addons/web/static/src/core/tooltip/tooltip_service.js @@ -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) => { 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 d8a3e47cac5..193faced077 100644 --- a/addons/web/static/tests/core/tooltip/tooltip_service_tests.js +++ b/addons/web/static/tests/core/tooltip/tooltip_service_tests.js @@ -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``; + 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"); + }); });