@kobalte/core
Version:
Unstyled components and primitives for building accessible web apps and design systems with SolidJS.
815 lines (795 loc) • 22 kB
JSX
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
};