UNPKG

@helpwave/hightide

Version:

helpwave's component and theming library

298 lines (292 loc) 8.37 kB
// src/components/user-action/Input.tsx import { forwardRef, useEffect as useEffect3, useImperativeHandle, useRef, useState as useState2 } from "react"; import clsx2 from "clsx"; // src/hooks/useDelay.ts import { useEffect, useState } from "react"; var defaultOptions = { delay: 3e3, disabled: false }; function useDelay(options) { const [timer, setTimer] = useState(void 0); const { delay, disabled } = { ...defaultOptions, ...options }; const clearTimer = () => { clearTimeout(timer); setTimer(void 0); }; const restartTimer = (onDelayFinish) => { if (disabled) { return; } clearTimeout(timer); setTimer(setTimeout(() => { onDelayFinish(); setTimer(void 0); }, delay)); }; useEffect(() => { return () => { clearTimeout(timer); }; }, [timer]); useEffect(() => { if (disabled) { clearTimeout(timer); setTimer(void 0); } }, [disabled, timer]); return { restartTimer, clearTimer, hasActiveTimer: !!timer }; } // src/util/noop.ts var noop = () => void 0; // src/components/user-action/Label.tsx import clsx from "clsx"; import { jsx } from "react/jsx-runtime"; var styleMapping = { labelSmall: "textstyle-label-sm", labelMedium: "textstyle-label-md", labelBig: "textstyle-label-lg" }; var Label = ({ children, name, labelType = "labelSmall", className, ...props }) => { return /* @__PURE__ */ jsx("label", { ...props, className: clsx(styleMapping[labelType], className), children: children ? children : name }); }; // src/hooks/useFocusManagement.ts import { useCallback } from "react"; function useFocusManagement() { const getFocusableElements = useCallback(() => { return Array.from( document.querySelectorAll( 'input, button, select, textarea, a[href], [tabindex]:not([tabindex="-1"])' ) ).filter( (el) => el instanceof HTMLElement && !el.hasAttribute("disabled") && !el.hasAttribute("hidden") && el.tabIndex !== -1 ); }, []); const getNextFocusElement = useCallback(() => { const elements = getFocusableElements(); if (elements.length === 0) { return void 0; } let nextElement = elements[0]; if (document.activeElement instanceof HTMLElement) { const currentIndex = elements.indexOf(document.activeElement); nextElement = elements[(currentIndex + 1) % elements.length]; } return nextElement; }, [getFocusableElements]); const focusNext = useCallback(() => { const nextElement = getNextFocusElement(); nextElement?.focus(); }, [getNextFocusElement]); const getPreviousFocusElement = useCallback(() => { const elements = getFocusableElements(); if (elements.length === 0) { return void 0; } let previousElement = elements[0]; if (document.activeElement instanceof HTMLElement) { const currentIndex = elements.indexOf(document.activeElement); if (currentIndex === 0) { previousElement = elements[elements.length - 1]; } else { previousElement = elements[currentIndex - 1]; } } return previousElement; }, [getFocusableElements]); const focusPrevious = useCallback(() => { const previousElement = getPreviousFocusElement(); if (previousElement) previousElement.focus(); }, [getPreviousFocusElement]); return { getFocusableElements, getNextFocusElement, getPreviousFocusElement, focusNext, focusPrevious }; } // src/hooks/useFocusOnceVisible.ts import React, { useEffect as useEffect2 } from "react"; var useFocusOnceVisible = (ref, disable = false) => { const [hasUsedFocus, setHasUsedFocus] = React.useState(false); useEffect2(() => { if (disable || hasUsedFocus) { return; } const observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting && !hasUsedFocus) { ref.current?.focus(); setHasUsedFocus(hasUsedFocus); } }, { threshold: 0.1 }); if (ref.current) { observer.observe(ref.current); } return () => observer.disconnect(); }, [disable, hasUsedFocus, ref]); }; // src/components/user-action/Input.tsx import { jsx as jsx2, jsxs } from "react/jsx-runtime"; var getInputClassName = ({ disabled = false, hasError = false }) => { return clsx2( "px-2 py-1.5 rounded-md border-2", { "bg-input-background text-input-text hover:border-primary focus:border-primary": !disabled && !hasError, "bg-on-negative text-negative border-negative-border hover:border-negative-border-hover": !disabled && hasError, "bg-disabled-background text-disabled-text border-disabled-border": disabled } ); }; var defaultEditCompleteOptions = { onBlur: true, afterDelay: true, delay: 2500 }; var Input = forwardRef(function Input2({ id, type = "text", value, label, onChange = noop, onChangeText = noop, onEditCompleted, className = "", allowEnterComplete = true, expanded = true, autoFocus = false, onBlur, editCompleteOptions, containerClassName, disabled, ...restProps }, forwardedRef) { const { onBlur: allowEditCompleteOnBlur, afterDelay, delay } = { ...defaultEditCompleteOptions, ...editCompleteOptions }; const { restartTimer, clearTimer } = useDelay({ delay, disabled: !afterDelay }); const innerRef = useRef(null); const { focusNext } = useFocusManagement(); useFocusOnceVisible(innerRef, !autoFocus); useImperativeHandle(forwardedRef, () => innerRef.current); const handleKeyDown = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); innerRef.current?.blur(); focusNext(); } }; return /* @__PURE__ */ jsxs("div", { className: clsx2({ "w-full": expanded }, containerClassName), children: [ label && /* @__PURE__ */ jsx2(Label, { ...label, htmlFor: id, className: clsx2("mb-1", label.className) }), /* @__PURE__ */ jsx2( "input", { ...restProps, ref: innerRef, value, id, type, disabled, className: clsx2(getInputClassName({ disabled }), className), onKeyDown: allowEnterComplete ? handleKeyDown : void 0, onBlur: (event) => { onBlur?.(event); if (onEditCompleted && allowEditCompleteOnBlur) { onEditCompleted(event.target.value); clearTimer(); } }, onChange: (e) => { const value2 = e.target.value; if (onEditCompleted) { restartTimer(() => { if (innerRef.current) { innerRef.current.blur(); if (!allowEditCompleteOnBlur) { onEditCompleted(value2); } } else { onEditCompleted(value2); } }); } onChange(e); onChangeText(value2); } } ) ] }); }); var InputUncontrolled = ({ value = "", onChangeText = noop, ...props }) => { const [usedValue, setUsedValue] = useState2(value); useEffect3(() => { setUsedValue(value); }, [value]); return /* @__PURE__ */ jsx2( Input, { ...props, value: usedValue, onChangeText: (text) => { setUsedValue(text); onChangeText(text); } } ); }; var FormInput = forwardRef(function FormInput2({ id, labelText, errorText, className, labelClassName, errorClassName, containerClassName, required, disabled, ...restProps }, ref) { const input = /* @__PURE__ */ jsx2( "input", { ...restProps, ref, id, disabled, className: clsx2( getInputClassName({ disabled, hasError: !!errorText }), className ) } ); return /* @__PURE__ */ jsxs("div", { className: clsx2("flex flex-col gap-y-1", containerClassName), children: [ labelText && /* @__PURE__ */ jsxs("label", { htmlFor: id, className: clsx2("textstyle-label-md", labelClassName), children: [ labelText, required && /* @__PURE__ */ jsx2("span", { className: "text-primary font-bold", children: "*" }) ] }), input, errorText && /* @__PURE__ */ jsx2("label", { htmlFor: id, className: clsx2("text-negative", errorClassName), children: errorText }) ] }); }); export { FormInput, Input, InputUncontrolled }; //# sourceMappingURL=Input.mjs.map