import type { JSX } from 'solid-js'; import { createComponent, createContext, createEffect, createSignal, createUniqueId, mergeProps, onCleanup, useContext, } from 'solid-js'; import { omitProps } from 'solid-use/props'; import assert from '../../utils/assert'; import createDynamic from '../../utils/create-dynamic'; import type { DynamicProps, HeadlessProps, ValidConstructor, } from '../../utils/dynamic-prop'; import { createTag } from '../../utils/namespace'; const TOAST_TAG = createTag('toast'); const TOASTER_TAG = createTag('toaster'); interface ToastContextData { ownerID: string; } const ToastContext = createContext(); function useToastContext(componentName: string): ToastContextData { const context = useContext(ToastContext); assert( context, new Error(`<${componentName}> must be used inside a `), ); return context; } export type ToastProps = HeadlessProps; export function Toast( props: ToastProps, ): JSX.Element { useToastContext('Toast'); return createDynamic( () => props.as || ('div' as T), mergeProps( TOAST_TAG, { role: 'status', 'aria-live': 'polite', }, omitProps(props, ['as']), ) as DynamicProps, ); } export type ToasterProps = HeadlessProps; export function Toaster( props: ToasterProps, ): JSX.Element { const ownerID = createUniqueId(); return createComponent(ToastContext.Provider, { value: { ownerID, }, get children() { return createDynamic( () => props.as || ('div' as T), mergeProps(TOASTER_TAG, omitProps(props, ['as'])) as DynamicProps, ); }, }); } export interface ToastData { id: string; data: T; } export type ToasterListener = (queue: ToastData[]) => void; export class ToasterStore { private static toasterID = 0; private id: number; private queue: ToastData[] = []; private listeners = new Set>(); private toastID = 0; constructor() { this.id = ToasterStore.toasterID; ToasterStore.toasterID += 1; } subscribe(callback: ToasterListener): () => void { this.listeners.add(callback); return () => { this.listeners.delete(callback); }; } private notify(): void { const clone = [...this.queue]; for (const listener of this.listeners.keys()) { listener(clone); } } create(data: T): string { const id = `toast-${this.id}-[${this.toastID}`; this.toastID += 1; this.queue.push({ id, data, }); this.notify(); return id; } remove(id: string): void { this.queue = this.queue.filter(item => item.id !== id); this.notify(); } clear(): void { this.queue = []; this.notify(); } getQueue(): ToastData[] { return this.queue; } } export function useToaster(toaster: ToasterStore): () => ToastData[] { const [signal, setSignal] = createSignal(toaster.getQueue()); createEffect(() => { onCleanup(toaster.subscribe(setSignal)); }); return signal; }