UNPKG

@kobalte/core

Version:

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

653 lines (645 loc) 20.7 kB
import { DATA_TOP_LAYER_ATTR } from './ZKYDDHM6.js'; import { ButtonRoot } from './7OVKXYPU.js'; import { createRegisterId } from './E4R2EMM4.js'; import { Polymorphic } from './6Y7B2NEO.js'; import { __export } from './5ZKAE4VZ.js'; import { createComponent, mergeProps, isServer } from 'solid-js/web'; import { mergeDefaultProps, getDocument, getWindow, mergeRefs, createGenerateId, callHandler, contains, focusWithoutScrolling, isFunction } from '@kobalte/utils'; import { createContext, useContext, splitProps, createEffect, onCleanup, on, For, createSignal, createUniqueId, createMemo, onMount, Show } from 'solid-js'; import { combineStyle } from '@solid-primitives/props'; import { createStore } from 'solid-js/store'; import createPresence from 'solid-presence'; // 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 }); 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 createComponent(ButtonRoot, mergeProps({ get ["aria-label"]() { return local["aria-label"] || context.translations().close; }, onClick }, others)); } function ToastDescription(props) { const context = useToastContext(); const mergedProps = mergeDefaultProps({ id: context.generateId("description") }, props); const [local, others] = splitProps(mergedProps, ["id"]); createEffect(() => onCleanup(context.registerDescriptionId(local.id))); return createComponent(Polymorphic, mergeProps({ as: "div", get id() { return local.id; } }, others)); } var ToastRegionContext = createContext(); function useToastRegionContext() { const context = useContext(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] = splitProps(props, ["ref", "onFocusIn", "onFocusOut", "onPointerMove", "onPointerLeave"]); const onFocusIn = (e) => { callHandler(e, local.onFocusIn); if (context.pauseOnInteraction() && !context.isPaused()) { context.pauseAllTimer(); } }; const onFocusOut = (e) => { callHandler(e, local.onFocusOut); if (!contains(ref, e.relatedTarget)) { context.resumeAllTimer(); } }; const onPointerMove = (e) => { callHandler(e, local.onPointerMove); if (context.pauseOnInteraction() && !context.isPaused()) { context.pauseAllTimer(); } }; const onPointerLeave = (e) => { callHandler(e, local.onPointerLeave); if (!contains(ref, getDocument(ref).activeElement)) { context.resumeAllTimer(); } }; createEffect(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); onCleanup(() => doc.removeEventListener("keydown", onKeyDown)); })); createEffect(() => { if (!context.pauseOnPageIdle()) { return; } const win = getWindow(ref); win.addEventListener("blur", context.pauseAllTimer); win.addEventListener("focus", context.resumeAllTimer); onCleanup(() => { win.removeEventListener("blur", context.pauseAllTimer); win.removeEventListener("focus", context.resumeAllTimer); }); }); return createComponent(Polymorphic, mergeProps({ as: "ol", ref(r$) { const _ref$ = mergeRefs((el) => ref = el, local.ref); typeof _ref$ === "function" && _ref$(r$); }, tabIndex: -1, onFocusIn, onFocusOut, onPointerMove, onPointerLeave }, others, { get children() { return createComponent(For, { get each() { return context.toasts(); }, children: (toast) => toast.toastComponent({ get toastId() { return toast.id; } }) }); } })); } function ToastProgressFill(props) { const rootContext = useToastRegionContext(); const context = useToastContext(); const [local, others] = splitProps(props, ["style"]); const [lifeTime, setLifeTime] = createSignal(100); let totalElapsedTime = 0; createEffect(() => { 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); }); onCleanup(() => { totalElapsedTime += (/* @__PURE__ */ new Date()).getTime() - context.closeTimerStartTime(); clearInterval(intervalId); }); }); return createComponent(Polymorphic, mergeProps({ as: "div", get style() { return combineStyle({ "--kb-toast-progress-fill-width": `${lifeTime()}%` }, local.style); } }, others)); } function ToastProgressTrack(props) { return createComponent(Polymorphic, mergeProps({ as: "div", "aria-hidden": "true", role: "presentation" }, props)); } 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 = mergeDefaultProps({ 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] = splitProps(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] = createSignal(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 createComponent(ToastRegionContext.Provider, { value: context, get children() { return createComponent(Polymorphic, mergeProps({ as: "div", role: "region", tabIndex: -1, get ["aria-label"]() { return ariaLabel(); }, get style() { return combineStyle({ "pointer-events": hasToasts() ? local.topLayer ? "auto" : void 0 : "none" }, local.style); } }, topLayerAttr, others)); } }); } 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 = mergeDefaultProps({ id: `toast-${createUniqueId()}`, priority: "high", translations: TOAST_INTL_TRANSLATIONS }, props); const [local, others] = splitProps(mergedProps, ["ref", "translations", "toastId", "style", "priority", "duration", "persistent", "onPause", "onResume", "onSwipeStart", "onSwipeMove", "onSwipeCancel", "onSwipeEnd", "onEscapeKeyDown", "onKeyDown", "onPointerDown", "onPointerMove", "onPointerUp"]); const [isOpen, setIsOpen] = createSignal(true); const [titleId, setTitleId] = createSignal(); const [descriptionId, setDescriptionId] = createSignal(); const [isAnimationEnabled, setIsAnimationEnabled] = createSignal(true); const [ref, setRef] = createSignal(); const { present } = createPresence({ show: isOpen, element: () => ref() ?? null }); const duration = createMemo(() => 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) => { callHandler(e, local.onKeyDown); if (e.key !== "Escape") { return; } local.onEscapeKeyDown?.(e); if (!e.defaultPrevented) { close(); } }; const onPointerDown = (e) => { callHandler(e, local.onPointerDown); if (e.button !== 0) { return; } pointerStart = { x: e.clientX, y: e.clientY }; }; const onPointerMove = (e) => { callHandler(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) => { callHandler(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); } }); createEffect(on(() => rootContext.isPaused(), (isPaused) => { if (isPaused) { pauseTimer(); } else { resumeTimer(); } }, { defer: true })); createEffect(on([isOpen, duration], ([isOpen2, duration2]) => { if (isOpen2 && !rootContext.isPaused()) { startTimer(duration2); } })); createEffect(on(() => toastStore.get(local.toastId)?.dismiss, (dismiss3) => dismiss3 && close())); createEffect(on(() => present(), (isPresent) => !isPresent && deleteToast())); const context = { translations: () => local.translations, close, duration, isPersistent: () => local.persistent ?? false, closeTimerStartTime: () => closeTimerStartTime, generateId: createGenerateId(() => others.id), registerTitleId: createRegisterId(setTitleId), registerDescriptionId: createRegisterId(setDescriptionId) }; return createComponent(Show, { get when() { return present(); }, get children() { return createComponent(ToastContext.Provider, { value: context, get children() { return createComponent(Polymorphic, mergeProps({ as: "li", ref(r$) { const _ref$ = mergeRefs(setRef, local.ref); typeof _ref$ === "function" && _ref$(r$); }, role: "status", tabIndex: 0, get style() { return combineStyle({ animation: isAnimationEnabled() ? void 0 : "none", "user-select": "none", "touch-action": "none" }, local.style); }, get ["aria-live"]() { return local.priority === "high" ? "assertive" : "polite"; }, "aria-atomic": "true", get ["aria-labelledby"]() { return titleId(); }, get ["aria-describedby"]() { return descriptionId(); }, get ["data-opened"]() { return isOpen() ? "" : void 0; }, get ["data-closed"]() { return !isOpen() ? "" : void 0; }, get ["data-swipe-direction"]() { return rootContext.swipeDirection(); }, onKeyDown, onPointerDown, onPointerMove, onPointerUp }, others)); } }); } }); } 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); } function ToastTitle(props) { const context = useToastContext(); const mergedProps = mergeDefaultProps({ id: context.generateId("title") }, props); const [local, others] = splitProps(mergedProps, ["id"]); createEffect(() => onCleanup(context.registerTitleId(local.id))); return createComponent(Polymorphic, mergeProps({ as: "div", get id() { return local.id; } }, others)); } 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 { Toast, ToastCloseButton, ToastDescription, ToastList, ToastProgressFill, ToastProgressTrack, ToastRegion, ToastRoot, ToastTitle, toast_exports, toaster, useToastContext };