UNPKG

@travelopia/react-components

Version:

Headless React components library focused on INP performance

476 lines (475 loc) 14.6 kB
"use strict"; 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