UNPKG

@melt-ui/svelte

Version:
237 lines (236 loc) 8.32 kB
import { usePortal } from '../../internal/actions/index.js'; import { addMeltEventListener, makeElement, createElHelpers, executeCallbacks, generateId, isTouch, kbd, noop, toWritableStores, } from '../../internal/helpers/index.js'; import { derived, readonly, writable } from 'svelte/store'; const { name } = createElHelpers('toast'); const defaults = { closeDelay: 5000, type: 'foreground', hover: 'pause', }; export function createToaster(props) { const withDefaults = { ...defaults, ...props }; const options = toWritableStores(withDefaults); const { closeDelay, type, hover } = options; const toastsMap = writable(new Map()); const addToast = (props) => { const propsWithDefaults = { closeDelay: closeDelay.get(), type: type.get(), ...props, }; const ids = { content: generateId(), title: generateId(), description: generateId(), }; const timeout = propsWithDefaults.closeDelay === 0 ? null : window.setTimeout(() => { removeToast(ids.content); }, propsWithDefaults.closeDelay); const getPercentage = () => { const { createdAt, pauseDuration, closeDelay, pausedAt } = toast; if (closeDelay === 0) return 0; if (pausedAt) { return (100 * (pausedAt - createdAt - pauseDuration)) / closeDelay; } else { const now = performance.now(); return (100 * (now - createdAt - pauseDuration)) / closeDelay; } }; const toast = { id: ids.content, ids, ...propsWithDefaults, timeout, createdAt: performance.now(), pauseDuration: 0, getPercentage, }; toastsMap.update((currentMap) => { currentMap.set(ids.content, toast); return new Map(currentMap); }); return toast; }; const removeToast = (id) => { toastsMap.update((currentMap) => { currentMap.delete(id); return new Map(currentMap); }); }; const updateToast = (id, data) => { toastsMap.update((currentMap) => { const toast = currentMap.get(id); if (!toast) return currentMap; currentMap.set(id, { ...toast, data }); return new Map(currentMap); }); }; const pauseToastTimer = (currentToast) => { if (currentToast.timeout !== null) { window.clearTimeout(currentToast.timeout); } currentToast.pausedAt = performance.now(); }; const restartToastTimer = (currentToast) => { const pausedAt = currentToast.pausedAt ?? currentToast.createdAt; const elapsed = pausedAt - currentToast.createdAt - currentToast.pauseDuration; const remaining = currentToast.closeDelay - elapsed; currentToast.timeout = window.setTimeout(() => { removeToast(currentToast.id); }, remaining); currentToast.pauseDuration += performance.now() - pausedAt; currentToast.pausedAt = undefined; }; const content = makeElement(name('content'), { stores: toastsMap, returned: ($toasts) => { return (id) => { const t = $toasts.get(id); if (!t) return null; const { ...toast } = t; return { id, role: 'alert', 'aria-describedby': toast.ids.description, 'aria-labelledby': toast.ids.title, 'aria-live': toast.type === 'foreground' ? 'assertive' : 'polite', tabindex: -1, }; }; }, action: (node) => { let destroy = noop; destroy = executeCallbacks(addMeltEventListener(node, 'pointerenter', (e) => { if (isTouch(e)) return; toastsMap.update((currentMap) => { switch (hover.get()) { case 'pause': { const currentToast = currentMap.get(node.id); if (!currentToast || currentToast.closeDelay === 0) return currentMap; pauseToastTimer(currentToast); break; } case 'pause-all': for (const [, currentToast] of currentMap) { if (!currentToast || currentToast.closeDelay === 0) continue; pauseToastTimer(currentToast); } break; } return new Map(currentMap); }); }), addMeltEventListener(node, 'pointerleave', (e) => { if (isTouch(e)) return; toastsMap.update((currentMap) => { switch (hover.get()) { case 'pause': { const currentToast = currentMap.get(node.id); if (!currentToast || currentToast.closeDelay === 0) return currentMap; restartToastTimer(currentToast); break; } case 'pause-all': for (const [, currentToast] of currentMap) { if (!currentToast || currentToast.closeDelay === 0) continue; restartToastTimer(currentToast); } break; } return new Map(currentMap); }); }), () => { removeToast(node.id); }); return { destroy, }; }, }); const title = makeElement(name('title'), { stores: toastsMap, returned: ($toasts) => { return (id) => { const toast = $toasts.get(id); if (!toast) return null; return { id: toast.ids.title, }; }; }, }); const description = makeElement(name('description'), { stores: toastsMap, returned: ($toasts) => { return (id) => { const toast = $toasts.get(id); if (!toast) return null; return { id: toast.ids.description, }; }; }, }); const close = makeElement(name('close'), { returned: () => { return (id) => ({ type: 'button', 'data-id': id, }); }, action: (node) => { function handleClose() { if (!node.dataset.id) return; removeToast(node.dataset.id); } const unsub = executeCallbacks(addMeltEventListener(node, 'click', () => { handleClose(); }), addMeltEventListener(node, 'keydown', (e) => { if (e.key !== kbd.ENTER && e.key !== kbd.SPACE) return; e.preventDefault(); handleClose(); })); return { destroy: unsub, }; }, }); const toasts = derived(toastsMap, ($toastsMap) => { return Array.from($toastsMap.values()); }); return { elements: { content, title, description, close, }, states: { toasts: readonly(toasts), }, helpers: { addToast, removeToast, updateToast, }, actions: { portal: usePortal, }, options, }; }