import { createContext, useCallback, useContext, useRef, useState } from 'react'; /** * App-wide toast notifications (issue #130). One provider near the root owns * the stack; `useToast()` hands out a fire-and-forget `showToast`. Toasts are * announced politely (`aria-live` on the stack container), stack bottom * center above the footer, and dismiss themselves — clicking one dismisses * it early. The plugin host's `ui.toast` capability feeds the same stack. */ export type ToastVariant = 'success' | 'error'; export type ShowToast = (message: string, variant?: ToastVariant) => void; interface ToastItem { id: number; message: string; variant: ToastVariant; } // 6 s statt 2,5 s (issue #170, WCAG 2.2.1): kurzlebige Statusmeldungen // waren für Screenreader-/Vergrößerungs-Nutzer kaum erfassbar. const TOAST_DURATION_MS = 6000; const ToastContext = createContext(() => {}); export function useToast(): ShowToast { return useContext(ToastContext); } export function ToastProvider({ children }: { children: React.ReactNode }): React.JSX.Element { const [toasts, setToasts] = useState([]); const nextId = useRef(0); const dismiss = useCallback((id: number): void => { setToasts((prev) => prev.filter((toast) => toast.id !== id)); }, []); const showToast = useCallback( (message, variant = 'success') => { nextId.current += 1; const id = nextId.current; setToasts((prev) => [...prev, { id, message, variant }]); window.setTimeout(() => dismiss(id), TOAST_DURATION_MS); }, [dismiss], ); return ( {children} {/* The live region exists permanently, so screen readers pick up toasts added to it; individual toasts must not carry their own role="status" (a region appearing WITH its content isn't read). Deliberately aria-live WITHOUT role="status": the region is global and always mounted, and a second status role broke every page-scoped getByRole('status') locator (legal.spec, CI). */}
{toasts.map((toast) => (
dismiss(toast.id)} > {toast.message}
))}
); }