make toasts accessible: ARIA live region, dismiss button, pause on hover

This commit is contained in:
Your Name
2026-02-19 19:44:06 +02:00
parent b1c5e9caa9
commit 2044a7026d
2 changed files with 66 additions and 6 deletions

View File

@@ -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<string, ReturnType<typeof setTimeout>>();
const remaining = new Map<string, number>();
const startTimes = new Map<string, number>();
const TOAST_DURATION = 8000;
function startTimer(id: string, duration: number, set: (fn: (s: ToastState) => Partial<ToastState>) => 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<ToastState>((set) => ({
toasts: [],
@@ -22,12 +41,33 @@ export const useToastStore = create<ToastState>((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);
}
},
}));