sync: update from private repo (9a86f49b)
This commit is contained in:
+47
-12
@@ -1,24 +1,59 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
|
||||
|
||||
export type ToastVariant = 'success' | 'error';
|
||||
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 [state, setState] = useState<ToastState | null>(null);
|
||||
const [toasts, setToasts] = useState<ToastState[]>([]);
|
||||
const timers = useRef(new Map<string, ReturnType<typeof setTimeout>>());
|
||||
|
||||
useEffect(() => {
|
||||
if (!state) return;
|
||||
const id = setTimeout(() => setState(null), durationMs);
|
||||
return () => clearTimeout(id);
|
||||
}, [state, durationMs]);
|
||||
|
||||
const showToast = useCallback((message: string, variant: ToastVariant = 'success') => {
|
||||
setState({ message, variant });
|
||||
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));
|
||||
}, []);
|
||||
|
||||
return { toast: state, showToast };
|
||||
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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user