[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:
co-authored by
Jorge Pinna Puissant
parent
ac6a882160
commit
156d41dbda
@@ -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"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user