UNPKG

@kobalte/core

Version:

Unstyled components and primitives for building accessible web apps and design systems with SolidJS.

815 lines (795 loc) 22 kB
import { DATA_TOP_LAYER_ATTR } from "./3NI6FTA2.jsx"; import { ButtonRoot } from "./UKTBL2JL.jsx"; import { createRegisterId } from "./JNCCF6MP.jsx"; import { Polymorphic } from "./FLVHQV4A.jsx"; import { __export } from "./5WXHJDCZ.jsx"; // src/toast/index.tsx var toast_exports = {}; __export(toast_exports, { CloseButton: () => ToastCloseButton, Description: () => ToastDescription, List: () => ToastList, ProgressFill: () => ToastProgressFill, ProgressTrack: () => ToastProgressTrack, Region: () => ToastRegion, Root: () => ToastRoot, Title: () => ToastTitle, Toast: () => Toast, toaster: () => toaster, useToastContext: () => useToastContext }); // src/toast/toast-close-button.tsx import { callHandler } from "@kobalte/utils"; import { splitProps } from "solid-js"; // src/toast/toast-context.tsx import { createContext, useContext } from "solid-js"; var ToastContext = createContext(); function useToastContext() { const context = useContext(ToastContext); if (context === void 0) { throw new Error( "[kobalte]: `useToastContext` must be used within a `Toast.Root` component" ); } return context; } // src/toast/toast-close-button.tsx function ToastCloseButton(props) { const context = useToastContext(); const [local, others] = splitProps(props, [ "aria-label", "onClick" ]); const onClick = (e) => { callHandler(e, local.onClick); context.close(); }; return <ButtonRoot aria-label={local["aria-label"] || context.translations().close} onClick={onClick} {...others} />; } // src/toast/toast-description.tsx import { mergeDefaultProps } from "@kobalte/utils"; import { createEffect, onCleanup, splitProps as splitProps2 } from "solid-js"; function ToastDescription(props) { const context = useToastContext(); const mergedProps = mergeDefaultProps( { id: context.generateId("description") }, props ); const [local, others] = splitProps2(mergedProps, ["id"]); createEffect(() => onCleanup(context.registerDescriptionId(local.id))); return <Polymorphic as="div" id={local.id} {...others} />; } // src/toast/toast-list.tsx import { callHandler as callHandler2, contains, focusWithoutScrolling, getDocument, getWindow, mergeRefs } from "@kobalte/utils"; import { For, createEffect as createEffect2, on, onCleanup as onCleanup2, splitProps as splitProps3 } from "solid-js"; import { isServer } from "solid-js/web"; // src/toast/toast-region-context.tsx import { createContext as createContext2, useContext as useContext2 } from "solid-js"; var ToastRegionContext = createContext2(); function useToastRegionContext() { const context = useContext2(ToastRegionContext); if (context === void 0) { throw new Error( "[kobalte]: `useToastRegionContext` must be used within a `Toast.Region` component" ); } return context; } // src/toast/toast-list.tsx function ToastList(props) { let ref; const context = useToastRegionContext(); const [local, others] = splitProps3(props, [ "ref", "onFocusIn", "onFocusOut", "onPointerMove", "onPointerLeave" ]); const onFocusIn = (e) => { callHandler2(e, local.onFocusIn); if (context.pauseOnInteraction() && !context.isPaused()) { context.pauseAllTimer(); } }; const onFocusOut = (e) => { callHandler2(e, local.onFocusOut); if (!contains(ref, e.relatedTarget)) { context.resumeAllTimer(); } }; const onPointerMove = (e) => { callHandler2(e, local.onPointerMove); if (context.pauseOnInteraction() && !context.isPaused()) { context.pauseAllTimer(); } }; const onPointerLeave = (e) => { callHandler2(e, local.onPointerLeave); if (!contains(ref, getDocument(ref).activeElement)) { context.resumeAllTimer(); } }; createEffect2( on([() => ref, () => context.hotkey()], ([ref2, hotkey]) => { if (isServer) { return; } if (!ref2) { return; } const doc = getDocument(ref2); const onKeyDown = (event) => { const isHotkeyPressed = hotkey.every( (key) => event[key] || event.code === key ); if (isHotkeyPressed) { focusWithoutScrolling(ref2); } }; doc.addEventListener("keydown", onKeyDown); onCleanup2(() => doc.removeEventListener("keydown", onKeyDown)); }) ); createEffect2(() => { if (!context.pauseOnPageIdle()) { return; } const win = getWindow(ref); win.addEventListener("blur", context.pauseAllTimer); win.addEventListener("focus", context.resumeAllTimer); onCleanup2(() => { win.removeEventListener("blur", context.pauseAllTimer); win.removeEventListener("focus", context.resumeAllTimer); }); }); return <Polymorphic as="ol" ref={mergeRefs((el) => ref = el, local.ref)} tabIndex={-1} onFocusIn={onFocusIn} onFocusOut={onFocusOut} onPointerMove={onPointerMove} onPointerLeave={onPointerLeave} {...others} ><For each={context.toasts()}>{(toast) => toast.toastComponent({ get toastId() { return toast.id; } })}</For></Polymorphic>; } // src/toast/toast-progress-fill.tsx import { createEffect as createEffect3, createSignal, onCleanup as onCleanup3, splitProps as splitProps4 } from "solid-js"; import { combineStyle } from "@solid-primitives/props"; function ToastProgressFill(props) { const rootContext = useToastRegionContext(); const context = useToastContext(); const [local, others] = splitProps4(props, [ "style" ]); const [lifeTime, setLifeTime] = createSignal(100); let totalElapsedTime = 0; createEffect3(() => { if (rootContext.isPaused() || context.isPersistent()) { return; } const intervalId = setInterval(() => { const elapsedTime = (/* @__PURE__ */ new Date()).getTime() - context.closeTimerStartTime() + totalElapsedTime; const life = Math.trunc(100 - elapsedTime / context.duration() * 100); setLifeTime(life < 0 ? 0 : life); }); onCleanup3(() => { totalElapsedTime += (/* @__PURE__ */ new Date()).getTime() - context.closeTimerStartTime(); clearInterval(intervalId); }); }); return <Polymorphic as="div" style={combineStyle( { "--kb-toast-progress-fill-width": `${lifeTime()}%` }, local.style )} {...others} />; } // src/toast/toast-progress-track.tsx function ToastProgressTrack(props) { return <Polymorphic as="div" aria-hidden="true" role="presentation" {...props} />; } // src/toast/toast-region.tsx import { createGenerateId, mergeDefaultProps as mergeDefaultProps2 } from "@kobalte/utils"; import { createMemo, createSignal as createSignal2, createUniqueId, splitProps as splitProps5 } from "solid-js"; import { combineStyle as combineStyle2 } from "@solid-primitives/props"; // src/toast/toast-store.ts import { createStore } from "solid-js/store"; var [state, setState] = createStore({ toasts: [] }); function add(toast) { setState("toasts", (prev) => [...prev, toast]); } function get(id) { return state.toasts.find((toast) => toast.id === id); } function update(id, toast) { const index = state.toasts.findIndex((toast2) => toast2.id === id); if (index !== -1) { setState("toasts", (prev) => [ ...prev.slice(0, index), toast, ...prev.slice(index + 1) ]); } } function dismiss(id) { setState("toasts", (toast) => toast.id === id, "dismiss", true); } function remove(id) { setState("toasts", (prev) => prev.filter((toast) => toast.id !== id)); } function clear() { setState("toasts", []); } var toastStore = { toasts: () => state.toasts, add, get, update, dismiss, remove, clear }; // src/toast/toast.intl.ts var TOAST_HOTKEY_PLACEHOLDER = "{hotkey}"; var TOAST_INTL_TRANSLATIONS = { // `aria-label` of Toast.CloseButton. close: "Close" }; var TOAST_REGION_INTL_TRANSLATIONS = { // `aria-label` of Toast.Region with notification count. notifications: (hotkeyPlaceholder) => `Notifications (${hotkeyPlaceholder})` }; // src/toast/toast-region.tsx function ToastRegion(props) { const mergedProps = mergeDefaultProps2( { id: `toast-region-${createUniqueId()}`, hotkey: ["altKey", "KeyT"], duration: 5e3, limit: 3, swipeDirection: "right", swipeThreshold: 50, pauseOnInteraction: true, pauseOnPageIdle: true, topLayer: true, translations: TOAST_REGION_INTL_TRANSLATIONS }, props ); const [local, others] = splitProps5( mergedProps, [ "translations", "style", "hotkey", "duration", "limit", "swipeDirection", "swipeThreshold", "pauseOnInteraction", "pauseOnPageIdle", "topLayer", "aria-label", "regionId" ] ); const toasts = createMemo( () => toastStore.toasts().filter( (toast) => toast.region === local.regionId && toast.dismiss === false ).slice(0, local.limit) ); const [isPaused, setIsPaused] = createSignal2(false); const hasToasts = () => toasts().length > 0; const hotkeyLabel = () => { return local.hotkey.join("+").replace(/Key/g, "").replace(/Digit/g, ""); }; const ariaLabel = () => { const label = local["aria-label"] || local.translations.notifications(TOAST_HOTKEY_PLACEHOLDER); return label.replace(TOAST_HOTKEY_PLACEHOLDER, hotkeyLabel()); }; const topLayerAttr = () => ({ [DATA_TOP_LAYER_ATTR]: local.topLayer ? "" : void 0 }); const context = { isPaused, toasts, hotkey: () => local.hotkey, duration: () => local.duration, swipeDirection: () => local.swipeDirection, swipeThreshold: () => local.swipeThreshold, pauseOnInteraction: () => local.pauseOnInteraction, pauseOnPageIdle: () => local.pauseOnPageIdle, pauseAllTimer: () => setIsPaused(true), resumeAllTimer: () => setIsPaused(false), generateId: createGenerateId(() => others.id) }; return <ToastRegionContext.Provider value={context}><Polymorphic as="div" role="region" tabIndex={-1} aria-label={ariaLabel()} style={combineStyle2( { "pointer-events": hasToasts() ? local.topLayer ? "auto" : void 0 : "none" }, local.style )} {...topLayerAttr()} {...others} /></ToastRegionContext.Provider>; } // src/toast/toast-root.tsx import { callHandler as callHandler3, createGenerateId as createGenerateId2, mergeDefaultProps as mergeDefaultProps3, mergeRefs as mergeRefs2 } from "@kobalte/utils"; import { Show, createEffect as createEffect4, createMemo as createMemo2, createSignal as createSignal3, createUniqueId as createUniqueId2, on as on2, onMount, splitProps as splitProps6 } from "solid-js"; import { combineStyle as combineStyle3 } from "@solid-primitives/props"; import createPresence from "solid-presence"; var TOAST_SWIPE_START_EVENT = "toast.swipeStart"; var TOAST_SWIPE_MOVE_EVENT = "toast.swipeMove"; var TOAST_SWIPE_CANCEL_EVENT = "toast.swipeCancel"; var TOAST_SWIPE_END_EVENT = "toast.swipeEnd"; function ToastRoot(props) { const rootContext = useToastRegionContext(); const mergedProps = mergeDefaultProps3( { id: `toast-${createUniqueId2()}`, priority: "high", translations: TOAST_INTL_TRANSLATIONS }, props ); const [local, others] = splitProps6( mergedProps, [ "ref", "translations", "toastId", "style", "priority", "duration", "persistent", "onPause", "onResume", "onSwipeStart", "onSwipeMove", "onSwipeCancel", "onSwipeEnd", "onEscapeKeyDown", "onKeyDown", "onPointerDown", "onPointerMove", "onPointerUp" ] ); const [isOpen, setIsOpen] = createSignal3(true); const [titleId, setTitleId] = createSignal3(); const [descriptionId, setDescriptionId] = createSignal3(); const [isAnimationEnabled, setIsAnimationEnabled] = createSignal3(true); const [ref, setRef] = createSignal3(); const { present } = createPresence({ show: isOpen, element: () => ref() ?? null }); const duration = createMemo2(() => local.duration || rootContext.duration()); let closeTimerId; let closeTimerStartTime = 0; let closeTimerRemainingTime = duration(); let pointerStart = null; let swipeDelta = null; const close = () => { setIsOpen(false); setIsAnimationEnabled(true); }; const deleteToast = () => { toastStore.remove(local.toastId); }; const startTimer = (duration2) => { if (!duration2 || local.persistent) { return; } window.clearTimeout(closeTimerId); closeTimerStartTime = (/* @__PURE__ */ new Date()).getTime(); closeTimerId = window.setTimeout(close, duration2); }; const resumeTimer = () => { startTimer(closeTimerRemainingTime); local.onResume?.(); }; const pauseTimer = () => { const elapsedTime = (/* @__PURE__ */ new Date()).getTime() - closeTimerStartTime; closeTimerRemainingTime = closeTimerRemainingTime - elapsedTime; window.clearTimeout(closeTimerId); local.onPause?.(); }; const onKeyDown = (e) => { callHandler3(e, local.onKeyDown); if (e.key !== "Escape") { return; } local.onEscapeKeyDown?.(e); if (!e.defaultPrevented) { close(); } }; const onPointerDown = (e) => { callHandler3(e, local.onPointerDown); if (e.button !== 0) { return; } pointerStart = { x: e.clientX, y: e.clientY }; }; const onPointerMove = (e) => { callHandler3(e, local.onPointerMove); if (!pointerStart) { return; } const x = e.clientX - pointerStart.x; const y = e.clientY - pointerStart.y; const hasSwipeMoveStarted = Boolean(swipeDelta); const isHorizontalSwipe = ["left", "right"].includes( rootContext.swipeDirection() ); const clamp = ["left", "up"].includes(rootContext.swipeDirection()) ? Math.min : Math.max; const clampedX = isHorizontalSwipe ? clamp(0, x) : 0; const clampedY = !isHorizontalSwipe ? clamp(0, y) : 0; const moveStartBuffer = e.pointerType === "touch" ? 10 : 2; const delta = { x: clampedX, y: clampedY }; const eventDetail = { originalEvent: e, delta }; if (hasSwipeMoveStarted) { swipeDelta = delta; handleAndDispatchCustomEvent( TOAST_SWIPE_MOVE_EVENT, local.onSwipeMove, eventDetail ); const { x: x2, y: y2 } = delta; e.currentTarget.setAttribute("data-swipe", "move"); e.currentTarget.style.setProperty("--kb-toast-swipe-move-x", `${x2}px`); e.currentTarget.style.setProperty("--kb-toast-swipe-move-y", `${y2}px`); } else if (isDeltaInDirection(delta, rootContext.swipeDirection(), moveStartBuffer)) { swipeDelta = delta; handleAndDispatchCustomEvent( TOAST_SWIPE_START_EVENT, local.onSwipeStart, eventDetail ); e.currentTarget.setAttribute("data-swipe", "start"); e.target.setPointerCapture(e.pointerId); } else if (Math.abs(x) > moveStartBuffer || Math.abs(y) > moveStartBuffer) { pointerStart = null; } }; const onPointerUp = (e) => { callHandler3(e, local.onPointerUp); const delta = swipeDelta; const target = e.target; if (target.hasPointerCapture(e.pointerId)) { target.releasePointerCapture(e.pointerId); } swipeDelta = null; pointerStart = null; if (delta) { const toast = e.currentTarget; const eventDetail = { originalEvent: e, delta }; if (isDeltaInDirection( delta, rootContext.swipeDirection(), rootContext.swipeThreshold() )) { handleAndDispatchCustomEvent( TOAST_SWIPE_END_EVENT, local.onSwipeEnd, eventDetail ); const { x, y } = delta; e.currentTarget.setAttribute("data-swipe", "end"); e.currentTarget.style.removeProperty("--kb-toast-swipe-move-x"); e.currentTarget.style.removeProperty("--kb-toast-swipe-move-y"); e.currentTarget.style.setProperty("--kb-toast-swipe-end-x", `${x}px`); e.currentTarget.style.setProperty("--kb-toast-swipe-end-y", `${y}px`); close(); } else { handleAndDispatchCustomEvent( TOAST_SWIPE_CANCEL_EVENT, local.onSwipeCancel, eventDetail ); e.currentTarget.setAttribute("data-swipe", "cancel"); e.currentTarget.style.removeProperty("--kb-toast-swipe-move-x"); e.currentTarget.style.removeProperty("--kb-toast-swipe-move-y"); e.currentTarget.style.removeProperty("--kb-toast-swipe-end-x"); e.currentTarget.style.removeProperty("--kb-toast-swipe-end-y"); } toast.addEventListener("click", (event) => event.preventDefault(), { once: true }); } }; onMount(() => { if (rootContext.toasts().find((toast) => toast.id === local.toastId && toast.update)) { setIsAnimationEnabled(false); } }); createEffect4( on2( () => rootContext.isPaused(), (isPaused) => { if (isPaused) { pauseTimer(); } else { resumeTimer(); } }, { defer: true } ) ); createEffect4( on2([isOpen, duration], ([isOpen2, duration2]) => { if (isOpen2 && !rootContext.isPaused()) { startTimer(duration2); } }) ); createEffect4( on2( () => toastStore.get(local.toastId)?.dismiss, (dismiss3) => dismiss3 && close() ) ); createEffect4( on2( () => present(), (isPresent) => !isPresent && deleteToast() ) ); const context = { translations: () => local.translations, close, duration, isPersistent: () => local.persistent ?? false, closeTimerStartTime: () => closeTimerStartTime, generateId: createGenerateId2(() => others.id), registerTitleId: createRegisterId(setTitleId), registerDescriptionId: createRegisterId(setDescriptionId) }; return <Show when={present()}><ToastContext.Provider value={context}><Polymorphic as="li" ref={mergeRefs2(setRef, local.ref)} role="status" tabIndex={0} style={combineStyle3( { animation: isAnimationEnabled() ? void 0 : "none", "user-select": "none", "touch-action": "none" }, local.style )} aria-live={local.priority === "high" ? "assertive" : "polite"} aria-atomic="true" aria-labelledby={titleId()} aria-describedby={descriptionId()} data-opened={isOpen() ? "" : void 0} data-closed={!isOpen() ? "" : void 0} data-swipe-direction={rootContext.swipeDirection()} onKeyDown={onKeyDown} onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={onPointerUp} {...others} /></ToastContext.Provider></Show>; } function isDeltaInDirection(delta, direction, threshold = 0) { const deltaX = Math.abs(delta.x); const deltaY = Math.abs(delta.y); const isDeltaX = deltaX > deltaY; if (direction === "left" || direction === "right") { return isDeltaX && deltaX > threshold; } return !isDeltaX && deltaY > threshold; } function handleAndDispatchCustomEvent(name, handler, detail) { const currentTarget = detail.originalEvent.currentTarget; const event = new CustomEvent(name, { bubbles: true, cancelable: true, detail }); if (handler) { currentTarget.addEventListener(name, handler, { once: true }); } currentTarget.dispatchEvent(event); } // src/toast/toast-title.tsx import { mergeDefaultProps as mergeDefaultProps4 } from "@kobalte/utils"; import { createEffect as createEffect5, onCleanup as onCleanup4, splitProps as splitProps7 } from "solid-js"; function ToastTitle(props) { const context = useToastContext(); const mergedProps = mergeDefaultProps4( { id: context.generateId("title") }, props ); const [local, others] = splitProps7(mergedProps, ["id"]); createEffect5(() => onCleanup4(context.registerTitleId(local.id))); return <Polymorphic as="div" id={local.id} {...others} />; } // src/toast/toaster.ts import { isFunction } from "@kobalte/utils"; var toastsCounter = 0; function show(toastComponent, options) { const id = toastsCounter++; toastStore.add({ id, toastComponent, dismiss: false, update: false, region: options?.region }); return id; } function update2(id, toastComponent) { toastStore.update(id, { id, toastComponent, dismiss: false, update: true }); } function promise(promise2, toastComponent, options) { const id = show((props) => { return toastComponent({ get toastId() { return props.toastId; }, state: "pending" }); }, options); (isFunction(promise2) ? promise2() : promise2).then( (data) => update2(id, (props) => { return toastComponent({ get toastId() { return props.toastId; }, state: "fulfilled", data }); }) ).catch( (error) => update2(id, (props) => { return toastComponent({ get toastId() { return props.toastId; }, state: "rejected", error }); }) ); return id; } function dismiss2(id) { toastStore.dismiss(id); return id; } function clear2() { toastStore.clear(); } var toaster = { show, update: update2, promise, dismiss: dismiss2, clear: clear2 }; // src/toast/index.tsx var Toast = Object.assign(ToastRoot, { CloseButton: ToastCloseButton, Description: ToastDescription, List: ToastList, ProgressFill: ToastProgressFill, ProgressTrack: ToastProgressTrack, Region: ToastRegion, Title: ToastTitle, toaster }); export { toaster, useToastContext, ToastCloseButton, ToastDescription, ToastList, ToastProgressFill, ToastProgressTrack, ToastRegion, ToastRoot, ToastTitle, Toast, toast_exports };