diff --git a/addons/web/static/src/core/browser/feature_detection.js b/addons/web/static/src/core/browser/feature_detection.js index 85740a1d88b..21c6294b1b0 100644 --- a/addons/web/static/src/core/browser/feature_detection.js +++ b/addons/web/static/src/core/browser/feature_detection.js @@ -62,7 +62,7 @@ export function isDisplayStandalone() { } export function hasTouch() { - return browser.ontouchstart !== undefined; + return browser.ontouchstart !== undefined || browser.matchMedia("(pointer:coarse)").matches; } export function maxTouchPoints() { diff --git a/addons/web/static/src/core/utils/draggable_hook_builder.js b/addons/web/static/src/core/utils/draggable_hook_builder.js index 4f883ea48de..df0bd02724b 100644 --- a/addons/web/static/src/core/utils/draggable_hook_builder.js +++ b/addons/web/static/src/core/utils/draggable_hook_builder.js @@ -95,6 +95,7 @@ const DEFAULT_DEFAULT_PARAMS = { }, delay: 0, tolerance: 10, + touch_delay: 300, }; const LEFT_CLICK = 0; const MANDATORY_PARAMS = ["ref"]; @@ -651,6 +652,8 @@ export function makeDraggableHook(hookParams) { const onPointerDown = (ev) => { updatePointerPosition(ev); + const initiationDelay = ev.pointerType === "touch" ? ctx.touch_delay : ctx.delay; + // A drag sequence can still be in progress if the pointerup occurred // outside of the window. dragEnd(null); @@ -679,8 +682,11 @@ export function makeDraggableHook(hookParams) { target.releasePointerCapture(pointerId); } - if (ctx.delay) { + if (initiationDelay) { if (hasTouch()) { + if (ev.pointerType === "touch") { + dom.addClass(target.closest(ctx.elementSelector), "o_touch_bounce"); + } if (isBrowserFirefox()) { // On Firefox mobile, long-touch events trigger an unpreventable // context menu to appear. To prevent this, all linkes are removed @@ -714,7 +720,7 @@ export function makeDraggableHook(hookParams) { // Note that the timeout is cleared in dragEnd dragEnd(null); } - }, ctx.delay); + }, initiationDelay); cleanup.add(() => browser.clearTimeout(ctx.current.timeout)); } else { willStartDrag(target); @@ -927,6 +933,7 @@ export function makeDraggableHook(hookParams) { // Delay & tolerance ctx.delay = actualParams.delay; + ctx.touch_delay = actualParams.delay || actualParams.touch_delay; ctx.tolerance = actualParams.tolerance; callBuildHandler("onComputeParams", { params: actualParams }); diff --git a/addons/web/static/src/core/utils/draggable_hook_builder.scss b/addons/web/static/src/core/utils/draggable_hook_builder.scss index 07e4f4c20ad..6af67c52dfa 100644 --- a/addons/web/static/src/core/utils/draggable_hook_builder.scss +++ b/addons/web/static/src/core/utils/draggable_hook_builder.scss @@ -1,3 +1,15 @@ +@keyframes bounce { + 0% { + transform: scale(1); + } + 60% { + transform: scale(.95); + } + 100% { + transform: scale(1); + } +} + .o_draggable { -webkit-touch-callout: none; // for iOS } @@ -6,3 +18,8 @@ z-index: 1000; pointer-events: none; } + +.o_touch_bounce { + animation: bounce .4s forwards; + user-select: none; +} diff --git a/addons/web/static/tests/core/utils/draggable_tests.js b/addons/web/static/tests/core/utils/draggable_tests.js index 4622acaf865..6b82c994852 100644 --- a/addons/web/static/tests/core/utils/draggable_tests.js +++ b/addons/web/static/tests/core/utils/draggable_tests.js @@ -1,7 +1,8 @@ /** @odoo-module **/ -import { drag, dragAndDrop, getFixture, mount, nextTick } from "@web/../tests/helpers/utils"; +import { drag, dragAndDrop, getFixture, mount, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils"; import { useDraggable } from "@web/core/utils/draggable"; +import { browser } from "@web/core/browser/browser"; import { Component, reactive, useRef, useState, xml } from "@odoo/owl"; @@ -213,4 +214,101 @@ QUnit.module("Draggable", ({ beforeEach }) => { assert.verifySteps([]); }); + + QUnit.test("Dragging element with touch event", async (assert) => { + assert.expect(10); + + patchWithCleanup(browser, { + matchMedia: (media) => { + if (media === "(pointer:coarse)") { + return { matches: true }; + } else { + this._super(); + } + }, + setTimeout: (fn, delay) => { + assert.strictEqual(delay, 300, "touch drag has a default 300ms initiation delay"); + fn(); + } + }); + + class List extends Component { + setup() { + useDraggable({ + ref: useRef("root"), + elements: ".item", + onDragStart({ element }) { + assert.step("start"); + assert.hasClass(element, "o_touch_bounce", "element has the animation class applied"); + }, + onDrag() { + assert.step("drag"); + }, + onDragEnd() { + assert.step("end"); + }, + async onDrop({ element }) { + assert.step("drop"); + await nextTick(); + assert.doesNotHaveClass(element, "o_touch_bounce", "element no longer has the animation class applied"); + }, + }); + } + } + + List.template = xml` +