[FIX] web: fix draggable with touch input

This commit fixes some isues with touch behaviors in the drag and
drop feature. Since commit (1), pointer events are used instead of
mouse events in the draggable hook. This makes the drag and drop
available with touch, but some issues appeared.

The following issues have been adressed by the commit:
- Scrolling in a kanban view has become difficult, since items are
dragged, and columns are moved. A simple scroll in the view to show
other items was nearly impossible without interacting with elements.
A small delay has now be introduced in the draggable_hook_builder by
default for touch interactions. The user has to do a long press (500ms)
to instanciate the drag sequence.

- on Windows laptops, the hasTouch helper would not return true, even
on PCs and tablets with a touchscreen, because 'ontouchstart' is not
present in the window object. It meant that any touch feature was not
working properly (e.g. moving icons from the home screen).
This commit adds a mediaQuery check right after the current check on
the window object, to also consider those laptops as touch devices.

Tests have been added to check the behavior of the touch drag, and the
presence of a bouncing animation when initiated. The corresponding drag
helper now has the possibility to set a custom pointerType.

task-3436591

closes odoo/odoo#132373

X-original-commit: 4c5cf0962f91d05085fea75e9c26ecdca9c0eb13
Signed-off-by: Julien Mougenot (jum) <jum@odoo.com>
This commit is contained in:
luvi
2023-08-19 12:01:57 +02:00
parent cd527b9394
commit 440c72f2aa
5 changed files with 128 additions and 5 deletions
@@ -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() {
@@ -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 });
@@ -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;
}
@@ -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`
<div t-ref="root" class="root">
<ul class="list">
<li t-foreach="[1, 2, 3]" t-as="i" t-key="i" t-esc="i" class="item" />
</ul>
</div>`;
await mount(List, target);
assert.verifySteps([]);
const { drop, moveTo } = await drag(".item:first-child", "touch");
await moveTo(".item:nth-child(2)");
assert.hasClass(target.querySelector(".item"), "o_dragged");
await drop();
assert.verifySteps(["start", "drag", "drop", "end"]);
});
QUnit.test("Dragging element with touch event: initiation delay can be overrided", async (assert) => {
patchWithCleanup(browser, {
matchMedia: (media) => {
if (media === "(pointer:coarse)") {
return { matches: true };
} else {
this._super();
}
},
setTimeout: (fn, delay) => {
assert.strictEqual(delay, 1000, "touch drag has the custom initiation delay");
fn();
}
});
class List extends Component {
setup() {
useDraggable({
ref: useRef("root"),
delay: 1000,
elements: ".item",
});
}
}
List.template = xml`
<div t-ref="root" class="root">
<ul class="list">
<li t-foreach="[1, 2, 3]" t-as="i" t-key="i" t-esc="i" class="item" />
</ul>
</div>`;
await mount(List, target);
const { drop, moveTo } = await drag(".item:first-child", "touch");
await moveTo(".item:nth-child(2)");
await drop();
});
});
+2 -1
View File
@@ -848,7 +848,7 @@ export async function dragAndDrop(from, to, position) {
*
* @param {Element | string} from
*/
export async function drag(from) {
export async function drag(from, pointerType = "mouse") {
const assertIsDragging = (fn, endDrag) => {
return {
async [fn.name](...args) {
@@ -961,6 +961,7 @@ export async function drag(from) {
// Pointer down on main target
await triggerEvent(source, null, "pointerdown", {
pointerType,
clientX: sourceRect.x + sourceRect.width / 2,
clientY: sourceRect.y + sourceRect.height / 2,
});