@travelopia/react-components
Version:
Headless React components library focused on INP performance
476 lines (475 loc) • 14.6 kB
JavaScript
;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const react = require("react");
const jsxRuntime = require("react/jsx-runtime");
const reactDom = require("react-dom");
async function yieldToMainThread() {
if (typeof window !== "undefined" && window.scheduler?.yield) {
await window.scheduler.yield();
return;
}
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
function useControllableState({
value: controlledValue,
defaultValue,
onChange
}) {
const [uncontrolledValue, setUncontrolledValue] = react.useState(defaultValue);
const isControlled = controlledValue !== void 0;
const value = isControlled ? controlledValue : uncontrolledValue;
const onChangeRef = react.useRef(onChange);
react.useEffect(() => {
onChangeRef.current = onChange;
});
const setValue = react.useCallback(
async (nextValue) => {
await yieldToMainThread();
if (!isControlled) {
setUncontrolledValue(nextValue);
}
onChangeRef.current?.(nextValue);
},
[isControlled]
);
return [value, setValue];
}
function useId(prefix) {
const id = react.useId();
return prefix ? `${prefix}-${id}` : id;
}
const TpAccordionContext = react.createContext(void 0);
function useTpAccordionContext() {
const context = react.useContext(TpAccordionContext);
if (!context) {
throw new Error("TpAccordion compound components must be used within TpAccordion");
}
return context;
}
function TpAccordion({
children,
value: controlledValue,
defaultValue = [],
onValueChange
}) {
const [value, setValue] = useControllableState({
value: controlledValue,
defaultValue,
onChange: onValueChange
});
const baseId = useId("tpaccordion");
const onItemToggle = react.useCallback(
(itemValue) => {
const currentValue = value ?? [];
const isOpen = currentValue.includes(itemValue);
if (isOpen) {
setValue(currentValue.filter((v) => v !== itemValue));
} else {
setValue([...currentValue, itemValue]);
}
},
[value, setValue]
);
const getItemId = react.useCallback((itemValue) => `${baseId}-item-${itemValue}`, [baseId]);
const getContentId = react.useCallback(
(itemValue) => `${baseId}-content-${itemValue}`,
[baseId]
);
const getTriggerId = react.useCallback(
(itemValue) => `${baseId}-trigger-${itemValue}`,
[baseId]
);
const contextValue = react.useMemo(
() => ({
value: value ?? [],
onItemToggle,
getItemId,
getContentId,
getTriggerId
}),
[value, onItemToggle, getItemId, getContentId, getTriggerId]
);
return /* @__PURE__ */ jsxRuntime.jsx(TpAccordionContext.Provider, { value: contextValue, children });
}
function r(e) {
var t, f, n = "";
if ("string" == typeof e || "number" == typeof e) n += e;
else if ("object" == typeof e) if (Array.isArray(e)) {
var o = e.length;
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
} else for (f in e) e[f] && (n && (n += " "), n += f);
return n;
}
function clsx() {
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
return n;
}
const item = "_item_eqm2n_2";
const trigger = "_trigger_eqm2n_7";
const content$1 = "_content_eqm2n_20";
const contentInner = "_contentInner_eqm2n_32";
const styles$1 = {
item,
trigger,
content: content$1,
contentInner
};
const TpAccordionItemContext = react.createContext(void 0);
function useTpAccordionItemContext() {
const context = react.useContext(TpAccordionItemContext);
if (!context) {
throw new Error("TpAccordionItem compound components must be used within TpAccordionItem");
}
return context;
}
function TpAccordionItem({ children, value, className, ...props }) {
const { value: openValues, getItemId } = useTpAccordionContext();
const isOpen = openValues.includes(value);
const contextValue = react.useMemo(
() => ({
value,
isOpen
}),
[value, isOpen]
);
return /* @__PURE__ */ jsxRuntime.jsx(TpAccordionItemContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxRuntime.jsx("div", { id: getItemId(value), className: clsx(styles$1.item, className), ...props, children }) });
}
function TpAccordionContent({ children, className, ...props }) {
const { getContentId, getTriggerId } = useTpAccordionContext();
const { value, isOpen } = useTpAccordionItemContext();
return /* @__PURE__ */ jsxRuntime.jsx(
"section",
{
id: getContentId(value),
"aria-labelledby": getTriggerId(value),
className: clsx(styles$1.content, className),
"data-state": isOpen ? "open" : "closed",
...props,
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: styles$1.contentInner, children })
}
);
}
function useCallbackWithYield(callback, deps) {
return react.useCallback(async (event) => {
await yieldToMainThread();
return callback(event);
}, deps);
}
function TpAccordionTrigger({ children, className, ...props }) {
const { onItemToggle, getTriggerId, getContentId } = useTpAccordionContext();
const { value, isOpen } = useTpAccordionItemContext();
const handleClick = useCallbackWithYield(() => {
onItemToggle(value);
}, [onItemToggle, value]);
return /* @__PURE__ */ jsxRuntime.jsx(
"button",
{
type: "button",
id: getTriggerId(value),
"aria-expanded": isOpen,
"aria-controls": getContentId(value),
className: clsx(styles$1.trigger, className),
onClick: handleClick,
...props,
children
}
);
}
const TpModalContext = react.createContext(null);
function useTpModalContext() {
const context = react.useContext(TpModalContext);
if (!context) {
throw new Error("TpModal components must be used within a TpModal");
}
return context;
}
function TpModal({
children,
open: controlledOpen,
defaultOpen = false,
onOpenChange
}) {
const [isOpen, setIsOpen] = useControllableState({
value: controlledOpen,
defaultValue: defaultOpen,
onChange: onOpenChange
});
const titleId = useId("tpmodal-title");
const descriptionId = useId("tpmodal-description");
const open = react.useCallback(() => {
setIsOpen(true);
}, [setIsOpen]);
const close2 = react.useCallback(() => {
setIsOpen(false);
}, [setIsOpen]);
const contextValue = react.useMemo(
() => ({
isOpen: isOpen ?? false,
open,
close: close2,
titleId,
descriptionId
}),
[isOpen, open, close2, titleId, descriptionId]
);
return /* @__PURE__ */ jsxRuntime.jsx(TpModalContext.Provider, { value: contextValue, children });
}
const overlay = "_overlay_1rwrj_2";
const content = "_content_1rwrj_13";
const close = "_close_1rwrj_24";
const title = "_title_1rwrj_34";
const description = "_description_1rwrj_42";
const styles = {
overlay,
content,
close,
title,
description
};
function TpModalClose({
children,
className,
onClick,
"aria-label": ariaLabel = "Close modal",
...props
}) {
const { close: close2 } = useTpModalContext();
const handleClick = useCallbackWithYield(
(event) => {
close2();
onClick?.(event);
},
[close2, onClick]
);
return /* @__PURE__ */ jsxRuntime.jsx(
"button",
{
type: "button",
className: clsx(styles.close, className),
onClick: handleClick,
"aria-label": ariaLabel,
...props,
children
}
);
}
const FOCUSABLE_ELEMENTS = [
"a[href]",
"area[href]",
'input:not([disabled]):not([type="hidden"])',
"select:not([disabled])",
"textarea:not([disabled])",
"button:not([disabled])",
"iframe",
"object",
"embed",
"[contenteditable]",
'[tabindex]:not([tabindex^="-"])'
];
function useFocusTrap(isActive) {
const containerRef = react.useRef(null);
const previousActiveElement = react.useRef(null);
react.useEffect(() => {
if (!isActive) return;
const container = containerRef.current;
if (!container) return;
previousActiveElement.current = document.activeElement;
const getFocusableElements = () => {
const elements = container.querySelectorAll(FOCUSABLE_ELEMENTS.join(","));
return Array.from(elements);
};
const focusableElements = getFocusableElements();
if (focusableElements.length > 0) {
focusableElements[0]?.focus();
}
const handleKeyDown = (event) => {
if (event.key !== "Tab") return;
const focusableElements2 = getFocusableElements();
if (focusableElements2.length === 0) return;
const firstElement = focusableElements2[0];
const lastElement = focusableElements2[focusableElements2.length - 1];
if (!firstElement || !lastElement) return;
if (event.shiftKey) {
if (document.activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
}
} else if (document.activeElement === lastElement) {
event.preventDefault();
firstElement.focus();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
previousActiveElement.current?.focus();
};
}, [isActive]);
return containerRef;
}
function TpModalContent({ children, className, ...props }) {
const { isOpen, close: close2, titleId, descriptionId } = useTpModalContext();
const contentRef = useFocusTrap(isOpen);
const handleKeyDown = react.useCallback(
(event) => {
if (event.key === "Escape") {
event.preventDefault();
close2();
}
},
[close2]
);
react.useEffect(() => {
if (!isOpen) return;
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [isOpen, handleKeyDown]);
const handleContentClick = (event) => {
event.stopPropagation();
};
if (!isOpen) return null;
return (
// biome-ignore lint/a11y/useKeyWithClickEvents: Keyboard events are handled via Escape key listener
/* @__PURE__ */ jsxRuntime.jsx(
"div",
{
ref: contentRef,
role: "dialog",
"aria-modal": "true",
"aria-labelledby": titleId,
"aria-describedby": descriptionId,
className: clsx(styles.content, className),
onClick: handleContentClick,
...props,
children
}
)
);
}
function TpModalDescription({ children, className, ...props }) {
const { descriptionId } = useTpModalContext();
return /* @__PURE__ */ jsxRuntime.jsx("div", { id: descriptionId, className: clsx(styles.description, className), ...props, children });
}
function useBodyScrollLock(isLocked) {
react.useEffect(() => {
if (!isLocked) return;
const originalOverflow = document.body.style.overflow;
const originalPaddingRight = document.body.style.paddingRight;
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
document.body.style.overflow = "hidden";
if (scrollbarWidth > 0) {
document.body.style.paddingRight = `${scrollbarWidth}px`;
}
return () => {
document.body.style.overflow = originalOverflow;
document.body.style.paddingRight = originalPaddingRight;
};
}, [isLocked]);
}
function TpModalOverlay({
children,
closeOnClick = true,
className,
onClick,
...props
}) {
const { isOpen, close: close2 } = useTpModalContext();
useBodyScrollLock(isOpen);
const handleClick = useCallbackWithYield(
(event) => {
if (closeOnClick) {
close2();
}
onClick?.(event);
},
[closeOnClick, close2, onClick]
);
if (!isOpen) return null;
const overlay2 = (
// biome-ignore lint/a11y/noStaticElementInteractions: Overlay is intentionally clickable for modal dismissal
// biome-ignore lint/a11y/useKeyWithClickEvents: Escape key is handled in TpModalContent
/* @__PURE__ */ jsxRuntime.jsx("div", { className: clsx(styles.overlay, className), onClick: handleClick, ...props, children })
);
return reactDom.createPortal(overlay2, document.body);
}
function TpModalTitle({
children,
className,
as: Component = "h2",
...props
}) {
const { titleId } = useTpModalContext();
return /* @__PURE__ */ jsxRuntime.jsx(Component, { id: titleId, className: clsx(styles.title, className), ...props, children });
}
function useStateWithYield(initialValue) {
const [state, setState] = react.useState(initialValue);
const setStateYielded = react.useCallback(async (value) => {
await yieldToMainThread();
setState(value);
}, []);
return [state, setStateYielded];
}
function useTpAccordion(defaultValue = []) {
const [value, setValue] = react.useState(defaultValue);
const openItem = react.useCallback((itemValue) => {
setValue((prev) => prev.includes(itemValue) ? prev : [...prev, itemValue]);
}, []);
const closeItem = react.useCallback((itemValue) => {
setValue((prev) => prev.filter((v) => v !== itemValue));
}, []);
const toggleItem = react.useCallback((itemValue) => {
setValue(
(prev) => prev.includes(itemValue) ? prev.filter((v) => v !== itemValue) : [...prev, itemValue]
);
}, []);
const openAll = react.useCallback((itemValues) => {
setValue(itemValues);
}, []);
const closeAll = react.useCallback(() => {
setValue([]);
}, []);
return {
value,
openItem,
closeItem,
toggleItem,
openAll,
closeAll
};
}
function useTpModal(defaultOpen = false) {
const [isOpen, setIsOpen] = react.useState(defaultOpen);
const open = react.useCallback(() => {
setIsOpen(true);
}, []);
const close2 = react.useCallback(() => {
setIsOpen(false);
}, []);
const toggle = react.useCallback(() => {
setIsOpen((prev) => !prev);
}, []);
return {
isOpen,
open,
close: close2,
toggle
};
}
exports.TpAccordion = TpAccordion;
exports.TpAccordionContent = TpAccordionContent;
exports.TpAccordionItem = TpAccordionItem;
exports.TpAccordionTrigger = TpAccordionTrigger;
exports.TpModal = TpModal;
exports.TpModalClose = TpModalClose;
exports.TpModalContent = TpModalContent;
exports.TpModalDescription = TpModalDescription;
exports.TpModalOverlay = TpModalOverlay;
exports.TpModalTitle = TpModalTitle;
exports.useCallbackWithYield = useCallbackWithYield;
exports.useStateWithYield = useStateWithYield;
exports.useTpAccordion = useTpAccordion;
exports.useTpModal = useTpModal;
exports.yieldToMainThread = yieldToMainThread;
//# sourceMappingURL=index.cjs.map