import { computed, ref } from "vue"; import type { Component, VNode } from "vue"; import type { ToastProps } from "."; const TOAST_LIMIT = 1; const TOAST_REMOVE_DELAY = 1000000; export type StringOrVNode = string | VNode | (() => VNode); type ToasterToast = ToastProps & { id: string; title?: string; description?: StringOrVNode; action?: Component; }; const actionTypes = { ADD_TOAST: "ADD_TOAST", UPDATE_TOAST: "UPDATE_TOAST", DISMISS_TOAST: "DISMISS_TOAST", REMOVE_TOAST: "REMOVE_TOAST", } as const; let count = 0; function genId() { count = (count + 1) % Number.MAX_VALUE; return count.toString(); } type ActionType = typeof actionTypes; type Action = | { type: ActionType["ADD_TOAST"]; toast: ToasterToast; } | { type: ActionType["UPDATE_TOAST"]; toast: Partial; } | { type: ActionType["DISMISS_TOAST"]; toastId?: ToasterToast["id"]; } | { type: ActionType["REMOVE_TOAST"]; toastId?: ToasterToast["id"]; }; interface State { toasts: ToasterToast[]; } const toastTimeouts = new Map>(); function addToRemoveQueue(toastId: string) { if (toastTimeouts.has(toastId)) return; const timeout = setTimeout(() => { toastTimeouts.delete(toastId); dispatch({ type: actionTypes.REMOVE_TOAST, toastId, }); }, TOAST_REMOVE_DELAY); toastTimeouts.set(toastId, timeout); } const state = ref({ toasts: [], }); function dispatch(action: Action) { switch (action.type) { case actionTypes.ADD_TOAST: state.value.toasts = [action.toast, ...state.value.toasts].slice( 0, TOAST_LIMIT, ); break; case actionTypes.UPDATE_TOAST: state.value.toasts = state.value.toasts.map((t) => t.id === action.toast.id ? { ...t, ...action.toast } : t, ); break; case actionTypes.DISMISS_TOAST: { const { toastId } = action; if (toastId) { addToRemoveQueue(toastId); } else { state.value.toasts.forEach((toast) => { addToRemoveQueue(toast.id); }); } state.value.toasts = state.value.toasts.map((t) => t.id === toastId || toastId === undefined ? { ...t, open: false, } : t, ); break; } case actionTypes.REMOVE_TOAST: if (action.toastId === undefined) state.value.toasts = []; else state.value.toasts = state.value.toasts.filter( (t) => t.id !== action.toastId, ); break; } } function useToast() { return { toasts: computed(() => state.value.toasts), toast, dismiss: (toastId?: string) => dispatch({ type: actionTypes.DISMISS_TOAST, toastId }), }; } type Toast = Omit; function toast(props: Toast) { const id = genId(); const update = (props: ToasterToast) => dispatch({ type: actionTypes.UPDATE_TOAST, toast: { ...props, id }, }); const dismiss = () => dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id }); dispatch({ type: actionTypes.ADD_TOAST, toast: { ...props, id, open: true, onOpenChange: (open: boolean) => { if (!open) dismiss(); }, }, }); return { id, dismiss, update, }; } export { toast, useToast };