mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-08-28 09:25:17 +00:00
Update Toast component with swipe functionality for dismissing notifications
This commit is contained in:
@@ -8,8 +8,14 @@
|
||||
<div
|
||||
v-for="toast in toasts"
|
||||
:key="toast.id"
|
||||
class="pointer-events-auto flex items-center p-4 w-full sm:min-w-[300px] sm:max-w-md rounded-xl shadow-lg border backdrop-blur-md transition-all duration-300"
|
||||
:class="toastClass(toast.type)"
|
||||
ref="toastRefs"
|
||||
class="pointer-events-auto flex items-center p-4 w-full sm:min-w-[300px] sm:max-w-md rounded-xl shadow-lg border backdrop-blur-md transition-all duration-300 select-none touch-pan-y"
|
||||
:class="[toastClass(toast.type), toast.swipeClass]"
|
||||
:style="toastSwipeStyle(toast)"
|
||||
@touchstart="onTouchStart($event, toast)"
|
||||
@touchmove="onTouchMove($event, toast)"
|
||||
@touchend="onTouchEnd(toast)"
|
||||
@touchcancel="onTouchEnd(toast)"
|
||||
>
|
||||
<!-- icon -->
|
||||
<div class="mr-3 shrink-0">
|
||||
@@ -66,6 +72,7 @@ export default {
|
||||
return {
|
||||
toasts: [],
|
||||
counter: 0,
|
||||
swipeThreshold: 100,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@@ -125,6 +132,9 @@ export default {
|
||||
type: toast.type || "info",
|
||||
duration: toast.duration !== undefined ? toast.duration : 5000,
|
||||
timer: null,
|
||||
_swipeX: 0,
|
||||
_swiping: false,
|
||||
swipeClass: "",
|
||||
};
|
||||
|
||||
if (newToast.duration > 0) {
|
||||
@@ -157,6 +167,39 @@ export default {
|
||||
return "bg-white/90 dark:bg-zinc-900/90 border-blue-500/30";
|
||||
}
|
||||
},
|
||||
toastSwipeStyle(toast) {
|
||||
const x = toast._swipeX || 0;
|
||||
if (x === 0) return {};
|
||||
return {
|
||||
transform: `translateX(${x}px)`,
|
||||
transition: toast._swiping ? "none" : "transform 0.3s ease",
|
||||
opacity: `${1 - Math.min(Math.abs(x) / this.swipeThreshold, 0.6)}`,
|
||||
};
|
||||
},
|
||||
onTouchStart(event, toast) {
|
||||
if (event.touches.length !== 1) return;
|
||||
toast._startX = event.touches[0].clientX;
|
||||
toast._swipeX = 0;
|
||||
toast._swiping = true;
|
||||
toast.swipeClass = "";
|
||||
},
|
||||
onTouchMove(event, toast) {
|
||||
if (!toast._swiping || event.touches.length !== 1) return;
|
||||
const deltaX = event.touches[0].clientX - toast._startX;
|
||||
toast._swipeX = deltaX;
|
||||
},
|
||||
onTouchEnd(toast) {
|
||||
if (!toast._swiping) return;
|
||||
toast._swiping = false;
|
||||
if (Math.abs(toast._swipeX) >= this.swipeThreshold) {
|
||||
toast.swipeClass = toast._swipeX > 0 ? "toast-swipe-out-right" : "toast-swipe-out-left";
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => this.remove(toast.id), 250);
|
||||
});
|
||||
} else {
|
||||
toast._swipeX = 0;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -182,4 +225,14 @@ export default {
|
||||
transform: translateX(30px);
|
||||
}
|
||||
}
|
||||
.toast-swipe-out-left {
|
||||
transform: translateX(-120%) !important;
|
||||
opacity: 0 !important;
|
||||
transition: transform 0.25s ease, opacity 0.25s ease !important;
|
||||
}
|
||||
.toast-swipe-out-right {
|
||||
transform: translateX(120%) !important;
|
||||
opacity: 0 !important;
|
||||
transition: transform 0.25s ease, opacity 0.25s ease !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -107,4 +107,36 @@ describe("Toast.vue", () => {
|
||||
expect(cls).toContain("max-sm:bottom-");
|
||||
expect(cls).not.toContain("max-sm:bottom-[calc(5.75rem");
|
||||
});
|
||||
|
||||
it("dismisses toast on horizontal swipe past threshold", async () => {
|
||||
GlobalEmitter.emit("toast", { message: "Swipe me", duration: 0 });
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain("Swipe me");
|
||||
const toastVm = wrapper.vm.toasts[0];
|
||||
|
||||
// Simulate swipe right by 120px (past 100px threshold)
|
||||
wrapper.vm.onTouchStart({ touches: [{ clientX: 100, clientY: 50 }] }, toastVm);
|
||||
wrapper.vm.onTouchMove({ touches: [{ clientX: 220, clientY: 50 }] }, toastVm);
|
||||
wrapper.vm.onTouchEnd(toastVm);
|
||||
await vi.advanceTimersByTimeAsync(300);
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.text()).not.toContain("Swipe me");
|
||||
});
|
||||
|
||||
it("snaps toast back when swipe is below threshold", async () => {
|
||||
GlobalEmitter.emit("toast", { message: "Stay", duration: 0 });
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const toastVm = wrapper.vm.toasts[0];
|
||||
|
||||
wrapper.vm.onTouchStart({ touches: [{ clientX: 100, clientY: 50 }] }, toastVm);
|
||||
wrapper.vm.onTouchMove({ touches: [{ clientX: 140, clientY: 50 }] }, toastVm);
|
||||
wrapper.vm.onTouchEnd(toastVm);
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain("Stay");
|
||||
expect(toastVm._swipeX).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user