import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
import "./AccessibleToast.css";
const ToastCtx = createContext({ fire: () => {} });
export const useToast = () => useContext(ToastCtx);
/**
* Wrap your app with <ToastProvider />, then from anywhere:
* const { fire } = useToast();
* fire('Saved.');
* fire('Payment failed.', { urgent: true });
* fire('1 item deleted.', { action: { label: 'Undo', onClick: undo } });
*/
export function ToastProvider({ children }) {
const [toasts, setToasts] = useState([]);
const fire = useCallback((text, opts = {}) => {
const id = Date.now() + Math.random();
setToasts((t) => [...t, { id, text, ...opts }]);
}, []);
const remove = useCallback((id) => {
setToasts((t) => t.filter((x) => x.id !== id));
}, []);
return (
<ToastCtx.Provider value={{ fire }}>
{children}
<div className="toast-root" aria-label="Notifications">
{toasts.map((t) => (
<Toast key={t.id} toast={t} onDismiss={() => remove(t.id)} />
))}
</div>
</ToastCtx.Provider>
);
}
function Toast({ toast, onDismiss }) {
const { text, urgent = false, action } = toast;
const timerRef = useRef(null);
const paused = useRef(false);
function schedule() {
if (urgent) return;
clearTimeout(timerRef.current);
const ms = action ? 8000 : 5000;
timerRef.current = setTimeout(onDismiss, ms);
}
useEffect(() => { schedule(); return () => clearTimeout(timerRef.current); }, []);
return (
<div
role={urgent ? "alert" : "status"}
className={`toast ${urgent ? "toast--err" : "toast--ok"}`}
onMouseEnter={() => { paused.current = true; clearTimeout(timerRef.current); }}
onMouseLeave={() => { paused.current = false; schedule(); }}
onFocus={() => { paused.current = true; clearTimeout(timerRef.current); }}
onBlur={() => { paused.current = false; schedule(); }}
>
<span>{text}</span>
{action && (
<button
type="button"
className="toast__action"
onClick={() => { action.onClick?.(); onDismiss(); }}
>
{action.label}
</button>
)}
<button
type="button"
className="toast__close"
aria-label="Dismiss notification"
onClick={onDismiss}
>×</button>
</div>
);
}
import { ToastProvider, useToast } from "./AccessibleToast";
function Editor() {
const { fire } = useToast();
async function save() {
try {
await fetch('/api/save', { method: 'POST' });
fire('Changes saved.');
} catch {
fire('Could not save. Try again.', { urgent: true });
}
}
function del(id) {
api.delete(id);
fire('1 item moved to trash.', {
action: { label: 'Undo', onClick: () => api.restore(id) },
});
}
return (
<>
<button type="button" onClick={save}>Save</button>
<button type="button" onClick={() => del(42)}>Delete</button>
</>
);
}
export default function App() {
return (
<ToastProvider>
<Editor />
</ToastProvider>
);
}