From 2044a7026dd5b784a026ac4ee842dbb0860b6b98 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 19 Feb 2026 19:44:06 +0200 Subject: [PATCH] make toasts accessible: ARIA live region, dismiss button, pause on hover --- src/components/toast/ToastContainer.tsx | 26 ++++++++++++-- src/stores/toast-store.ts | 46 +++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/components/toast/ToastContainer.tsx b/src/components/toast/ToastContainer.tsx index 756879d..e0143e6 100644 --- a/src/components/toast/ToastContainer.tsx +++ b/src/components/toast/ToastContainer.tsx @@ -1,4 +1,5 @@ import { AnimatePresence, motion } from "framer-motion"; +import { X } from "lucide-react"; import { springs } from "@/lib/motion"; import { useToastStore } from "@/stores/toast-store"; @@ -10,9 +11,17 @@ const TYPE_STYLES = { export function ToastContainer() { const toasts = useToastStore((s) => s.toasts); + const removeToast = useToastStore((s) => s.removeToast); + const pauseToast = useToastStore((s) => s.pauseToast); + const resumeToast = useToastStore((s) => s.resumeToast); return ( -
+
{toasts.map((toast) => ( pauseToast(toast.id)} + onMouseLeave={() => resumeToast(toast.id)} + onFocus={() => pauseToast(toast.id)} + onBlur={() => resumeToast(toast.id)} > - {toast.message} + {toast.message} + ))} diff --git a/src/stores/toast-store.ts b/src/stores/toast-store.ts index f918f39..7ecedfa 100644 --- a/src/stores/toast-store.ts +++ b/src/stores/toast-store.ts @@ -12,9 +12,28 @@ interface ToastState { toasts: Toast[]; addToast: (message: string, type?: ToastType) => void; removeToast: (id: string) => void; + pauseToast: (id: string) => void; + resumeToast: (id: string) => void; } let nextId = 0; +const timers = new Map>(); +const remaining = new Map(); +const startTimes = new Map(); + +const TOAST_DURATION = 8000; + +function startTimer(id: string, duration: number, set: (fn: (s: ToastState) => Partial) => void) { + startTimes.set(id, Date.now()); + remaining.set(id, duration); + const timer = setTimeout(() => { + set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })); + timers.delete(id); + remaining.delete(id); + startTimes.delete(id); + }, duration); + timers.set(id, timer); +} export const useToastStore = create((set) => ({ toasts: [], @@ -22,12 +41,33 @@ export const useToastStore = create((set) => ({ addToast: (message, type = "info") => { const id = String(++nextId); set((s) => ({ toasts: [...s.toasts, { id, message, type }] })); - setTimeout(() => { - set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })); - }, 3000); + startTimer(id, TOAST_DURATION, set); }, removeToast: (id) => { + const timer = timers.get(id); + if (timer) clearTimeout(timer); + timers.delete(id); + remaining.delete(id); + startTimes.delete(id); set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })); }, + + pauseToast: (id) => { + const timer = timers.get(id); + const start = startTimes.get(id); + const rem = remaining.get(id); + if (timer && start != null && rem != null) { + clearTimeout(timer); + timers.delete(id); + remaining.set(id, rem - (Date.now() - start)); + } + }, + + resumeToast: (id) => { + const rem = remaining.get(id); + if (rem != null && rem > 0) { + startTimer(id, rem, set); + } + }, }));