[MOV] web: transfer ActionSwiper to community

This commit adapts codebase following the move and integration of the
touch device detection and customization (through the o_touch_device
class) in community.

X-original-commit: 5ae86ad473dd69559820b1f599ce389484cdebbc
Part-of: odoo/odoo#101185
Co-authored-by: Jorge Pinna Puissant <jpp@odoo.com>
This commit is contained in:
Bruno Boi
2022-09-26 22:48:43 +02:00
co-authored by Jorge Pinna Puissant
parent ac6a882160
commit 156d41dbda
5 changed files with 1190 additions and 0 deletions
@@ -0,0 +1,219 @@
/** @odoo-module **/
import { browser } from "@web/core/browser/browser";
import { localization } from "@web/core/l10n/localization";
import { clamp } from "@web/core/utils/numbers";
const { Component, onMounted, onWillUnmount, useRef, useState } = owl;
const isScrollSwipable = (scrollables) => {
return {
left: !scrollables.filter((e) => e.scrollLeft !== 0).length,
right: !scrollables.filter(
(e) => e.scrollLeft + Math.round(e.getBoundingClientRect().width) !== e.scrollWidth
).length,
};
};
/**
* Action Swiper
*
* This component is intended to perform action once a user has completed a touch swipe.
* You can choose the direction allowed for such behavior (left, right or both).
* The action to perform must be passed as a props. It is possible to define a condition
* to allow the swipe interaction conditionnally.
* @extends Component
*/
export class ActionSwiper extends Component {
setup() {
this.actionTimeoutId = null;
this.resetTimeoutId = null;
this.defaultState = {
containerStyle: "",
isSwiping: false,
width: undefined,
};
this.root = useRef("root");
this.targetContainer = useRef("targetContainer");
this.state = useState({ ...this.defaultState });
this.scrollables = undefined;
this.startX = undefined;
this.swipedDistance = 0;
this.isScrollValidated = false;
onMounted(() => {
if (this.targetContainer.el) {
this.state.width = this.targetContainer.el.getBoundingClientRect().width;
}
// Forward classes set on component to slot, as we only want to wrap an
// existing component without altering the DOM structure any more than
// strictly necessary
if (this.props.onLeftSwipe || this.props.onRightSwipe) {
const classes = new Set(this.root.el.classList);
classes.delete("o_actionswiper");
for (const className of classes) {
this.targetContainer.el.firstChild.classList.add(className);
this.root.el.classList.remove(className);
}
}
});
onWillUnmount(() => {
browser.clearTimeout(this.actionTimeoutId);
browser.clearTimeout(this.resetTimeoutId);
});
}
get localizedProps() {
return {
onLeftSwipe:
localization.direction === "rtl" ? this.props.onRightSwipe : this.props.onLeftSwipe,
onRightSwipe:
localization.direction === "rtl" ? this.props.onLeftSwipe : this.props.onRightSwipe,
};
}
/**
* @private
* @param {TouchEvent} ev
*/
_onTouchEndSwipe() {
if (this.state.isSwiping) {
this.state.isSwiping = false;
if (
this.localizedProps.onRightSwipe &&
this.swipedDistance > this.state.width / this.props.swipeDistanceRatio
) {
this.swipedDistance = this.state.width;
this.handleSwipe(this.localizedProps.onRightSwipe.action);
} else if (
this.localizedProps.onLeftSwipe &&
this.swipedDistance < -this.state.width / this.props.swipeDistanceRatio
) {
this.swipedDistance = -this.state.width;
this.handleSwipe(this.localizedProps.onLeftSwipe.action);
} else {
this.state.containerStyle = "";
}
}
}
/**
* @private
* @param {TouchEvent} ev
*/
_onTouchMoveSwipe(ev) {
if (this.state.isSwiping) {
const { onLeftSwipe, onRightSwipe } = this.localizedProps;
this.swipedDistance = clamp(
ev.touches[0].clientX - this.startX,
onLeftSwipe ? -this.state.width : 0,
onRightSwipe ? this.state.width : 0
);
// Prevent the browser to navigate back/forward when using swipe
// gestures while still allowing to scroll vertically.
if (Math.abs(this.swipedDistance) > 40) {
ev.preventDefault();
}
// If there are scrollable elements under touch pressure,
// they must be at their limits to allow swiping.
if (
!this.isScrollValidated &&
this.scrollables &&
!isScrollSwipable(this.scrollables)[this.swipedDistance > 0 ? "left" : "right"]
) {
return this._reset();
}
this.isScrollValidated = true;
if (this.props.animationOnMove) {
this.state.containerStyle = `transform: translateX(${this.swipedDistance}px)`;
}
}
}
/**
* @private
* @param {TouchEvent} ev
*/
_onTouchStartSwipe(ev) {
this.scrollables = ev
.composedPath()
.filter(
(e) =>
e.nodeType === 1 &&
this.targetContainer.el.contains(e) &&
e.scrollWidth > e.getBoundingClientRect().width &&
["auto", "scroll"].includes(window.getComputedStyle(e)["overflow-x"])
);
if (!this.state.width) {
this.state.width =
this.targetContainer && this.targetContainer.el.getBoundingClientRect().width;
}
this.state.isSwiping = true;
this.isScrollValidated = false;
this.startX = ev.touches[0].clientX;
}
/**
* @private
*/
_reset() {
Object.assign(this.state, { ...this.defaultState });
this.scrollables = undefined;
this.startX = undefined;
this.swipedDistance = 0;
this.isScrollValidated = false;
}
handleSwipe(action) {
if (this.props.animationType === "bounce") {
this.state.containerStyle = `transform: translateX(${this.swipedDistance}px)`;
this.actionTimeoutId = browser.setTimeout(() => {
action();
this._reset();
}, 500);
} else if (this.props.animationType === "forwards") {
this.state.containerStyle = `transform: translateX(${this.swipedDistance}px)`;
this.actionTimeoutId = browser.setTimeout(() => {
action();
this.state.isSwiping = true;
this.state.containerStyle = `transform: translateX(${-this.swipedDistance}px)`;
this.resetTimeoutId = browser.setTimeout(() => {
this._reset();
}, 100);
}, 100);
} else {
return action();
}
}
}
ActionSwiper.props = {
onLeftSwipe: {
type: Object,
args: {
action: Function,
icon: String,
bgColor: String,
},
optional: true,
},
onRightSwipe: {
type: Object,
args: {
action: Function,
icon: String,
bgColor: String,
},
optional: true,
},
slots: Object,
animationOnMove: { type: Boolean, optional: true },
animationType: { type: String, optional: true },
swipeDistanceRatio: { type: Number, optional: true },
};
ActionSwiper.defaultProps = {
onLeftSwipe: undefined,
onRightSwipe: undefined,
animationOnMove: true,
animationType: "bounce",
swipeDistanceRatio: 2,
};
ActionSwiper.template = "web.ActionSwiper";
@@ -0,0 +1,20 @@
.o_actionswiper {
position: relative;
touch-action: pan-y;
}
.o_actionswiper_target_container {
transition: transform 0.4s;
}
.o_actionswiper_swiping {
transition: none;
}
.o_actionswiper_right_swipe_area {
/*rtl:ignore*/
transform: translateX(-100%);
inset: 0 auto auto 0;
}
.o_actionswiper_left_swipe_area {
/*rtl:ignore*/
transform: translateX(100%);
inset: 0 0 auto auto;
}
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="web.ActionSwiper" owl="1">
<t t-if="props.onRightSwipe || props.onLeftSwipe">
<div class="o_actionswiper" t-on-touchend="_onTouchEndSwipe" t-on-touchmove="_onTouchMoveSwipe" t-on-touchstart="_onTouchStartSwipe" t-ref="root">
<div class="o_actionswiper_overflow_container position-relative overflow-hidden">
<div class="o_actionswiper_target_container" t-ref="targetContainer" t-att-style="state.containerStyle" t-att-class="{ o_actionswiper_swiping: state.isSwiping }">
<t t-slot="default"/>
<t t-if="localizedProps.onRightSwipe and (localizedProps.onRightSwipe.icon or localizedProps.onRightSwipe.bgColor)">
<div t-att-style="'max-width: ' + swipedDistance + 'px;'" class="o_actionswiper_right_swipe_area position-absolute overflow-hidden w-100 h-100 d-flex align-items-center justify-content-center rounded-end" t-att-class="localizedProps.onRightSwipe.bgColor">
<span><i class="fa fa-2x" t-att-class="localizedProps.onRightSwipe.icon"/></span>
</div>
</t>
<t t-if="localizedProps.onLeftSwipe and (localizedProps.onLeftSwipe.icon or localizedProps.onLeftSwipe.bgColor)">
<div t-att-style="'max-width: ' + -swipedDistance + 'px;'" class="o_actionswiper_left_swipe_area position-absolute overflow-hidden w-100 h-100 d-flex align-items-center justify-content-center rounded-start" t-att-class="localizedProps.onLeftSwipe.bgColor">
<span><i class="fa fa-2x" t-att-class="localizedProps.onLeftSwipe.icon"/></span>
</div>
</t>
</div>
</div>
</div>
</t>
<t t-else="">
<t t-slot="default"/>
</t>
</t>
</templates>
@@ -0,0 +1,866 @@
/** @odoo-module **/
import { ActionSwiper } from "@web/core/action_swiper/action_swiper";
import { registry } from "@web/core/registry";
import { makeFakeLocalizationService } from "@web/../tests/helpers/mock_services";
import { makeTestEnv } from "@web/../tests/helpers/mock_env";
import {
mount,
nextTick,
triggerEvent,
getFixture,
mockTimeout,
} from "@web/../tests/helpers/utils";
const { Component, xml } = owl;
const serviceRegistry = registry.category("services");
let env;
let target;
QUnit.module("ActionSwiper", ({ beforeEach }) => {
beforeEach(async () => {
env = await makeTestEnv();
target = getFixture();
});
// Tests marked as [REQUIRE TOUCHEVENT] will fail on browsers that don't support
// TouchEvent by default. It might be an option to activate on some browser.
QUnit.test("render only its target if no props is given", async (assert) => {
class Parent extends Component {}
Parent.components = { ActionSwiper };
Parent.template = xml`
<div class="d-flex">
<ActionSwiper>
<div class="target-component"/>
</ActionSwiper>
</div>
`;
await mount(Parent, target, { env });
assert.containsNone(target, "div.o_actionswiper");
assert.containsOnce(target, "div.target-component");
});
QUnit.test("only render the necessary divs", async (assert) => {
await mount(ActionSwiper, target, {
env,
props: {
onRightSwipe: {
action: () => {},
icon: "fa-circle",
bgColor: "bg-warning",
},
slots: {},
},
});
assert.containsOnce(target, "div.o_actionswiper_right_swipe_area");
assert.containsNone(target, "div.o_actionswiper_left_swipe_area");
await mount(ActionSwiper, target, {
env,
props: {
onLeftSwipe: {
action: () => {},
icon: "fa-circle",
bgColor: "bg-warning",
},
slots: {},
},
});
assert.containsOnce(target, "div.o_actionswiper_right_swipe_area");
assert.containsOnce(target, "div.o_actionswiper_left_swipe_area");
});
QUnit.test("render with the height of its content", async (assert) => {
assert.expect(2);
class Parent extends Component {
onRightSwipe() {
assert.step("onRightSwipe");
}
}
Parent.components = { ActionSwiper };
Parent.template = xml`
<div class="o-container d-flex" style="width: 200px; height: 200px; overflow: auto">
<ActionSwiper onRightSwipe = "{
action: onRightSwipe,
icon: 'fa-circle',
bgColor: 'bg-warning'
}">
<div class="target-component" style="height: 800px">This element is very high and
the o-container element must have a scrollbar</div>
</ActionSwiper>
</div>
`;
await mount(Parent, target, { env });
assert.ok(
target.querySelector(".o_actionswiper").scrollHeight ===
target.querySelector(".target-component").scrollHeight,
"the swiper has the height of its content"
);
assert.ok(
target.querySelector(".o_actionswiper").scrollHeight >
target.querySelector(".o_actionswiper").clientHeight,
"the height of the swiper must make the parent div scrollable"
);
});
QUnit.test(
"can perform actions by swiping to the right [REQUIRE TOUCHEVENT]",
async (assert) => {
assert.expect(5);
const { execRegisteredTimeouts } = mockTimeout();
class Parent extends Component {
onRightSwipe() {
assert.step("onRightSwipe");
}
}
Parent.components = { ActionSwiper };
Parent.template = xml`
<div class="d-flex">
<ActionSwiper onRightSwipe = "{
action: onRightSwipe,
icon: 'fa-circle',
bgColor: 'bg-warning'
}">
<div class="target-component" style="width: 200px; height: 80px">Test</div>
</ActionSwiper>
</div>
`;
await mount(Parent, target, { env });
const swiper = target.querySelector(".o_actionswiper");
const targetContainer = target.querySelector(".o_actionswiper_target_container");
await triggerEvent(target, ".o_actionswiper", "touchstart", {
touches: [
{
identifier: 0,
clientX: 0,
clientY: 0,
target: target,
},
],
});
await triggerEvent(target, ".o_actionswiper", "touchmove", {
touches: [
{
identifier: 0,
clientX: (3 * swiper.clientWidth) / 4,
clientY: 0,
target: target,
},
],
});
assert.ok(
targetContainer.style.transform.includes("translateX"),
"target has translateX"
);
// Touch ends before the half of the distance has been reached
await triggerEvent(target, ".o_actionswiper", "touchmove", {
touches: [
{
identifier: 0,
clientX: swiper.clientWidth / 2 - 1,
clientY: 0,
target: target,
},
],
});
await triggerEvent(target, ".o_actionswiper", "touchend", {});
execRegisteredTimeouts();
await nextTick();
assert.ok(
!targetContainer.style.transform.includes("translateX"),
"target does not have a translate value"
);
// Touch ends once the half of the distance has been crossed
await triggerEvent(target, ".o_actionswiper", "touchstart", {
touches: [
{
identifier: 0,
clientX: swiper.clientWidth / 2,
clientY: 0,
target: target,
},
],
});
await triggerEvent(target, ".o_actionswiper", "touchmove", {
touches: [
{
identifier: 0,
clientX: swiper.clientWidth + 1,
clientY: 0,
target: target,
},
],
});
await triggerEvent(target, ".o_actionswiper", "touchend", {});
execRegisteredTimeouts();
await nextTick();
// The action is performed AND the component is reset
assert.ok(
!targetContainer.style.transform.includes("translateX"),
"target doesn't have translateX after action is performed"
);
assert.verifySteps(["onRightSwipe"]);
}
);
QUnit.test(
"can perform actions by swiping in both directions [REQUIRE TOUCHEVENT]",
async (assert) => {
assert.expect(7);
const { execRegisteredTimeouts } = mockTimeout();
class Parent extends Component {
onRightSwipe() {
assert.step("onRightSwipe");
}
onLeftSwipe() {
assert.step("onLeftSwipe");
}
}
Parent.components = { ActionSwiper };
Parent.template = xml`
<div class="d-flex">
<ActionSwiper
onRightSwipe = "{
action: onRightSwipe,
icon: 'fa-circle',
bgColor: 'bg-warning'
}"
onLeftSwipe = "{
action: onLeftSwipe,
icon: 'fa-check',
bgColor: 'bg-success'
}">
<div class="target-component" style="width: 250px; height: 80px">Swipe in both directions</div>
</ActionSwiper>
</div>
`;
await mount(Parent, target, { env });
const swiper = target.querySelector(".o_actionswiper");
const targetContainer = target.querySelector(".o_actionswiper_target_container");
await triggerEvent(target, ".o_actionswiper", "touchstart", {
touches: [
{
identifier: 0,
clientX: 0,
clientY: 0,
target: target,
},
],
});
await triggerEvent(target, ".o_actionswiper", "touchmove", {
touches: [
{
identifier: 0,
clientX: (3 * swiper.clientWidth) / 4,
clientY: 0,
target: target,
},
],
});
assert.ok(
targetContainer.style.transform.includes("translateX"),
"target has translateX"
);
// Touch ends before the half of the distance has been reached to the left
await triggerEvent(target, ".o_actionswiper", "touchmove", {
touches: [
{
identifier: 0,
clientX: -swiper.clientWidth / 2 + 1,
clientY: 0,
target: target,
},
],
});
await triggerEvent(target, ".o_actionswiper", "touchend", {});
execRegisteredTimeouts();
await nextTick();
assert.ok(
!targetContainer.style.transform.includes("translateX"),
"target does not have a translate value"
);
// Touch ends once the half of the distance has been crossed to the left
await triggerEvent(target, ".o_actionswiper", "touchstart", {
touches: [
{
identifier: 0,
clientX: swiper.clientWidth / 2,
clientY: 0,
target: target,
},
],
});
await triggerEvent(target, ".o_actionswiper", "touchmove", {
touches: [
{
identifier: 0,
clientX: -swiper.clientWidth - 1,
clientY: 0,
target: target,
},
],
});
await triggerEvent(target, ".o_actionswiper", "touchend", {});
execRegisteredTimeouts();
await nextTick();
assert.verifySteps(["onLeftSwipe"], "the onLeftSwipe props action has been performed");
// Touch ends once the half of the distance has been crossed to the right
await triggerEvent(target, ".o_actionswiper", "touchstart", {
touches: [
{
identifier: 0,
clientX: swiper.clientWidth / 2,
clientY: 0,
target: target,
},
],
});
await triggerEvent(target, ".o_actionswiper", "touchmove", {
touches: [
{
identifier: 0,
clientX: swiper.clientWidth + 1,
clientY: 0,
target: target,
},
],
});
await triggerEvent(target, ".o_actionswiper", "touchend", {});
execRegisteredTimeouts();
await nextTick();
assert.ok(
!targetContainer.style.transform.includes("translateX"),
"target doesn't have translateX after all actions are performed"
);
assert.verifySteps(
["onRightSwipe"],
"the onRightSwipe props action has been performed"
);
}
);
QUnit.test(
"invert the direction of swipes when language is rtl [REQUIRE TOUCHEVENT]",
async (assert) => {
assert.expect(7);
const { execRegisteredTimeouts } = mockTimeout();
class Parent extends Component {
onRightSwipe() {
assert.step("onRightSwipe");
}
onLeftSwipe() {
assert.step("onLeftSwipe");
}
}
Parent.components = { ActionSwiper };
Parent.template = xml`
<div class="d-flex">
<ActionSwiper
onRightSwipe = "{
action: onRightSwipe,
icon: 'fa-circle',
bgColor: 'bg-warning'
}"
onLeftSwipe = "{
action: onLeftSwipe,
icon: 'fa-check',
bgColor: 'bg-success'
}">
<div class="target-component" style="width: 250px; height: 80px">Swipe in both directions</div>
</ActionSwiper>
</div>
`;
serviceRegistry.add("localization", makeFakeLocalizationService({ direction: "rtl" }));
await mount(Parent, target, { env });
const swiper = target.querySelector(".o_actionswiper");
const targetContainer = target.querySelector(".o_actionswiper_target_container");
await triggerEvent(target, ".o_actionswiper", "touchstart", {
touches: [
{
identifier: 0,
clientX: 0,
clientY: 0,
target: target,
},
],
});
await triggerEvent(target, ".o_actionswiper", "touchmove", {
touches: [
{
identifier: 0,
clientX: (3 * swiper.clientWidth) / 4,
clientY: 0,
target: target,
},
],
});
assert.ok(
targetContainer.style.transform.includes("translateX"),
"target has translateX"
);
// Touch ends before the half of the distance has been reached to the left
await triggerEvent(target, ".o_actionswiper", "touchmove", {
touches: [
{
identifier: 0,
clientX: -swiper.clientWidth / 2 + 1,
clientY: 0,
target: target,
},
],
});
await triggerEvent(target, ".o_actionswiper", "touchend", {});
execRegisteredTimeouts();
await nextTick();
assert.ok(
!targetContainer.style.transform.includes("translateX"),
"target does not have a translate value"
);
// Touch ends once the half of the distance has been crossed to the left
await triggerEvent(target, ".o_actionswiper", "touchstart", {
touches: [
{
identifier: 0,
clientX: swiper.clientWidth / 2,
clientY: 0,
target: target,
},
],
});
await triggerEvent(target, ".o_actionswiper", "touchmove", {
touches: [
{
identifier: 0,
clientX: -swiper.clientWidth - 1,
clientY: 0,
target: target,
},
],
});
await triggerEvent(target, ".o_actionswiper", "touchend", {});
execRegisteredTimeouts();
await nextTick();
// In rtl languages, actions are permuted
assert.verifySteps(
["onRightSwipe"],
"the onRightSwipe props action has been performed"
);
await triggerEvent(target, ".o_actionswiper", "touchstart", {
touches: [
{
identifier: 0,
clientX: swiper.clientWidth / 2,
clientY: 0,
target: target,
},
],
});
await triggerEvent(target, ".o_actionswiper", "touchmove", {
touches: [
{
identifier: 0,
clientX: swiper.clientWidth + 1,
clientY: 0,
target: target,
},
],
});
await triggerEvent(target, ".o_actionswiper", "touchend", {});
execRegisteredTimeouts();
await nextTick();
assert.ok(
!targetContainer.style.transform.includes("translateX"),
"target doesn't have translateX after all actions are performed"
);
// In rtl languages, actions are permuted
assert.verifySteps(["onLeftSwipe"], "the onLeftSwipe props action has been performed");
}
);
QUnit.test(
"swiping when the swiper contains scrollable areas [REQUIRE TOUCHEVENT]",
async (assert) => {
assert.expect(9);
const { execRegisteredTimeouts } = mockTimeout();
class Parent extends Component {
onRightSwipe() {
assert.step("onRightSwipe");
}
onLeftSwipe() {
assert.step("onLeftSwipe");
}
}
Parent.components = { ActionSwiper };
Parent.template = xml`
<div class="d-flex">
<ActionSwiper
onRightSwipe = "{
action: onRightSwipe,
icon: 'fa-circle',
bgColor: 'bg-warning'
}"
onLeftSwipe = "{
action: onLeftSwipe,
icon: 'fa-check',
bgColor: 'bg-success'
}">
<div class="target-component" style="width: 200px; height: 300px">
<h1>Test about swiping and scrolling</h1>
<div class="large-content" style="overflow: auto">
<h2>This div contains a larger element that will make it scrollable</h2>
<p class="large-text" style="width: 400px">This element is so large it needs to be scrollable</p>
</div>
</div>
</ActionSwiper>
</div>
`;
await mount(Parent, target, { env });
const swiper = target.querySelector(".o_actionswiper");
const targetContainer = target.querySelector(".o_actionswiper_target_container");
const scrollable = target.querySelector(".large-content");
// The scrollable element is set as scrollable
scrollable.scrollLeft = 100;
await triggerEvent(target, ".o_actionswiper", "touchstart", {
touches: [
{
identifier: 0,
clientX: 0,
clientY: 0,
target: target,
},
],
});
await triggerEvent(target, ".o_actionswiper", "touchmove", {
touches: [
{
identifier: 0,
clientX: (3 * swiper.clientWidth) / 4,
clientY: 0,
target: target,
},
],
});
assert.ok(
targetContainer.style.transform.includes("translateX"),
"the swiper can swipe if the scrollable area is not under touch pressure"
);
await triggerEvent(target, ".o_actionswiper", "touchmove", {
touches: [
{
identifier: 0,
clientX: 0,
clientY: 0,
target: target,
},
],
});
await triggerEvent(target, ".o_actionswiper", "touchend", {});
await triggerEvent(scrollable, ".large-text", "touchstart", {
touches: [
{
identifier: 0,
clientX: scrollable.clientLeft,
clientY:
scrollable.getBoundingClientRect().top +
scrollable.getBoundingClientRect().height / 2,
target: scrollable.querySelector(".large-text"),
},
],
});
await triggerEvent(scrollable, ".large-text", "touchmove", {
touches: [
{
identifier: 0,
clientX: scrollable.clientWidth,
clientY:
scrollable.getBoundingClientRect().top +
scrollable.getBoundingClientRect().height / 2,
target: scrollable.querySelector(".large-text"),
},
],
});
assert.ok(
!targetContainer.style.transform.includes("translateX"),
"the swiper has not swiped to the right because the scrollable element was scrollable to the left"
);
// The scrollable element is set at its left limit
scrollable.scrollLeft = 0;
await triggerEvent(scrollable, ".large-text", "touchstart", {
touches: [
{
identifier: 0,
clientX: scrollable.clientLeft,
clientY:
scrollable.getBoundingClientRect().top +
scrollable.getBoundingClientRect().height / 2,
target: scrollable.querySelector(".large-text"),
},
],
});
await triggerEvent(scrollable, ".large-text", "touchmove", {
touches: [
{
identifier: 0,
clientX: scrollable.clientWidth,
clientY:
scrollable.getBoundingClientRect().top +
scrollable.getBoundingClientRect().height / 2,
target: scrollable.querySelector(".large-text"),
},
],
});
assert.ok(
targetContainer.style.transform.includes("translateX"),
"the swiper has swiped to the right because the scrollable element couldn't scroll anymore to the left"
);
await triggerEvent(target, ".o_actionswiper", "touchend", {});
execRegisteredTimeouts();
await nextTick();
assert.verifySteps(
["onRightSwipe"],
"the onRightSwipe props action has been performed"
);
await triggerEvent(scrollable, ".large-text", "touchstart", {
touches: [
{
identifier: 0,
clientX: scrollable.clientWidth,
clientY:
scrollable.getBoundingClientRect().top +
scrollable.getBoundingClientRect().height / 2,
target: scrollable.querySelector(".large-text"),
},
],
});
await triggerEvent(scrollable, ".large-text", "touchmove", {
touches: [
{
identifier: 0,
clientX: scrollable.clientLeft,
clientY:
scrollable.getBoundingClientRect().top +
scrollable.getBoundingClientRect().height / 2,
target: scrollable.querySelector(".large-text"),
},
],
});
assert.ok(
!targetContainer.style.transform.includes("translateX"),
"the swiper has not swiped to the left because the scrollable element was scrollable to the right"
);
// The scrollable element is set at its right limit
scrollable.scrollLeft =
scrollable.scrollWidth - scrollable.getBoundingClientRect().right;
await triggerEvent(scrollable, ".large-text", "touchstart", {
touches: [
{
identifier: 0,
clientX: scrollable.clientWidth,
clientY:
scrollable.getBoundingClientRect().top +
scrollable.getBoundingClientRect().height / 2,
target: scrollable.querySelector(".large-text"),
},
],
});
await triggerEvent(scrollable, ".large-text", "touchmove", {
touches: [
{
identifier: 0,
clientX: scrollable.clientLeft,
clientY:
scrollable.getBoundingClientRect().top +
scrollable.getBoundingClientRect().height / 2,
target: scrollable.querySelector(".large-text"),
},
],
});
assert.ok(
targetContainer.style.transform.includes("translateX"),
"the swiper has swiped to the left because the scrollable element couldn't scroll anymore to the right"
);
await triggerEvent(target, ".o_actionswiper", "touchend", {});
execRegisteredTimeouts();
await nextTick();
assert.verifySteps(["onLeftSwipe"], "the onLeftSwipe props action has been performed");
}
);
QUnit.test(
"preventing swipe on scrollable areas when language is rtl [REQUIRE TOUCHEVENT]",
async (assert) => {
assert.expect(8);
const { execRegisteredTimeouts } = mockTimeout();
class Parent extends Component {
onRightSwipe() {
assert.step("onRightSwipe");
}
onLeftSwipe() {
assert.step("onLeftSwipe");
}
}
Parent.components = { ActionSwiper };
Parent.template = xml`
<div class="d-flex">
<ActionSwiper
onRightSwipe = "{
action: onRightSwipe,
icon: 'fa-circle',
bgColor: 'bg-warning'
}"
onLeftSwipe = "{
action: onLeftSwipe,
icon: 'fa-check',
bgColor: 'bg-success'
}">
<div class="target-component" style="width: 200px; height: 300px">
<h1>Test about swiping and scrolling for rtl</h1>
<div class="large-content" style="overflow: auto">
<h2>elballorcs ti ekam lliw taht tnemele regral a sniatnoc vid sihT</h2>
<p class="large-text" style="width: 400px">elballorcs eb ot sdeen ti egral os si tnemele sihT</p>
</div>
</div>
</ActionSwiper>
</div>
`;
serviceRegistry.add("localization", makeFakeLocalizationService({ direction: "rtl" }));
await mount(Parent, target, { env });
const targetContainer = target.querySelector(".o_actionswiper_target_container");
const scrollable = target.querySelector(".large-content");
// The scrollable element is set as scrollable
scrollable.scrollLeft = 100;
await triggerEvent(scrollable, ".large-text", "touchstart", {
touches: [
{
identifier: 0,
clientX: scrollable.clientLeft,
clientY:
scrollable.getBoundingClientRect().top +
scrollable.getBoundingClientRect().height / 2,
target: scrollable.querySelector(".large-text"),
},
],
});
await triggerEvent(scrollable, ".large-text", "touchmove", {
touches: [
{
identifier: 0,
clientX: scrollable.clientWidth,
clientY:
scrollable.getBoundingClientRect().top +
scrollable.getBoundingClientRect().height / 2,
target: scrollable.querySelector(".large-text"),
},
],
});
assert.ok(
!targetContainer.style.transform.includes("translateX"),
"the swiper has not swiped to the right because the scrollable element was scrollable to the left"
);
// The scrollable element is set at its left limit
scrollable.scrollLeft = 0;
await triggerEvent(scrollable, ".large-text", "touchstart", {
touches: [
{
identifier: 0,
clientX: scrollable.clientLeft,
clientY:
scrollable.getBoundingClientRect().top +
scrollable.getBoundingClientRect().height / 2,
target: scrollable.querySelector(".large-text"),
},
],
});
await triggerEvent(scrollable, ".large-text", "touchmove", {
touches: [
{
identifier: 0,
clientX: scrollable.clientWidth,
clientY:
scrollable.getBoundingClientRect().top +
scrollable.getBoundingClientRect().height / 2,
target: scrollable.querySelector(".large-text"),
},
],
});
assert.ok(
targetContainer.style.transform.includes("translateX"),
"the swiper has swiped to the right because the scrollable element couldn't scroll anymore to the left"
);
await triggerEvent(target, ".o_actionswiper", "touchend", {});
execRegisteredTimeouts();
await nextTick();
// In rtl languages, actions are permuted
assert.verifySteps(["onLeftSwipe"], "the onLeftSwipe props action has been performed");
await triggerEvent(scrollable, ".large-text", "touchstart", {
touches: [
{
identifier: 0,
clientX: scrollable.clientWidth,
clientY:
scrollable.getBoundingClientRect().top +
scrollable.getBoundingClientRect().height / 2,
target: scrollable.querySelector(".large-text"),
},
],
});
await triggerEvent(scrollable, ".large-text", "touchmove", {
touches: [
{
identifier: 0,
clientX: scrollable.clientLeft,
clientY:
scrollable.getBoundingClientRect().top +
scrollable.getBoundingClientRect().height / 2,
target: scrollable.querySelector(".large-text"),
},
],
});
assert.ok(
!targetContainer.style.transform.includes("translateX"),
"the swiper has not swiped to the left because the scrollable element was scrollable to the right"
);
// The scrollable element is set at its right limit
scrollable.scrollLeft =
scrollable.scrollWidth - scrollable.getBoundingClientRect().right;
await triggerEvent(scrollable, ".large-text", "touchstart", {
touches: [
{
identifier: 0,
clientX: scrollable.clientWidth,
clientY:
scrollable.getBoundingClientRect().top +
scrollable.getBoundingClientRect().height / 2,
target: scrollable.querySelector(".large-text"),
},
],
});
await triggerEvent(scrollable, ".large-text", "touchmove", {
touches: [
{
identifier: 0,
clientX: scrollable.clientLeft,
clientY:
scrollable.getBoundingClientRect().top +
scrollable.getBoundingClientRect().height / 2,
target: scrollable.querySelector(".large-text"),
},
],
});
assert.ok(
targetContainer.style.transform.includes("translateX"),
"the swiper has swiped to the left because the scrollable element couldn't scroll anymore to the right"
);
await triggerEvent(target, ".o_actionswiper", "touchend", {});
execRegisteredTimeouts();
await nextTick();
// In rtl languages, actions are permuted
assert.verifySteps(
["onRightSwipe"],
"the onRightSwipe props action has been performed"
);
}
);
});
+58
View File
@@ -0,0 +1,58 @@
/** @odoo-module **/
import { findElement, triggerEvent } from "../helpers/utils";
async function swipe(target, selector, direction) {
const touchTarget = findElement(target, selector);
if (direction === "left") {
// The scrollable element is set at its right limit
touchTarget.scrollLeft = touchTarget.scrollWidth - touchTarget.offsetWidth;
} else {
// The scrollable element is set at its left limit
touchTarget.scrollLeft = 0;
}
await triggerEvent(target, selector, "touchstart", {
touches: [
{
identifier: 0,
clientX: 0,
clientY: 0,
target: touchTarget,
},
],
});
await triggerEvent(target, selector, "touchmove", {
touches: [
{
identifier: 0,
clientX: (direction === "left" ? -1 : 1) * touchTarget.clientWidth,
clientY: 0,
target: touchTarget,
},
],
});
await triggerEvent(target, selector, "touchend", {});
}
/**
* Will simulate a swipe right on the target element with the given selector.
*
* @param {HTMLElement} target
* @param {DOMSelector} [selector]
* @returns {Promise}
*/
export async function swipeRight(target, selector) {
return swipe(target, selector, "right");
}
/**
* Will simulate a swipe left on the target element with the given selector.
*
* @param {HTMLElement} target
* @param {DOMSelector} [selector]
* @returns {Promise}
*/
export async function swipeLeft(target, selector) {
return swipe(target, selector, "left");
}