Skip to content
Clone
eeff292
106 lines 3.2 KB
import { IconX as X } from '@tabler/icons-react'
import { createContext, type ReactNode, useCallback, useContext, useMemo, useRef, useState } from 'react'
export interface ToastAction {
label: string
onClick: () => void
}
export interface ToastInput {
title: string
description?: string
action?: ToastAction
/** 毫秒;0 表示不自动关闭。 */
timeout?: number
}
interface Toast extends ToastInput {
id: number
}
interface ToastApi {
push(input: ToastInput): number
dismiss(id: number): void
}
const ToastContext = createContext<ToastApi | null>(null)
export function ToastProvider({ children }: { children: ReactNode }) {
const [items, setItems] = useState<Toast[]>([])
const seq = useRef(0)
const timers = useRef(new Map<number, number>())
const dismiss = useCallback((id: number) => {
setItems((current) => current.filter((item) => item.id !== id))
const timer = timers.current.get(id)
if (timer) {
window.clearTimeout(timer)
timers.current.delete(id)
}
}, [])
const push = useCallback(
(input: ToastInput) => {
seq.current += 1
const id = seq.current
setItems((current) => [...current.slice(-4), { ...input, id }])
const timeout = input.timeout ?? 6_000
if (timeout > 0) {
const timer = window.setTimeout(() => dismiss(id), timeout)
timers.current.set(id, timer)
}
return id
},
[dismiss],
)
const value = useMemo<ToastApi>(() => ({ push, dismiss }), [push, dismiss])
return (
<ToastContext.Provider value={value}>
{children}
<div className="pointer-events-none fixed inset-x-0 top-3 z-[70] flex flex-col items-center gap-2 px-3 sm:items-end sm:pr-4">
{items.map((item) => (
<div
key={item.id}
role="status"
className="pointer-events-auto flex w-full max-w-sm items-start gap-3 rounded-xl border border-shell-hairline bg-shell-surface px-3.5 py-3 shadow-lg"
>
<div className="min-w-0 flex-1">
<p className="truncate text-[13px] font-semibold text-foreground">{item.title}</p>
{item.description ? (
<p className="mt-0.5 line-clamp-2 text-[12px] text-shell-ink-muted">{item.description}</p>
) : null}
{item.action ? (
<button
type="button"
onClick={() => {
item.action?.onClick()
dismiss(item.id)
}}
className="mt-2 rounded-md bg-shell-action px-2.5 py-1 text-[12px] font-semibold text-white"
>
{item.action.label}
</button>
) : null}
</div>
<button
type="button"
aria-label="关闭"
onClick={() => dismiss(item.id)}
className="rounded-md p-1 text-shell-ink-muted hover:bg-shell-field"
>
<X className="h-3.5 w-3.5" />
</button>
</div>
))}
</div>
</ToastContext.Provider>
)
}
const NOOP: ToastApi = { push: () => 0, dismiss: () => {} }
export function useToast(): ToastApi {
return useContext(ToastContext) ?? NOOP
}