+ *
* Swipe here
*
*
* @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) {
super(owner, args);
registerDestructor(this, (instance) => instance.cleanup());
}
/**
- * Sets up the modifier by attaching event listeners for touch events to the element.
- *
- * @param {Element} element The DOM element to which the modifier is applied.
- * @param {unused} _ Unused parameter, placeholder for positional arguments.
- * @param {Object} options The named arguments passed to the modifier.
- * @param {Function} options.didStartSwipe Callback to be executed when a swipe starts.
- * @param {Function} options.didSwipe Callback to be executed when a swipe moves.
- * @param {Function} options.didEndSwipe Callback to be executed when a swipe ends.
- * @param {Boolean} options.enabled Enable or disable the swipe modifier.
+ * Modifies the element for swipe functionality.
+ * @param {HTMLElement} element - The element to modify.
+ * @param {*} _ - Unused argument.
+ * @param {Object} options - Options for modifying the swipe behavior.
+ * @param {Function} options.onDidStartSwipe - Callback function when swipe starts.
+ * @param {Function} options.onDidSwipe - Callback function when swipe occurs.
+ * @param {Function} options.onDidEndSwipe - Callback function when swipe ends.
+ * @param {Function} options.onDidCancelSwipe - Callback function when swipe is canceled.
+ * @param {boolean} options.enabled - Flag to enable/disable swipe.
*/
- modify(element, _, { didStartSwipe, didSwipe, didEndSwipe, enabled }) {
- if (enabled === false) {
+ modify(
+ element,
+ _,
+ { onDidStartSwipe, onDidSwipe, onDidEndSwipe, onDidCancelSwipe, enabled }
+ ) {
+ if (enabled === false || !this.site.mobileView) {
this.enabled = enabled;
return;
}
this.element = element;
- this.didSwipeCallback = didSwipe;
- this.didStartSwipeCallback = didStartSwipe;
- this.didEndSwipeCallback = didEndSwipe;
+ this.onDidSwipeCallback = onDidSwipe;
+ this.onDidStartSwipeCallback = onDidStartSwipe;
+ this.onDidCancelSwipeCallback = onDidCancelSwipe;
+ this.onDidEndSwipeCallback = onDidEndSwipe;
- element.addEventListener("touchstart", this.handleTouchStart, {
- passive: true,
- });
- element.addEventListener("touchmove", this.handleTouchMove, {
- passive: true,
- });
- element.addEventListener("touchend", this.handleTouchEnd, {
- passive: true,
- });
+ this._swipeEvents = new SwipeEvents(this.element);
+ this._swipeEvents.addTouchListeners();
+ this.element.addEventListener("swipestart", this.onDidStartSwipe);
+ this.element.addEventListener("swipeend", this.onDidEndSwipe);
+ this.element.addEventListener("swipecancel", this.onDidCancelSwipe);
+ this.element.addEventListener("swipe", this.onDidSwipe);
}
/**
- * Handles the touchstart event.
- * Initializes the swipe state and executes the `didStartSwipe` callback.
- *
- * @param {TouchEvent} event The touchstart event object.
+ * Handler for swipe start event.
+ * @param {Event} event - The swipe start event.
*/
@bind
- handleTouchStart(event) {
+ onDidStartSwipe(event) {
disableBodyScroll(this.element);
-
- 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);
+ this.onDidStartSwipeCallback?.(event.detail);
}
/**
- * Handles the touchend event.
- * Executes the `didEndSwipe` callback.
- *
- * @param {TouchEvent} event The touchend event object.
+ * Handler for swipe end event.
+ * @param {Event} event - The swipe end event.
*/
@bind
- handleTouchEnd() {
+ onDidEndSwipe() {
enableBodyScroll(this.element);
-
- this.didEndSwipeCallback?.(this.state);
+ this.onDidEndSwipeCallback?.(event.detail);
}
/**
- * Handles the touchmove event.
- * Updates the swipe state based on movement and executes the `didSwipe` callback.
- *
- * @param {TouchEvent} event The touchmove event object.
+ * Handler for swipe event.
+ * @param {Event} event - The swipe event.
*/
@bind
- handleTouchMove(event) {
- const touch = event.touches[0];
- 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);
+ onDidSwipe(event) {
+ this.onDidSwipeCallback?.(event.detail);
}
/**
- * 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() {
- if (!this.enabled) {
+ if (!this.enabled || !this.element || !this._swipeEvents) {
return;
}
- this.element?.removeEventListener("touchstart", this.handleTouchStart);
- this.element?.removeEventListener("touchmove", this.handleTouchMove);
- this.element?.removeEventListener("touchend", this.handleTouchEnd);
+ this.element.removeEventListener("swipestart", this.onDidStartSwipe);
+ this.element.removeEventListener("swipeend", this.onDidEndSwipe);
+ this.element.removeEventListener("swipecancel", this.onDidCancelSwipe);
+ this.element.removeEventListener("swipe", this.onDidSwipe);
+ this._swipeEvents.removeTouchListeners();
enableBodyScroll(this.element);
}
diff --git a/app/assets/javascripts/discourse/tests/integration/components/float-kit/d-toast-test.gjs b/app/assets/javascripts/discourse/tests/integration/components/float-kit/d-toast-test.gjs
index 6f6699073ed..0614d67e811 100644
--- a/app/assets/javascripts/discourse/tests/integration/components/float-kit/d-toast-test.gjs
+++ b/app/assets/javascripts/discourse/tests/integration/components/float-kit/d-toast-test.gjs
@@ -48,6 +48,7 @@ module("Integration | Component | FloatKit | d-toast", function (hooks) {
});
await triggerEvent(TOAST_SELECTOR, "touchend", {
+ touches: [{ clientX: 0, clientY: -100 }],
changedTouches: [{ clientX: 0, clientY: -100 }],
});
diff --git a/app/assets/javascripts/discourse/tests/modifiers/swipe-test.gjs b/app/assets/javascripts/discourse/tests/modifiers/swipe-test.gjs
index e512af801c5..5abfaa8218d 100644
--- a/app/assets/javascripts/discourse/tests/modifiers/swipe-test.gjs
+++ b/app/assets/javascripts/discourse/tests/modifiers/swipe-test.gjs
@@ -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 hbs from "htmlbars-inline-precompile";
import { module, test } from "qunit";
@@ -6,16 +7,47 @@ import { module, test } from "qunit";
module("Integration | Modifier | swipe", function (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) => {
assert.ok(state, "didStartSwipe called with state");
};
- await render(hbs`
`);
+ await render(
+ hbs`
x
`
+ );
- await triggerEvent("div", "touchstart", {
- touches: [{ clientX: 0, clientY: 0 }],
- });
+ await swipe();
});
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");
};
- await render(hbs`
`);
+ await render(hbs`
x
`);
- await triggerEvent("div", "touchstart", {
- touches: [{ clientX: 0, clientY: 0 }],
- changedTouches: [{ clientX: 0, clientY: 0 }],
- });
-
- await triggerEvent("div", "touchmove", {
- touches: [{ clientX: 5, clientY: 5 }],
- });
+ await swipe();
});
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");
};
- await render(hbs`
`);
+ await render(hbs`
x
`);
- await triggerEvent("div", "touchstart", {
- 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 }],
- });
+ await swipe();
});
test("it does not trigger when disabled", async function (assert) {
@@ -67,19 +80,27 @@ module("Integration | Modifier | swipe", function (hooks) {
this.set("isEnabled", false);
await render(
- hbs`
`
+ hbs`
x
`
);
- await triggerEvent("div", "touchstart", {
- touches: [{ clientX: 0, clientY: 0 }],
- });
+ await swipe();
this.set("isEnabled", true);
- await triggerEvent("div", "touchstart", {
- touches: [{ clientX: 0, clientY: 0 }],
- });
+ await swipe();
assert.deepEqual(calls, 1, "didStartSwipe should be called once");
+
+ await clearRender();
+
+ getOwner(this).lookup("service:site").mobileView = false;
+
+ await render(
+ hbs`
x
`
+ );
+
+ await swipe();
+
+ assert.deepEqual(calls, 1, "swipe is not enabled on desktop");
});
});
diff --git a/app/assets/javascripts/float-kit/addon/components/d-default-toast.gjs b/app/assets/javascripts/float-kit/addon/components/d-default-toast.gjs
index 087fc53ac7c..8d89f00fa03 100644
--- a/app/assets/javascripts/float-kit/addon/components/d-default-toast.gjs
+++ b/app/assets/javascripts/float-kit/addon/components/d-default-toast.gjs
@@ -1,70 +1,63 @@
-import Component from "@glimmer/component";
import { concat, fn, hash } from "@ember/helper";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
-import { inject as service } from "@ember/service";
import { or } from "truth-helpers";
import DButton from "discourse/components/d-button";
import concatClass from "discourse/helpers/concat-class";
import icon from "discourse-common/helpers/d-icon";
-export default class DDefaultToast extends Component {
- @service site;
-
-
-
- {{#if @showProgressBar}}
-
- {{/if}}
- {{#if @data.icon}}
-
- {{icon @data.icon}}
-
- {{/if}}
-
-
- {{#if @data.title}}
-
- {{@data.title}}
-
- {{/if}}
- {{#if @data.message}}
-
- {{@data.message}}
-
- {{/if}}
-
-
- {{#if @data.actions}}
-
- {{#each @data.actions as |toastAction|}}
- {{#if toastAction.action}}
-
- {{/if}}
- {{/each}}
+const DDefaultToast =
+
+ {{#if @showProgressBar}}
+
+ {{/if}}
+ {{#if @data.icon}}
+
+ {{icon @data.icon}}
+
+ {{/if}}
+
+
+ {{#if @data.title}}
+
+ {{@data.title}}
+
+ {{/if}}
+ {{#if @data.message}}
+
+ {{@data.message}}
{{/if}}
-
-
-
+
+ {{#if @data.actions}}
+
+ {{#each @data.actions as |toastAction|}}
+ {{#if toastAction.action}}
+
+ {{/if}}
+ {{/each}}
+
+ {{/if}}
-
-}
+
+
+
+
+;
+
+export default DDefaultToast;
diff --git a/app/assets/javascripts/float-kit/addon/components/d-toast.gjs b/app/assets/javascripts/float-kit/addon/components/d-toast.gjs
index 7d97a3551ce..c939acf8234 100644
--- a/app/assets/javascripts/float-kit/addon/components/d-toast.gjs
+++ b/app/assets/javascripts/float-kit/addon/components/d-toast.gjs
@@ -4,45 +4,40 @@ import { action } from "@ember/object";
import { service } from "@ember/service";
import { and } from "truth-helpers";
import concatClass from "discourse/helpers/concat-class";
+import { getMaxAnimationTimeMs } from "discourse/lib/swipe-events";
import swipe from "discourse/modifiers/swipe";
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 {
@service site;
@tracked progressBar;
- animating = false;
-
@action
registerProgressBar(element) {
this.progressBar = element;
}
@action
- async handleSwipe(state) {
- if (this.animating) {
- return;
- }
-
- if (state.deltaY < 0) {
+ async didSwipe(state) {
+ if (state.deltaY >= 0) {
this.#animateWrapperPosition(state.element, 0);
return;
}
- if (state.deltaY > CLOSE_SWIPE_THRESHOLD) {
- this.#close(state.element);
+ if (state.velocityY < VELOCITY_THRESHOLD) {
+ await this.#close(state.element);
} else {
await this.#animateWrapperPosition(state.element, state.deltaY);
}
}
@action
- async handleSwipeEnded(state) {
- if (state.deltaY > CLOSE_SWIPE_THRESHOLD) {
- this.#close(state.element);
+ async didEndSwipe(state) {
+ if (state.velocityY < VELOCITY_THRESHOLD) {
+ await this.#close(state.element);
} else {
await this.#animateWrapperPosition(state.element, 0);
}
@@ -54,24 +49,16 @@ export default class DToast extends Component {
}
async #closeWrapperAnimation(element) {
- this.animating = true;
-
await element.animate([{ transform: "translateY(-150px)" }], {
fill: "forwards",
- duration: 250,
+ duration: getMaxAnimationTimeMs(),
}).finished;
-
- this.animating = false;
}
async #animateWrapperPosition(element, position) {
- this.animating = true;
-
- await element.animate([{ transform: `translateY(${-position}px)` }], {
+ await element.animate([{ transform: `translateY(${position}px)` }], {
fill: "forwards",
}).finished;
-
- this.animating = false;
}
@@ -85,11 +72,7 @@ export default class DToast extends Component {
progressBar=this.progressBar
enabled=@toast.options.autoClose
}}
- {{swipe
- didSwipe=this.handleSwipe
- didEndSwipe=this.handleSwipeEnded
- enabled=this.site.mobileView
- }}
+ {{swipe onDidSwipe=this.didSwipe onDidEndSwipe=this.didEndSwipe}}
>
<@toast.options.component
@data={{@toast.options.data}}
diff --git a/app/assets/javascripts/float-kit/addon/modifiers/auto-close-toast.js b/app/assets/javascripts/float-kit/addon/modifiers/auto-close-toast.js
index df2930d48b9..bdba438f249 100644
--- a/app/assets/javascripts/float-kit/addon/modifiers/auto-close-toast.js
+++ b/app/assets/javascripts/float-kit/addon/modifiers/auto-close-toast.js
@@ -33,6 +33,10 @@ export default class AutoCloseToast extends Modifier {
this.duration = duration;
this.timeRemaining = duration;
this.progressBar = progressBar;
+ this.element.addEventListener("touchstart", this.stopTimer, {
+ passive: true,
+ once: true,
+ });
this.element.addEventListener("mouseenter", this.stopTimer, {
passive: true,
});