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