60 lines
2.1 KiB
TypeScript
60 lines
2.1 KiB
TypeScript
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
|
|
|
|
export type ToastVariant = 'success' | 'error' | 'info';
|
|
|
|
export interface ToastState {
|
|
id: string;
|
|
message: string;
|
|
variant: ToastVariant;
|
|
title?: string;
|
|
actionLabel?: string;
|
|
onAction?: () => void;
|
|
visual?: ReactNode;
|
|
}
|
|
|
|
export interface ShowToastOptions {
|
|
/** 同じ ID の通知は更新し、重複して積まない。 */
|
|
id?: string;
|
|
title?: string;
|
|
actionLabel?: string;
|
|
onAction?: () => void;
|
|
visual?: ReactNode;
|
|
}
|
|
|
|
let nextToastId = 0;
|
|
|
|
/**
|
|
* 画面内通知のキュー。既存の `toast` は後方互換のため最新1件を返す。
|
|
* 同じ ID は置き換えるため、ポーリング由来の重複通知を表示しない。
|
|
*/
|
|
export function useToast(durationMs = 3500) {
|
|
const [toasts, setToasts] = useState<ToastState[]>([]);
|
|
const timers = useRef(new Map<string, ReturnType<typeof setTimeout>>());
|
|
|
|
const dismissToast = useCallback((id: string) => {
|
|
const timer = timers.current.get(id);
|
|
if (timer) clearTimeout(timer);
|
|
timers.current.delete(id);
|
|
setToasts(current => current.filter(toast => toast.id !== id));
|
|
}, []);
|
|
|
|
const showToast = useCallback((message: string, variant: ToastVariant = 'success', options: ShowToastOptions = {}) => {
|
|
const id = options.id ?? `toast-${++nextToastId}`;
|
|
const timer = timers.current.get(id);
|
|
if (timer) clearTimeout(timer);
|
|
const toast: ToastState = { id, message, variant, title: options.title, actionLabel: options.actionLabel, onAction: options.onAction, visual: options.visual };
|
|
setToasts(current => [...current.filter(item => item.id !== id), toast]);
|
|
timers.current.set(id, setTimeout(() => dismissToast(id), durationMs));
|
|
}, [dismissToast, durationMs]);
|
|
|
|
useEffect(() => () => {
|
|
timers.current.forEach(clearTimeout);
|
|
timers.current.clear();
|
|
}, []);
|
|
|
|
const latest = toasts.at(-1);
|
|
// Legacy consumers only expect these two properties.
|
|
const toast = latest ? { message: latest.message, variant: latest.variant } : null;
|
|
return { toast, toasts, showToast, dismissToast };
|
|
}
|