import classNames from 'classnames'; import type { EditorView } from 'codemirror'; import type { ReactNode } from 'react'; import { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'; import { useStateWithDeps } from '../../hooks/useStateWithDeps'; import { generateId } from '../../lib/generateId'; import type { EditorProps } from './Editor/Editor'; import { Editor } from './Editor/Editor'; import { IconButton } from './IconButton'; import { Label } from './Label'; import { HStack } from './Stacks'; export type InputProps = Pick< EditorProps, | 'language' | 'useTemplating' | 'autocomplete' | 'forceUpdateKey' | 'disabled' | 'autoFocus' | 'autoSelect' | 'autocompleteVariables' | 'onKeyDown' | 'readOnly' > & { name?: string; type?: 'text' | 'password'; label: ReactNode; hideLabel?: boolean; labelPosition?: 'top' | 'left'; labelClassName?: string; containerClassName?: string; inputWrapperClassName?: string; onChange?: (value: string) => void; onFocus?: () => void; onBlur?: () => void; onPaste?: (value: string) => void; onPasteOverwrite?: (value: string) => void; defaultValue?: string; leftSlot?: ReactNode; rightSlot?: ReactNode; size?: 'xs' | 'sm' | 'md' | 'auto'; className?: string; placeholder?: string; validate?: boolean | ((v: string) => boolean); required?: boolean; wrapLines?: boolean; multiLine?: boolean; fullHeight?: boolean; stateKey: EditorProps['stateKey']; }; export const Input = forwardRef(function Input( { className, containerClassName, inputWrapperClassName, defaultValue, forceUpdateKey, fullHeight, hideLabel, label, labelClassName, labelPosition = 'top', leftSlot, onBlur, onChange, onFocus, onPaste, onPasteOverwrite, placeholder, required, rightSlot, wrapLines, size = 'md', type = 'text', validate, readOnly, stateKey, multiLine, disabled, ...props }: InputProps, ref, ) { const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]); const [currentValue, setCurrentValue] = useState(defaultValue ?? ''); const [focused, setFocused] = useState(false); const [hasChanged, setHasChanged] = useStateWithDeps(false, [stateKey, forceUpdateKey]); const editorRef = useRef(null); useImperativeHandle(ref, () => editorRef.current); const handleFocus = useCallback(() => { if (readOnly) return; setFocused(true); // Select all text on focus editorRef.current?.dispatch({ selection: { anchor: 0, head: editorRef.current.state.doc.length }, }); onFocus?.(); }, [onFocus, readOnly]); const handleBlur = useCallback(() => { setFocused(false); // Move selection to the end on blur editorRef.current?.dispatch({ selection: { anchor: editorRef.current.state.doc.length }, }); onBlur?.(); }, [onBlur]); const id = useRef(`input-${generateId()}`); const editorClassName = classNames( className, '!bg-transparent min-w-0 h-auto w-full focus:outline-none placeholder:text-placeholder', ); const isValid = useMemo(() => { if (required && !validateRequire(currentValue)) return false; if (typeof validate === 'boolean') return validate; if (typeof validate === 'function' && !validate(currentValue)) return false; return true; }, [required, currentValue, validate]); const handleChange = useCallback( (value: string) => { setCurrentValue(value); onChange?.(value); setHasChanged(true); }, [onChange, setHasChanged], ); const wrapperRef = useRef(null); // Submit the nearest form on Enter key press const handleKeyDown = useCallback( (e: KeyboardEvent) => { if (e.key !== 'Enter') return; const form = wrapperRef.current?.closest('form'); if (!isValid || form == null) return; form?.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true })); }, [isValid], ); return (
{leftSlot} {type === 'password' && ( setObscured((o) => !o)} /> )} {rightSlot}
); }); function validateRequire(v: string) { return v.length > 0; }