mirror of
https://github.com/discourse/discourse.git
synced 2025-01-30 03:39:46 +08:00
DEV: uses swipe-events lib for swipe modifier (#26905)
This commit also: uses the swipe modifier in the glimmer-site-header component changes closing condition for d-modal and toast from distance to velocity cancels toast auto close on touch
This commit is contained in:
parent
69e5c9f611
commit
92a59e2480
|
@ -16,6 +16,7 @@ import {
|
||||||
disableBodyScroll,
|
disableBodyScroll,
|
||||||
enableBodyScroll,
|
enableBodyScroll,
|
||||||
} from "discourse/lib/body-scroll-lock";
|
} from "discourse/lib/body-scroll-lock";
|
||||||
|
import { getMaxAnimationTimeMs } from "discourse/lib/swipe-events";
|
||||||
import swipe from "discourse/modifiers/swipe";
|
import swipe from "discourse/modifiers/swipe";
|
||||||
import trapTab from "discourse/modifiers/trap-tab";
|
import trapTab from "discourse/modifiers/trap-tab";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
@ -28,8 +29,7 @@ export const CLOSE_INITIATED_BY_SWIPE_DOWN = "initiatedBySwipeDown";
|
||||||
|
|
||||||
const FLASH_TYPES = ["success", "error", "warning", "info"];
|
const FLASH_TYPES = ["success", "error", "warning", "info"];
|
||||||
|
|
||||||
const ANIMATE_MODAL_DURATION = 250;
|
const SWIPE_VELOCITY_THRESHOLD = 0.7;
|
||||||
const MIN_SWIPE_THRESHOLD = -5;
|
|
||||||
|
|
||||||
export default class DModal extends Component {
|
export default class DModal extends Component {
|
||||||
@service modal;
|
@service modal;
|
||||||
|
@ -69,7 +69,7 @@ export default class DModal extends Component {
|
||||||
await element.animate(
|
await element.animate(
|
||||||
[{ transform: "translateY(100%)" }, { transform: "translateY(0)" }],
|
[{ transform: "translateY(100%)" }, { transform: "translateY(0)" }],
|
||||||
{
|
{
|
||||||
duration: ANIMATE_MODAL_DURATION,
|
duration: getMaxAnimationTimeMs(),
|
||||||
easing: "ease",
|
easing: "ease",
|
||||||
fill: "forwards",
|
fill: "forwards",
|
||||||
}
|
}
|
||||||
|
@ -121,39 +121,36 @@ export default class DModal extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async handleSwipe(state) {
|
async handleSwipe(swipeEvent) {
|
||||||
if (!this.site.mobileView) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.animating) {
|
if (this.animating) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.deltaY < 0) {
|
if (swipeEvent.deltaY >= 0) {
|
||||||
await this.#animateWrapperPosition(Math.abs(state.deltaY));
|
return await this.#animateWrapperPosition(swipeEvent.deltaY);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleSwipeEnded(state) {
|
async handleSwipeEnded(swipeEvent) {
|
||||||
if (!this.site.mobileView) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.animating) {
|
if (this.animating) {
|
||||||
// if the modal is animating we don't want to risk resetting the position
|
// if the modal is animating we don't want to risk resetting the position
|
||||||
// as the user releases the swipe at the same time
|
// as the user releases the swipe at the same time
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.deltaY < MIN_SWIPE_THRESHOLD) {
|
if (swipeEvent.goingUp()) {
|
||||||
|
return await this.#animateWrapperPosition(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (swipeEvent.velocityY >= SWIPE_VELOCITY_THRESHOLD) {
|
||||||
this.wrapperElement.querySelector(
|
this.wrapperElement.querySelector(
|
||||||
".d-modal__container"
|
".d-modal__container"
|
||||||
).style.transform = `translateY(${Math.abs(state.deltaY)}px)`;
|
).style.transform = `translateY(${swipeEvent.deltaY}px)`;
|
||||||
|
|
||||||
this.closeModal(CLOSE_INITIATED_BY_SWIPE_DOWN);
|
this.closeModal(CLOSE_INITIATED_BY_SWIPE_DOWN);
|
||||||
|
} else {
|
||||||
|
return await this.#animateWrapperPosition(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,7 +185,7 @@ export default class DModal extends Component {
|
||||||
{ visibility: "visible", offset: 0.01 },
|
{ visibility: "visible", offset: 0.01 },
|
||||||
{ transform: "translateY(100%)", offset: 1 },
|
{ transform: "translateY(100%)", offset: 1 },
|
||||||
],
|
],
|
||||||
{ duration: ANIMATE_MODAL_DURATION, fill: "forwards" }
|
{ duration: getMaxAnimationTimeMs(), fill: "forwards" }
|
||||||
).finished;
|
).finished;
|
||||||
this.animating = false;
|
this.animating = false;
|
||||||
}
|
}
|
||||||
|
@ -273,6 +270,7 @@ export default class DModal extends Component {
|
||||||
[{ transform: `translateY(${position}px)` }],
|
[{ transform: `translateY(${position}px)` }],
|
||||||
{
|
{
|
||||||
fill: "forwards",
|
fill: "forwards",
|
||||||
|
duration: getMaxAnimationTimeMs(),
|
||||||
}
|
}
|
||||||
).finished;
|
).finished;
|
||||||
}
|
}
|
||||||
|
@ -319,8 +317,8 @@ export default class DModal extends Component {
|
||||||
<div
|
<div
|
||||||
class={{concatClass "d-modal__header" @headerClass}}
|
class={{concatClass "d-modal__header" @headerClass}}
|
||||||
{{swipe
|
{{swipe
|
||||||
didSwipe=this.handleSwipe
|
onDidSwipe=this.handleSwipe
|
||||||
didEndSwipe=this.handleSwipeEnded
|
onDidEndSwipe=this.handleSwipeEnded
|
||||||
enabled=this.dismissable
|
enabled=this.dismissable
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -417,8 +415,8 @@ export default class DModal extends Component {
|
||||||
<div
|
<div
|
||||||
class="d-modal__backdrop"
|
class="d-modal__backdrop"
|
||||||
{{swipe
|
{{swipe
|
||||||
didSwipe=this.handleSwipe
|
onDidSwipe=this.handleSwipe
|
||||||
didEndSwipe=this.handleSwipeEnded
|
onDidEndSwipe=this.handleSwipeEnded
|
||||||
enabled=this.dismissable
|
enabled=this.dismissable
|
||||||
}}
|
}}
|
||||||
{{on "click" this.handleWrapperClick}}
|
{{on "click" this.handleWrapperClick}}
|
||||||
|
|
|
@ -8,8 +8,12 @@ import { waitForPromise } from "@ember/test-waiters";
|
||||||
import ItsATrap from "@discourse/itsatrap";
|
import ItsATrap from "@discourse/itsatrap";
|
||||||
import concatClass from "discourse/helpers/concat-class";
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
import scrollLock from "discourse/lib/scroll-lock";
|
import scrollLock from "discourse/lib/scroll-lock";
|
||||||
import SwipeEvents from "discourse/lib/swipe-events";
|
import {
|
||||||
|
getMaxAnimationTimeMs,
|
||||||
|
shouldCloseMenu,
|
||||||
|
} from "discourse/lib/swipe-events";
|
||||||
import { isDocumentRTL } from "discourse/lib/text-direction";
|
import { isDocumentRTL } from "discourse/lib/text-direction";
|
||||||
|
import swipe from "discourse/modifiers/swipe";
|
||||||
import { isTesting } from "discourse-common/config/environment";
|
import { isTesting } from "discourse-common/config/environment";
|
||||||
import discourseLater from "discourse-common/lib/later";
|
import discourseLater from "discourse-common/lib/later";
|
||||||
import { bind, debounce } from "discourse-common/utils/decorators";
|
import { bind, debounce } from "discourse-common/utils/decorators";
|
||||||
|
@ -31,7 +35,6 @@ export default class GlimmerSiteHeader extends Component {
|
||||||
_animate = false;
|
_animate = false;
|
||||||
_headerWrap;
|
_headerWrap;
|
||||||
_swipeMenuOrigin;
|
_swipeMenuOrigin;
|
||||||
_swipeEvents;
|
|
||||||
_applicationElement;
|
_applicationElement;
|
||||||
_resizeObserver;
|
_resizeObserver;
|
||||||
_docAt;
|
_docAt;
|
||||||
|
@ -142,15 +145,6 @@ export default class GlimmerSiteHeader extends Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
this._resizeObserver.observe(this._headerWrap);
|
this._resizeObserver.observe(this._headerWrap);
|
||||||
|
|
||||||
this._swipeEvents = new SwipeEvents(this._headerWrap);
|
|
||||||
if (this.site.mobileView) {
|
|
||||||
this._swipeEvents.addTouchListeners();
|
|
||||||
this._headerWrap.addEventListener("swipestart", this.onSwipeStart);
|
|
||||||
this._headerWrap.addEventListener("swipeend", this.onSwipeEnd);
|
|
||||||
this._headerWrap.addEventListener("swipecancel", this.onSwipeCancel);
|
|
||||||
this._headerWrap.addEventListener("swipe", this.onSwipe);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,9 +268,9 @@ export default class GlimmerSiteHeader extends Component {
|
||||||
@bind
|
@bind
|
||||||
_animateOpening(panel, event = null) {
|
_animateOpening(panel, event = null) {
|
||||||
const cloakElement = document.querySelector(".header-cloak");
|
const cloakElement = document.querySelector(".header-cloak");
|
||||||
let durationMs = this._swipeEvents.getMaxAnimationTimeMs();
|
let durationMs = getMaxAnimationTimeMs();
|
||||||
if (event && this.pxClosed > 0) {
|
if (event && this.pxClosed > 0) {
|
||||||
durationMs = this._swipeEvents.getMaxAnimationTimeMs(
|
durationMs = getMaxAnimationTimeMs(
|
||||||
this.pxClosed / Math.abs(event.velocityX)
|
this.pxClosed / Math.abs(event.velocityX)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -294,10 +288,10 @@ export default class GlimmerSiteHeader extends Component {
|
||||||
_animateClosing(event, panel, menuOrigin) {
|
_animateClosing(event, panel, menuOrigin) {
|
||||||
this._animate = true;
|
this._animate = true;
|
||||||
const cloakElement = document.querySelector(".header-cloak");
|
const cloakElement = document.querySelector(".header-cloak");
|
||||||
let durationMs = this._swipeEvents.getMaxAnimationTimeMs();
|
let durationMs = getMaxAnimationTimeMs();
|
||||||
if (event && this.pxClosed > 0) {
|
if (event && this.pxClosed > 0) {
|
||||||
const distancePx = PANEL_WIDTH - this.pxClosed;
|
const distancePx = PANEL_WIDTH - this.pxClosed;
|
||||||
durationMs = this._swipeEvents.getMaxAnimationTimeMs(
|
durationMs = getMaxAnimationTimeMs(
|
||||||
distancePx / Math.abs(event.velocityX)
|
distancePx / Math.abs(event.velocityX)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -328,9 +322,8 @@ export default class GlimmerSiteHeader extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
onSwipeStart(event) {
|
onSwipeStart(swipeEvent) {
|
||||||
const e = event.detail;
|
const center = swipeEvent.center;
|
||||||
const center = e.center;
|
|
||||||
const swipeOverValidElement = document
|
const swipeOverValidElement = document
|
||||||
.elementsFromPoint(center.x, center.y)
|
.elementsFromPoint(center.x, center.y)
|
||||||
.some(
|
.some(
|
||||||
|
@ -340,7 +333,7 @@ export default class GlimmerSiteHeader extends Component {
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
swipeOverValidElement &&
|
swipeOverValidElement &&
|
||||||
(e.direction === "left" || e.direction === "right")
|
(swipeEvent.direction === "left" || swipeEvent.direction === "right")
|
||||||
) {
|
) {
|
||||||
scrollLock(true, document.querySelector(".panel-body"));
|
scrollLock(true, document.querySelector(".panel-body"));
|
||||||
} else {
|
} else {
|
||||||
|
@ -349,16 +342,15 @@ export default class GlimmerSiteHeader extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
onSwipeEnd(event) {
|
onSwipeEnd(swipeEvent) {
|
||||||
const e = event.detail;
|
|
||||||
const menuPanels = document.querySelectorAll(".menu-panel");
|
const menuPanels = document.querySelectorAll(".menu-panel");
|
||||||
scrollLock(false, document.querySelector(".panel-body"));
|
scrollLock(false, document.querySelector(".panel-body"));
|
||||||
menuPanels.forEach((panel) => {
|
menuPanels.forEach((panel) => {
|
||||||
if (this._swipeEvents.shouldCloseMenu(e, this._swipeMenuOrigin)) {
|
if (shouldCloseMenu(swipeEvent, this._swipeMenuOrigin)) {
|
||||||
this._animateClosing(e, panel, this._swipeMenuOrigin);
|
this._animateClosing(swipeEvent, panel, this._swipeMenuOrigin);
|
||||||
scrollLock(false);
|
scrollLock(false);
|
||||||
} else {
|
} else {
|
||||||
this._animateOpening(panel, e);
|
this._animateOpening(panel, swipeEvent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -373,17 +365,15 @@ export default class GlimmerSiteHeader extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
onSwipe(event) {
|
onSwipe(swipeEvent) {
|
||||||
const e = event.detail;
|
|
||||||
|
|
||||||
const movingElement = document.querySelector(".menu-panel");
|
const movingElement = document.querySelector(".menu-panel");
|
||||||
const cloakElement = document.querySelector(".header-cloak");
|
const cloakElement = document.querySelector(".header-cloak");
|
||||||
|
|
||||||
//origin left
|
//origin left
|
||||||
this.pxClosed = Math.max(0, -e.deltaX);
|
this.pxClosed = Math.max(0, -swipeEvent.deltaX);
|
||||||
let translation = -this.pxClosed;
|
let translation = -this.pxClosed;
|
||||||
if (this._swipeMenuOrigin === "right") {
|
if (this._swipeMenuOrigin === "right") {
|
||||||
this.pxClosed = Math.max(0, e.deltaX);
|
this.pxClosed = Math.max(0, swipeEvent.deltaX);
|
||||||
translation = this.pxClosed;
|
translation = this.pxClosed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -421,13 +411,6 @@ export default class GlimmerSiteHeader extends Component {
|
||||||
|
|
||||||
window.removeEventListener("scroll", this._onScroll);
|
window.removeEventListener("scroll", this._onScroll);
|
||||||
this._resizeObserver?.disconnect();
|
this._resizeObserver?.disconnect();
|
||||||
if (this.site.mobileView) {
|
|
||||||
this._headerWrap.removeEventListener("swipestart", this.onSwipeStart);
|
|
||||||
this._headerWrap.removeEventListener("swipeend", this.onSwipeEnd);
|
|
||||||
this._headerWrap.removeEventListener("swipecancel", this.onSwipeCancel);
|
|
||||||
this._headerWrap.removeEventListener("swipe", this.onSwipe);
|
|
||||||
this._swipeEvents.removeTouchListeners();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -437,6 +420,12 @@ export default class GlimmerSiteHeader extends Component {
|
||||||
"d-header-wrap"
|
"d-header-wrap"
|
||||||
}}
|
}}
|
||||||
{{didInsert this.setupHeader}}
|
{{didInsert this.setupHeader}}
|
||||||
|
{{swipe
|
||||||
|
onDidStartSwipe=this.onSwipeStart
|
||||||
|
onDidEndSwipe=this.onSwipeEnd
|
||||||
|
onDidCancelSwipe=this.onSwipeCancel
|
||||||
|
onDidSwipe=this.onSwipe
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Header
|
<Header
|
||||||
@canSignUp={{@canSignUp}}
|
@canSignUp={{@canSignUp}}
|
||||||
|
|
|
@ -5,7 +5,10 @@ import ItsATrap from "@discourse/itsatrap";
|
||||||
import MountWidget from "discourse/components/mount-widget";
|
import MountWidget from "discourse/components/mount-widget";
|
||||||
import { topicTitleDecorators } from "discourse/components/topic-title";
|
import { topicTitleDecorators } from "discourse/components/topic-title";
|
||||||
import scrollLock from "discourse/lib/scroll-lock";
|
import scrollLock from "discourse/lib/scroll-lock";
|
||||||
import SwipeEvents from "discourse/lib/swipe-events";
|
import SwipeEvents, {
|
||||||
|
getMaxAnimationTimeMs,
|
||||||
|
shouldCloseMenu,
|
||||||
|
} from "discourse/lib/swipe-events";
|
||||||
import { isDocumentRTL } from "discourse/lib/text-direction";
|
import { isDocumentRTL } from "discourse/lib/text-direction";
|
||||||
import Docking from "discourse/mixins/docking";
|
import Docking from "discourse/mixins/docking";
|
||||||
import RerenderOnDoNotDisturbChange from "discourse/mixins/rerender-on-do-not-disturb-change";
|
import RerenderOnDoNotDisturbChange from "discourse/mixins/rerender-on-do-not-disturb-change";
|
||||||
|
@ -58,9 +61,9 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
|
|
||||||
_animateOpening(panel, event = null) {
|
_animateOpening(panel, event = null) {
|
||||||
const headerCloak = document.querySelector(".header-cloak");
|
const headerCloak = document.querySelector(".header-cloak");
|
||||||
let durationMs = this._swipeEvents.getMaxAnimationTimeMs();
|
let durationMs = getMaxAnimationTimeMs();
|
||||||
if (event && this.pxClosed > 0) {
|
if (event && this.pxClosed > 0) {
|
||||||
durationMs = this._swipeEvents.getMaxAnimationTimeMs(
|
durationMs = getMaxAnimationTimeMs(
|
||||||
this.pxClosed / Math.abs(event.velocityX)
|
this.pxClosed / Math.abs(event.velocityX)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -77,10 +80,10 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
_animateClosing(event, panel, menuOrigin) {
|
_animateClosing(event, panel, menuOrigin) {
|
||||||
this._animate = true;
|
this._animate = true;
|
||||||
const headerCloak = document.querySelector(".header-cloak");
|
const headerCloak = document.querySelector(".header-cloak");
|
||||||
let durationMs = this._swipeEvents.getMaxAnimationTimeMs();
|
let durationMs = getMaxAnimationTimeMs();
|
||||||
if (event && this.pxClosed > 0) {
|
if (event && this.pxClosed > 0) {
|
||||||
const distancePx = this._PANEL_WIDTH - this.pxClosed;
|
const distancePx = this._PANEL_WIDTH - this.pxClosed;
|
||||||
durationMs = this._swipeEvents.getMaxAnimationTimeMs(
|
durationMs = getMaxAnimationTimeMs(
|
||||||
distancePx / Math.abs(event.velocityX)
|
distancePx / Math.abs(event.velocityX)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -139,7 +142,7 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
const menuOrigin = this._swipeMenuOrigin;
|
const menuOrigin = this._swipeMenuOrigin;
|
||||||
scrollLock(false, document.querySelector(".panel-body"));
|
scrollLock(false, document.querySelector(".panel-body"));
|
||||||
menuPanels.forEach((panel) => {
|
menuPanels.forEach((panel) => {
|
||||||
if (this._swipeEvents.shouldCloseMenu(e, menuOrigin)) {
|
if (shouldCloseMenu(e, menuOrigin)) {
|
||||||
this._animateClosing(e, panel, menuOrigin);
|
this._animateClosing(e, panel, menuOrigin);
|
||||||
} else {
|
} else {
|
||||||
this._animateOpening(panel, e);
|
this._animateOpening(panel, e);
|
||||||
|
|
|
@ -1,6 +1,52 @@
|
||||||
import { isTesting } from "discourse-common/config/environment";
|
import { isTesting } from "discourse-common/config/environment";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
|
// common max animation time in ms for swipe events for swipe end
|
||||||
|
// prefers reduced motion and tests return 0
|
||||||
|
export function getMaxAnimationTimeMs(durationMs = MAX_ANIMATION_TIME) {
|
||||||
|
if (
|
||||||
|
isTesting() ||
|
||||||
|
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||||
|
) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Math.min(durationMs, MAX_ANIMATION_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
//functions to calculate if a swipe should close
|
||||||
|
//based on origin of right, left, top, bottom
|
||||||
|
// menu should close after a swipe either:
|
||||||
|
// if a user moved the panel closed past a threshold and away and is NOT swiping back open
|
||||||
|
// if a user swiped to close fast enough regardless of distance
|
||||||
|
export function shouldCloseMenu(e, origin) {
|
||||||
|
if (origin === "right") {
|
||||||
|
return (
|
||||||
|
(e.deltaX > SWIPE_DISTANCE_THRESHOLD &&
|
||||||
|
e.velocityX > -SWIPE_VELOCITY_THRESHOLD) ||
|
||||||
|
e.velocityX > 0
|
||||||
|
);
|
||||||
|
} else if (origin === "left") {
|
||||||
|
return (
|
||||||
|
(e.deltaX < -SWIPE_DISTANCE_THRESHOLD &&
|
||||||
|
e.velocityX < SWIPE_VELOCITY_THRESHOLD) ||
|
||||||
|
e.velocityX < 0
|
||||||
|
);
|
||||||
|
} else if (origin === "bottom") {
|
||||||
|
return (
|
||||||
|
(e.deltaY > SWIPE_DISTANCE_THRESHOLD &&
|
||||||
|
e.velocityY > -SWIPE_VELOCITY_THRESHOLD) ||
|
||||||
|
e.velocityY > 0
|
||||||
|
);
|
||||||
|
} else if (origin === "top") {
|
||||||
|
return (
|
||||||
|
(e.deltaY < -SWIPE_DISTANCE_THRESHOLD &&
|
||||||
|
e.velocityY < SWIPE_VELOCITY_THRESHOLD) ||
|
||||||
|
e.velocityY < 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Swipe events is a class that allows components to detect and respond to swipe gestures
|
Swipe events is a class that allows components to detect and respond to swipe gestures
|
||||||
It sets up custom events for swipestart, swipeend, and swipe for beginning swipe, end swipe, and during swipe. Event returns detail.state with swipe state, and the original event.
|
It sets up custom events for swipestart, swipeend, and swipe for beginning swipe, end swipe, and during swipe. Event returns detail.state with swipe state, and the original event.
|
||||||
|
@ -11,6 +57,7 @@ export const SWIPE_DISTANCE_THRESHOLD = 50;
|
||||||
export const SWIPE_VELOCITY_THRESHOLD = 0.12;
|
export const SWIPE_VELOCITY_THRESHOLD = 0.12;
|
||||||
export const MINIMUM_SWIPE_DISTANCE = 5;
|
export const MINIMUM_SWIPE_DISTANCE = 5;
|
||||||
export const MAX_ANIMATION_TIME = 200;
|
export const MAX_ANIMATION_TIME = 200;
|
||||||
|
|
||||||
export default class SwipeEvents {
|
export default class SwipeEvents {
|
||||||
//velocity is pixels per ms
|
//velocity is pixels per ms
|
||||||
|
|
||||||
|
@ -35,7 +82,7 @@ export default class SwipeEvents {
|
||||||
this.element.dispatchEvent(event);
|
this.element.dispatchEvent(event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.#swipeStart(e.touches[0]);
|
this.swipeState = this.#swipeStart(e.touches[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
|
@ -63,9 +110,8 @@ export default class SwipeEvents {
|
||||||
}
|
}
|
||||||
|
|
||||||
addTouchListeners() {
|
addTouchListeners() {
|
||||||
const opts = {
|
const opts = { passive: false };
|
||||||
passive: false,
|
|
||||||
};
|
|
||||||
this.element.addEventListener("touchstart", this.touchStart, opts);
|
this.element.addEventListener("touchstart", this.touchStart, opts);
|
||||||
this.element.addEventListener("touchmove", this.touchMove, opts);
|
this.element.addEventListener("touchmove", this.touchMove, opts);
|
||||||
this.element.addEventListener("touchend", this.touchEnd, opts);
|
this.element.addEventListener("touchend", this.touchEnd, opts);
|
||||||
|
@ -80,52 +126,6 @@ export default class SwipeEvents {
|
||||||
this.element.removeEventListener("touchcancel", this.touchCancel);
|
this.element.removeEventListener("touchcancel", this.touchCancel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// common max animation time in ms for swipe events for swipe end
|
|
||||||
// prefers reduced motion and tests return 0
|
|
||||||
getMaxAnimationTimeMs(durationMs = MAX_ANIMATION_TIME) {
|
|
||||||
if (
|
|
||||||
isTesting() ||
|
|
||||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
||||||
) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return Math.min(durationMs, MAX_ANIMATION_TIME);
|
|
||||||
}
|
|
||||||
|
|
||||||
//functions to calculate if a swipe should close
|
|
||||||
//based on origin of right, left, top, bottom
|
|
||||||
// menu should close after a swipe either:
|
|
||||||
// if a user moved the panel closed past a threshold and away and is NOT swiping back open
|
|
||||||
// if a user swiped to close fast enough regardless of distance
|
|
||||||
shouldCloseMenu(e, origin) {
|
|
||||||
if (origin === "right") {
|
|
||||||
return (
|
|
||||||
(e.deltaX > SWIPE_DISTANCE_THRESHOLD &&
|
|
||||||
e.velocityX > -SWIPE_VELOCITY_THRESHOLD) ||
|
|
||||||
e.velocityX > 0
|
|
||||||
);
|
|
||||||
} else if (origin === "left") {
|
|
||||||
return (
|
|
||||||
(e.deltaX < -SWIPE_DISTANCE_THRESHOLD &&
|
|
||||||
e.velocityX < SWIPE_VELOCITY_THRESHOLD) ||
|
|
||||||
e.velocityX < 0
|
|
||||||
);
|
|
||||||
} else if (origin === "bottom") {
|
|
||||||
return (
|
|
||||||
(e.deltaY > SWIPE_DISTANCE_THRESHOLD &&
|
|
||||||
e.velocityY > -SWIPE_VELOCITY_THRESHOLD) ||
|
|
||||||
e.velocityY > 0
|
|
||||||
);
|
|
||||||
} else if (origin === "top") {
|
|
||||||
return (
|
|
||||||
(e.deltaY < -SWIPE_DISTANCE_THRESHOLD &&
|
|
||||||
e.velocityY < SWIPE_VELOCITY_THRESHOLD) ||
|
|
||||||
e.velocityY < 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
#calculateDirection(oldState, deltaX, deltaY) {
|
#calculateDirection(oldState, deltaX, deltaY) {
|
||||||
if (oldState.start || !oldState.direction) {
|
if (oldState.start || !oldState.direction) {
|
||||||
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
||||||
|
@ -155,6 +155,7 @@ export default class SwipeEvents {
|
||||||
const eventDeltaY = e.clientY - oldState.center.y;
|
const eventDeltaY = e.clientY - oldState.center.y;
|
||||||
const velocityX = eventDeltaX / timeDiffSeconds;
|
const velocityX = eventDeltaX / timeDiffSeconds;
|
||||||
const velocityY = eventDeltaY / timeDiffSeconds;
|
const velocityY = eventDeltaY / timeDiffSeconds;
|
||||||
|
const direction = this.#calculateDirection(oldState, deltaX, deltaY);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
startLocation: oldState.startLocation,
|
startLocation: oldState.startLocation,
|
||||||
|
@ -165,12 +166,15 @@ export default class SwipeEvents {
|
||||||
deltaY,
|
deltaY,
|
||||||
start: false,
|
start: false,
|
||||||
timestamp: newTimestamp,
|
timestamp: newTimestamp,
|
||||||
direction: this.#calculateDirection(oldState, deltaX, deltaY),
|
direction,
|
||||||
|
element: this.element,
|
||||||
|
goingUp: () => direction === "up",
|
||||||
|
goingDown: () => direction === "down",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#swipeStart(e) {
|
#swipeStart(e) {
|
||||||
const newState = {
|
return {
|
||||||
center: { x: e.clientX, y: e.clientY },
|
center: { x: e.clientX, y: e.clientY },
|
||||||
startLocation: { x: e.clientX, y: e.clientY },
|
startLocation: { x: e.clientX, y: e.clientY },
|
||||||
velocityX: 0,
|
velocityX: 0,
|
||||||
|
@ -180,8 +184,10 @@ export default class SwipeEvents {
|
||||||
start: true,
|
start: true,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
direction: null,
|
direction: null,
|
||||||
|
element: this.element,
|
||||||
|
goingUp: () => false,
|
||||||
|
goingDown: () => false,
|
||||||
};
|
};
|
||||||
this.swipeState = newState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#swipeMove(e, originalEvent) {
|
#swipeMove(e, originalEvent) {
|
||||||
|
@ -189,7 +195,7 @@ export default class SwipeEvents {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.swipeState) {
|
if (!this.swipeState) {
|
||||||
this.#swipeStart(e);
|
this.swipeState = this.#swipeStart(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { registerDestructor } from "@ember/destroyable";
|
import { registerDestructor } from "@ember/destroyable";
|
||||||
|
import { service } from "@ember/service";
|
||||||
import Modifier from "ember-modifier";
|
import Modifier from "ember-modifier";
|
||||||
import {
|
import {
|
||||||
disableBodyScroll,
|
disableBodyScroll,
|
||||||
enableBodyScroll,
|
enableBodyScroll,
|
||||||
} from "discourse/lib/body-scroll-lock";
|
} from "discourse/lib/body-scroll-lock";
|
||||||
|
import SwipeEvents from "discourse/lib/swipe-events";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A modifier for handling swipe gestures on an element.
|
* A modifier for handling swipe gestures on an element.
|
||||||
*
|
*
|
||||||
|
@ -16,136 +17,122 @@ import { bind } from "discourse-common/utils/decorators";
|
||||||
* with the current state of the swipe, including its direction, orientation, and delta values.
|
* with the current state of the swipe, including its direction, orientation, and delta values.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* <div {{swipe didStartSwipe=this.didStartSwipe
|
* <div {{swipe
|
||||||
* didSwipe=this.didSwipe
|
* onDidStartSwipe=this.onDidStartSwipe
|
||||||
* didEndSwipe=this.didEndSwipe}}>
|
* onDidSwipe=this.onDidSwipe
|
||||||
|
* onDidEndSwipe=this.onDidEndSwipe
|
||||||
|
* onDidCancelSwipe=this.onDidCancelSwipe
|
||||||
|
* }}
|
||||||
|
* >
|
||||||
* Swipe here
|
* Swipe here
|
||||||
* </div>
|
* </div>
|
||||||
*
|
*
|
||||||
* @extends Modifier
|
* @extends Modifier
|
||||||
*/
|
*/
|
||||||
export default class SwipeModifier extends Modifier {
|
|
||||||
/**
|
|
||||||
* The DOM element the modifier is attached to.
|
|
||||||
* @type {Element}
|
|
||||||
*/
|
|
||||||
element;
|
|
||||||
enabled = true;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SwipeModifier class.
|
||||||
|
*/
|
||||||
|
export default class SwipeModifier extends Modifier {
|
||||||
|
@service site;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance of SwipeModifier.
|
||||||
|
* @param {Owner} owner - The owner.
|
||||||
|
* @param {Object} args - The arguments.
|
||||||
|
*/
|
||||||
constructor(owner, args) {
|
constructor(owner, args) {
|
||||||
super(owner, args);
|
super(owner, args);
|
||||||
registerDestructor(this, (instance) => instance.cleanup());
|
registerDestructor(this, (instance) => instance.cleanup());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the modifier by attaching event listeners for touch events to the element.
|
* Modifies the element for swipe functionality.
|
||||||
*
|
* @param {HTMLElement} element - The element to modify.
|
||||||
* @param {Element} element The DOM element to which the modifier is applied.
|
* @param {*} _ - Unused argument.
|
||||||
* @param {unused} _ Unused parameter, placeholder for positional arguments.
|
* @param {Object} options - Options for modifying the swipe behavior.
|
||||||
* @param {Object} options The named arguments passed to the modifier.
|
* @param {Function} options.onDidStartSwipe - Callback function when swipe starts.
|
||||||
* @param {Function} options.didStartSwipe Callback to be executed when a swipe starts.
|
* @param {Function} options.onDidSwipe - Callback function when swipe occurs.
|
||||||
* @param {Function} options.didSwipe Callback to be executed when a swipe moves.
|
* @param {Function} options.onDidEndSwipe - Callback function when swipe ends.
|
||||||
* @param {Function} options.didEndSwipe Callback to be executed when a swipe ends.
|
* @param {Function} options.onDidCancelSwipe - Callback function when swipe is canceled.
|
||||||
* @param {Boolean} options.enabled Enable or disable the swipe modifier.
|
* @param {boolean} options.enabled - Flag to enable/disable swipe.
|
||||||
*/
|
*/
|
||||||
modify(element, _, { didStartSwipe, didSwipe, didEndSwipe, enabled }) {
|
modify(
|
||||||
if (enabled === false) {
|
element,
|
||||||
|
_,
|
||||||
|
{ onDidStartSwipe, onDidSwipe, onDidEndSwipe, onDidCancelSwipe, enabled }
|
||||||
|
) {
|
||||||
|
if (enabled === false || !this.site.mobileView) {
|
||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.element = element;
|
this.element = element;
|
||||||
this.didSwipeCallback = didSwipe;
|
this.onDidSwipeCallback = onDidSwipe;
|
||||||
this.didStartSwipeCallback = didStartSwipe;
|
this.onDidStartSwipeCallback = onDidStartSwipe;
|
||||||
this.didEndSwipeCallback = didEndSwipe;
|
this.onDidCancelSwipeCallback = onDidCancelSwipe;
|
||||||
|
this.onDidEndSwipeCallback = onDidEndSwipe;
|
||||||
|
|
||||||
element.addEventListener("touchstart", this.handleTouchStart, {
|
this._swipeEvents = new SwipeEvents(this.element);
|
||||||
passive: true,
|
this._swipeEvents.addTouchListeners();
|
||||||
});
|
this.element.addEventListener("swipestart", this.onDidStartSwipe);
|
||||||
element.addEventListener("touchmove", this.handleTouchMove, {
|
this.element.addEventListener("swipeend", this.onDidEndSwipe);
|
||||||
passive: true,
|
this.element.addEventListener("swipecancel", this.onDidCancelSwipe);
|
||||||
});
|
this.element.addEventListener("swipe", this.onDidSwipe);
|
||||||
element.addEventListener("touchend", this.handleTouchEnd, {
|
|
||||||
passive: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the touchstart event.
|
* Handler for swipe start event.
|
||||||
* Initializes the swipe state and executes the `didStartSwipe` callback.
|
* @param {Event} event - The swipe start event.
|
||||||
*
|
|
||||||
* @param {TouchEvent} event The touchstart event object.
|
|
||||||
*/
|
*/
|
||||||
@bind
|
@bind
|
||||||
handleTouchStart(event) {
|
onDidStartSwipe(event) {
|
||||||
disableBodyScroll(this.element);
|
disableBodyScroll(this.element);
|
||||||
|
this.onDidStartSwipeCallback?.(event.detail);
|
||||||
this.state = {
|
|
||||||
initialY: event.touches[0].clientY,
|
|
||||||
initialX: event.touches[0].clientX,
|
|
||||||
deltaY: 0,
|
|
||||||
deltaX: 0,
|
|
||||||
direction: null,
|
|
||||||
orientation: null,
|
|
||||||
element: this.element,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.didStartSwipeCallback?.(this.state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the touchend event.
|
* Handler for swipe end event.
|
||||||
* Executes the `didEndSwipe` callback.
|
* @param {Event} event - The swipe end event.
|
||||||
*
|
|
||||||
* @param {TouchEvent} event The touchend event object.
|
|
||||||
*/
|
*/
|
||||||
@bind
|
@bind
|
||||||
handleTouchEnd() {
|
onDidEndSwipe() {
|
||||||
enableBodyScroll(this.element);
|
enableBodyScroll(this.element);
|
||||||
|
this.onDidEndSwipeCallback?.(event.detail);
|
||||||
this.didEndSwipeCallback?.(this.state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the touchmove event.
|
* Handler for swipe event.
|
||||||
* Updates the swipe state based on movement and executes the `didSwipe` callback.
|
* @param {Event} event - The swipe event.
|
||||||
*
|
|
||||||
* @param {TouchEvent} event The touchmove event object.
|
|
||||||
*/
|
*/
|
||||||
@bind
|
@bind
|
||||||
handleTouchMove(event) {
|
onDidSwipe(event) {
|
||||||
const touch = event.touches[0];
|
this.onDidSwipeCallback?.(event.detail);
|
||||||
const deltaY = this.state.initialY - touch.clientY;
|
|
||||||
const deltaX = this.state.initialX - touch.clientX;
|
|
||||||
|
|
||||||
this.state.direction =
|
|
||||||
Math.abs(deltaY) > Math.abs(deltaX) ? "vertical" : "horizontal";
|
|
||||||
this.state.orientation =
|
|
||||||
this.state.direction === "vertical"
|
|
||||||
? deltaY > 0
|
|
||||||
? "up"
|
|
||||||
: "down"
|
|
||||||
: deltaX > 0
|
|
||||||
? "left"
|
|
||||||
: "right";
|
|
||||||
|
|
||||||
this.state.deltaY = deltaY;
|
|
||||||
this.state.deltaX = deltaX;
|
|
||||||
|
|
||||||
this.didSwipeCallback?.(this.state);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleans up the modifier by removing event listeners from the element.
|
* Handler for swipe cancel event.
|
||||||
|
* @param {Event} event - The swipe cancel event.
|
||||||
|
*/
|
||||||
|
@bind
|
||||||
|
onDidCancelSwipe(event) {
|
||||||
|
enableBodyScroll(this.element);
|
||||||
|
this.onDidCancelSwipe?.(event.detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up the swipe modifier.
|
||||||
*/
|
*/
|
||||||
cleanup() {
|
cleanup() {
|
||||||
if (!this.enabled) {
|
if (!this.enabled || !this.element || !this._swipeEvents) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.element?.removeEventListener("touchstart", this.handleTouchStart);
|
this.element.removeEventListener("swipestart", this.onDidStartSwipe);
|
||||||
this.element?.removeEventListener("touchmove", this.handleTouchMove);
|
this.element.removeEventListener("swipeend", this.onDidEndSwipe);
|
||||||
this.element?.removeEventListener("touchend", this.handleTouchEnd);
|
this.element.removeEventListener("swipecancel", this.onDidCancelSwipe);
|
||||||
|
this.element.removeEventListener("swipe", this.onDidSwipe);
|
||||||
|
this._swipeEvents.removeTouchListeners();
|
||||||
|
|
||||||
enableBodyScroll(this.element);
|
enableBodyScroll(this.element);
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@ module("Integration | Component | FloatKit | d-toast", function (hooks) {
|
||||||
});
|
});
|
||||||
|
|
||||||
await triggerEvent(TOAST_SELECTOR, "touchend", {
|
await triggerEvent(TOAST_SELECTOR, "touchend", {
|
||||||
|
touches: [{ clientX: 0, clientY: -100 }],
|
||||||
changedTouches: [{ clientX: 0, clientY: -100 }],
|
changedTouches: [{ clientX: 0, clientY: -100 }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { render, triggerEvent } from "@ember/test-helpers";
|
import { getOwner } from "@ember/application";
|
||||||
|
import { clearRender, render, triggerEvent } from "@ember/test-helpers";
|
||||||
import { setupRenderingTest } from "ember-qunit";
|
import { setupRenderingTest } from "ember-qunit";
|
||||||
import hbs from "htmlbars-inline-precompile";
|
import hbs from "htmlbars-inline-precompile";
|
||||||
import { module, test } from "qunit";
|
import { module, test } from "qunit";
|
||||||
|
@ -6,16 +7,47 @@ import { module, test } from "qunit";
|
||||||
module("Integration | Modifier | swipe", function (hooks) {
|
module("Integration | Modifier | swipe", function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
test("it calls didStartSwipe on touchstart", async function (assert) {
|
hooks.beforeEach(function () {
|
||||||
|
getOwner(this).lookup("service:site").mobileView = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function swipe() {
|
||||||
|
await triggerEvent("div", "touchstart", {
|
||||||
|
changedTouches: [{ screenX: 0, screenY: 0 }],
|
||||||
|
touches: [{ clientX: 0, clientY: 0 }],
|
||||||
|
});
|
||||||
|
await triggerEvent("div", "touchmove", {
|
||||||
|
changedTouches: [{ screenX: 2, screenY: 2 }],
|
||||||
|
touches: [{ clientX: 2, clientY: 2 }],
|
||||||
|
});
|
||||||
|
await triggerEvent("div", "touchmove", {
|
||||||
|
changedTouches: [{ screenX: 4, screenY: 4 }],
|
||||||
|
touches: [{ clientX: 4, clientY: 4 }],
|
||||||
|
});
|
||||||
|
await triggerEvent("div", "touchmove", {
|
||||||
|
changedTouches: [{ screenX: 7, screenY: 7 }],
|
||||||
|
touches: [{ clientX: 7, clientY: 7 }],
|
||||||
|
});
|
||||||
|
await triggerEvent("div", "touchmove", {
|
||||||
|
changedTouches: [{ screenX: 9, screenY: 9 }],
|
||||||
|
touches: [{ clientX: 9, clientY: 9 }],
|
||||||
|
});
|
||||||
|
await triggerEvent("div", "touchend", {
|
||||||
|
changedTouches: [{ screenX: 10, screenY: 10 }],
|
||||||
|
touches: [{ clientX: 10, clientY: 10 }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("it calls onDidStartSwipe on touchstart", async function (assert) {
|
||||||
this.didStartSwipe = (state) => {
|
this.didStartSwipe = (state) => {
|
||||||
assert.ok(state, "didStartSwipe called with state");
|
assert.ok(state, "didStartSwipe called with state");
|
||||||
};
|
};
|
||||||
|
|
||||||
await render(hbs`<div {{swipe didStartSwipe=this.didStartSwipe}}></div>`);
|
await render(
|
||||||
|
hbs`<div {{swipe onDidStartSwipe=this.didStartSwipe}}>x</div>`
|
||||||
|
);
|
||||||
|
|
||||||
await triggerEvent("div", "touchstart", {
|
await swipe();
|
||||||
touches: [{ clientX: 0, clientY: 0 }],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("it calls didSwipe on touchmove", async function (assert) {
|
test("it calls didSwipe on touchmove", async function (assert) {
|
||||||
|
@ -23,16 +55,9 @@ module("Integration | Modifier | swipe", function (hooks) {
|
||||||
assert.ok(state, "didSwipe called with state");
|
assert.ok(state, "didSwipe called with state");
|
||||||
};
|
};
|
||||||
|
|
||||||
await render(hbs`<div {{swipe didSwipe=this.didSwipe}}></div>`);
|
await render(hbs`<div {{swipe onDidSwipe=this.didSwipe}}>x</div>`);
|
||||||
|
|
||||||
await triggerEvent("div", "touchstart", {
|
await swipe();
|
||||||
touches: [{ clientX: 0, clientY: 0 }],
|
|
||||||
changedTouches: [{ clientX: 0, clientY: 0 }],
|
|
||||||
});
|
|
||||||
|
|
||||||
await triggerEvent("div", "touchmove", {
|
|
||||||
touches: [{ clientX: 5, clientY: 5 }],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("it calls didEndSwipe on touchend", async function (assert) {
|
test("it calls didEndSwipe on touchend", async function (assert) {
|
||||||
|
@ -40,21 +65,9 @@ module("Integration | Modifier | swipe", function (hooks) {
|
||||||
assert.ok(state, "didEndSwipe called with state");
|
assert.ok(state, "didEndSwipe called with state");
|
||||||
};
|
};
|
||||||
|
|
||||||
await render(hbs`<div {{swipe didEndSwipe=this.didEndSwipe}}></div>`);
|
await render(hbs`<div {{swipe onDidEndSwipe=this.didEndSwipe}}>x</div>`);
|
||||||
|
|
||||||
await triggerEvent("div", "touchstart", {
|
await swipe();
|
||||||
touches: [{ clientX: 0, clientY: 0 }],
|
|
||||||
changedTouches: [{ clientX: 0, clientY: 0 }],
|
|
||||||
});
|
|
||||||
|
|
||||||
await triggerEvent("div", "touchmove", {
|
|
||||||
touches: [{ clientX: 10, clientY: 0 }],
|
|
||||||
changedTouches: [{ clientX: 10, clientY: 0 }],
|
|
||||||
});
|
|
||||||
|
|
||||||
await triggerEvent("div", "touchend", {
|
|
||||||
changedTouches: [{ clientX: 10, clientY: 0 }],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("it does not trigger when disabled", async function (assert) {
|
test("it does not trigger when disabled", async function (assert) {
|
||||||
|
@ -67,19 +80,27 @@ module("Integration | Modifier | swipe", function (hooks) {
|
||||||
this.set("isEnabled", false);
|
this.set("isEnabled", false);
|
||||||
|
|
||||||
await render(
|
await render(
|
||||||
hbs`<div {{swipe didStartSwipe=this.didStartSwipe enabled=this.isEnabled}}></div>`
|
hbs`<div {{swipe onDidStartSwipe=this.didStartSwipe enabled=this.isEnabled}}>x</div>`
|
||||||
);
|
);
|
||||||
|
|
||||||
await triggerEvent("div", "touchstart", {
|
await swipe();
|
||||||
touches: [{ clientX: 0, clientY: 0 }],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.set("isEnabled", true);
|
this.set("isEnabled", true);
|
||||||
|
|
||||||
await triggerEvent("div", "touchstart", {
|
await swipe();
|
||||||
touches: [{ clientX: 0, clientY: 0 }],
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.deepEqual(calls, 1, "didStartSwipe should be called once");
|
assert.deepEqual(calls, 1, "didStartSwipe should be called once");
|
||||||
|
|
||||||
|
await clearRender();
|
||||||
|
|
||||||
|
getOwner(this).lookup("service:site").mobileView = false;
|
||||||
|
|
||||||
|
await render(
|
||||||
|
hbs`<div {{swipe onDidStartSwipe=this.didStartSwipe enabled=this.isEnabled}}>x</div>`
|
||||||
|
);
|
||||||
|
|
||||||
|
await swipe();
|
||||||
|
|
||||||
|
assert.deepEqual(calls, 1, "swipe is not enabled on desktop");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,70 +1,63 @@
|
||||||
import Component from "@glimmer/component";
|
|
||||||
import { concat, fn, hash } from "@ember/helper";
|
import { concat, fn, hash } from "@ember/helper";
|
||||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||||
import { inject as service } from "@ember/service";
|
|
||||||
import { or } from "truth-helpers";
|
import { or } from "truth-helpers";
|
||||||
import DButton from "discourse/components/d-button";
|
import DButton from "discourse/components/d-button";
|
||||||
import concatClass from "discourse/helpers/concat-class";
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
import icon from "discourse-common/helpers/d-icon";
|
import icon from "discourse-common/helpers/d-icon";
|
||||||
|
|
||||||
export default class DDefaultToast extends Component {
|
const DDefaultToast = <template>
|
||||||
@service site;
|
<div
|
||||||
|
class={{concatClass
|
||||||
<template>
|
"fk-d-default-toast"
|
||||||
<div
|
(concat "-" (or @data.theme "default"))
|
||||||
class={{concatClass
|
}}
|
||||||
"fk-d-default-toast"
|
...attributes
|
||||||
(concat "-" (or @data.theme "default"))
|
>
|
||||||
}}
|
{{#if @showProgressBar}}
|
||||||
...attributes
|
<div
|
||||||
>
|
class="fk-d-default-toast__progress-bar"
|
||||||
{{#if @showProgressBar}}
|
{{didInsert @onRegisterProgressBar}}
|
||||||
<div
|
></div>
|
||||||
class="fk-d-default-toast__progress-bar"
|
{{/if}}
|
||||||
{{didInsert @onRegisterProgressBar}}
|
{{#if @data.icon}}
|
||||||
></div>
|
<div class="fk-d-default-toast__icon-container">
|
||||||
{{/if}}
|
{{icon @data.icon}}
|
||||||
{{#if @data.icon}}
|
</div>
|
||||||
<div class="fk-d-default-toast__icon-container">
|
{{/if}}
|
||||||
{{icon @data.icon}}
|
<div class="fk-d-default-toast__main-container">
|
||||||
</div>
|
<div class="fk-d-default-toast__texts">
|
||||||
{{/if}}
|
{{#if @data.title}}
|
||||||
<div class="fk-d-default-toast__main-container">
|
<div class="fk-d-default-toast__title">
|
||||||
<div class="fk-d-default-toast__texts">
|
{{@data.title}}
|
||||||
{{#if @data.title}}
|
</div>
|
||||||
<div class="fk-d-default-toast__title">
|
{{/if}}
|
||||||
{{@data.title}}
|
{{#if @data.message}}
|
||||||
</div>
|
<div class="fk-d-default-toast__message">
|
||||||
{{/if}}
|
{{@data.message}}
|
||||||
{{#if @data.message}}
|
|
||||||
<div class="fk-d-default-toast__message">
|
|
||||||
{{@data.message}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if @data.actions}}
|
|
||||||
<div class="fk-d-default-toast__actions">
|
|
||||||
{{#each @data.actions as |toastAction|}}
|
|
||||||
{{#if toastAction.action}}
|
|
||||||
<DButton
|
|
||||||
@icon={{toastAction.icon}}
|
|
||||||
@translatedLabel={{toastAction.label}}
|
|
||||||
@action={{fn
|
|
||||||
toastAction.action
|
|
||||||
(hash data=@data close=@close)
|
|
||||||
}}
|
|
||||||
class={{toastAction.class}}
|
|
||||||
tabindex="0"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
<div class="fk-d-default-toast__close-container">
|
|
||||||
<DButton class="btn-transparent" @icon="times" @action={{@close}} />
|
{{#if @data.actions}}
|
||||||
</div>
|
<div class="fk-d-default-toast__actions">
|
||||||
|
{{#each @data.actions as |toastAction|}}
|
||||||
|
{{#if toastAction.action}}
|
||||||
|
<DButton
|
||||||
|
@icon={{toastAction.icon}}
|
||||||
|
@translatedLabel={{toastAction.label}}
|
||||||
|
@action={{fn toastAction.action (hash data=@data close=@close)}}
|
||||||
|
class={{toastAction.class}}
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<div class="fk-d-default-toast__close-container">
|
||||||
}
|
<DButton class="btn-transparent" @icon="times" @action={{@close}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>;
|
||||||
|
|
||||||
|
export default DDefaultToast;
|
||||||
|
|
|
@ -4,45 +4,40 @@ import { action } from "@ember/object";
|
||||||
import { service } from "@ember/service";
|
import { service } from "@ember/service";
|
||||||
import { and } from "truth-helpers";
|
import { and } from "truth-helpers";
|
||||||
import concatClass from "discourse/helpers/concat-class";
|
import concatClass from "discourse/helpers/concat-class";
|
||||||
|
import { getMaxAnimationTimeMs } from "discourse/lib/swipe-events";
|
||||||
import swipe from "discourse/modifiers/swipe";
|
import swipe from "discourse/modifiers/swipe";
|
||||||
import autoCloseToast from "float-kit/modifiers/auto-close-toast";
|
import autoCloseToast from "float-kit/modifiers/auto-close-toast";
|
||||||
|
|
||||||
const CLOSE_SWIPE_THRESHOLD = 50;
|
const VELOCITY_THRESHOLD = -1.2;
|
||||||
|
|
||||||
export default class DToast extends Component {
|
export default class DToast extends Component {
|
||||||
@service site;
|
@service site;
|
||||||
|
|
||||||
@tracked progressBar;
|
@tracked progressBar;
|
||||||
|
|
||||||
animating = false;
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
registerProgressBar(element) {
|
registerProgressBar(element) {
|
||||||
this.progressBar = element;
|
this.progressBar = element;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async handleSwipe(state) {
|
async didSwipe(state) {
|
||||||
if (this.animating) {
|
if (state.deltaY >= 0) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.deltaY < 0) {
|
|
||||||
this.#animateWrapperPosition(state.element, 0);
|
this.#animateWrapperPosition(state.element, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.deltaY > CLOSE_SWIPE_THRESHOLD) {
|
if (state.velocityY < VELOCITY_THRESHOLD) {
|
||||||
this.#close(state.element);
|
await this.#close(state.element);
|
||||||
} else {
|
} else {
|
||||||
await this.#animateWrapperPosition(state.element, state.deltaY);
|
await this.#animateWrapperPosition(state.element, state.deltaY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async handleSwipeEnded(state) {
|
async didEndSwipe(state) {
|
||||||
if (state.deltaY > CLOSE_SWIPE_THRESHOLD) {
|
if (state.velocityY < VELOCITY_THRESHOLD) {
|
||||||
this.#close(state.element);
|
await this.#close(state.element);
|
||||||
} else {
|
} else {
|
||||||
await this.#animateWrapperPosition(state.element, 0);
|
await this.#animateWrapperPosition(state.element, 0);
|
||||||
}
|
}
|
||||||
|
@ -54,24 +49,16 @@ export default class DToast extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
async #closeWrapperAnimation(element) {
|
async #closeWrapperAnimation(element) {
|
||||||
this.animating = true;
|
|
||||||
|
|
||||||
await element.animate([{ transform: "translateY(-150px)" }], {
|
await element.animate([{ transform: "translateY(-150px)" }], {
|
||||||
fill: "forwards",
|
fill: "forwards",
|
||||||
duration: 250,
|
duration: getMaxAnimationTimeMs(),
|
||||||
}).finished;
|
}).finished;
|
||||||
|
|
||||||
this.animating = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async #animateWrapperPosition(element, position) {
|
async #animateWrapperPosition(element, position) {
|
||||||
this.animating = true;
|
await element.animate([{ transform: `translateY(${position}px)` }], {
|
||||||
|
|
||||||
await element.animate([{ transform: `translateY(${-position}px)` }], {
|
|
||||||
fill: "forwards",
|
fill: "forwards",
|
||||||
}).finished;
|
}).finished;
|
||||||
|
|
||||||
this.animating = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -85,11 +72,7 @@ export default class DToast extends Component {
|
||||||
progressBar=this.progressBar
|
progressBar=this.progressBar
|
||||||
enabled=@toast.options.autoClose
|
enabled=@toast.options.autoClose
|
||||||
}}
|
}}
|
||||||
{{swipe
|
{{swipe onDidSwipe=this.didSwipe onDidEndSwipe=this.didEndSwipe}}
|
||||||
didSwipe=this.handleSwipe
|
|
||||||
didEndSwipe=this.handleSwipeEnded
|
|
||||||
enabled=this.site.mobileView
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<@toast.options.component
|
<@toast.options.component
|
||||||
@data={{@toast.options.data}}
|
@data={{@toast.options.data}}
|
||||||
|
|
|
@ -33,6 +33,10 @@ export default class AutoCloseToast extends Modifier {
|
||||||
this.duration = duration;
|
this.duration = duration;
|
||||||
this.timeRemaining = duration;
|
this.timeRemaining = duration;
|
||||||
this.progressBar = progressBar;
|
this.progressBar = progressBar;
|
||||||
|
this.element.addEventListener("touchstart", this.stopTimer, {
|
||||||
|
passive: true,
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
this.element.addEventListener("mouseenter", this.stopTimer, {
|
this.element.addEventListener("mouseenter", this.stopTimer, {
|
||||||
passive: true,
|
passive: true,
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Block a user