import classNames from 'classnames'; import type { FocusEvent, HTMLAttributes, ReactNode } from 'react'; import { forwardRef, useCallback, useImperativeHandle, useLayoutEffect, useRef, useState, } from 'react'; import { useRandomKey } from '../../hooks/useRandomKey'; import { useStateWithDeps } from '../../hooks/useStateWithDeps'; import { generateId } from '../../lib/generateId'; import { IconButton } from './IconButton'; import type { InputProps } from './Input'; import { Label } from './Label'; import { HStack } from './Stacks'; export type PlainInputProps = Omit< InputProps, | 'wrapLines' | 'onKeyDown' | 'type' | 'stateKey' | 'autocompleteVariables' | 'autocompleteFunctions' | 'autocomplete' | 'extraExtensions' | 'forcedEnvironmentId' > & Pick, 'onKeyDownCapture'> & { onFocusRaw?: HTMLAttributes['onFocus']; type?: 'text' | 'password' | 'number'; step?: number; hideObscureToggle?: boolean; labelRightSlot?: ReactNode; }; export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(function PlainInput( { autoFocus, autoSelect, className, containerClassName, defaultValue, forceUpdateKey: forceUpdateKeyFromAbove, help, hideLabel, hideObscureToggle, label, labelClassName, labelPosition = 'top', labelRightSlot, leftSlot, name, onBlur, onChange, onFocus, onFocusRaw, onKeyDownCapture, onPaste, placeholder, required, rightSlot, size = 'md', tint, type = 'text', validate, }, ref, ) { // Track a local key for updates. If the default value is changed when the input is not in focus, // regenerate this to force the field to update. const [focusedUpdateKey, regenerateFocusedUpdateKey] = useRandomKey(); const forceUpdateKey = `${forceUpdateKeyFromAbove}::${focusedUpdateKey}`; const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]); const [focused, setFocused] = useState(false); const [hasChanged, setHasChanged] = useStateWithDeps(false, [forceUpdateKey]); const textareaRef = useRef(null); const inputRef = useRef(null); useImperativeHandle<{ focus: () => void } | null, { focus: () => void } | null>( ref, () => inputRef.current, ); const handleFocus = useCallback( (e: FocusEvent) => { onFocusRaw?.(e); setFocused(true); if (autoSelect) { inputRef.current?.select(); textareaRef.current?.select(); } onFocus?.(); }, [autoSelect, onFocus, onFocusRaw], ); const handleBlur = useCallback(() => { setFocused(false); onBlur?.(); }, [onBlur]); // Force input to update when receiving change and not in focus useLayoutEffect(() => { const isFocused = document.activeElement === inputRef.current; if (defaultValue != null && !isFocused) { regenerateFocusedUpdateKey(); } }, [regenerateFocusedUpdateKey, defaultValue]); const id = useRef(`input-${generateId()}`); const commonClassName = classNames( className, '!bg-transparent min-w-0 w-full focus:outline-none placeholder:text-placeholder', 'px-2 text-xs font-mono cursor-text', ); const handleChange = useCallback( (value: string) => { onChange?.(value); setHasChanged(true); const isValid = (value: string) => { if (required && !validateRequire(value)) return false; if (typeof validate === 'boolean') return validate; if (typeof validate === 'function' && !validate(value)) return false; return true; }; inputRef.current?.setCustomValidity(isValid(value) ? '' : 'Invalid value'); }, [onChange, required, setHasChanged, validate], ); const wrapperRef = useRef(null); return (
{tint != null && (
)} {leftSlot} handleChange(e.target.value)} onPaste={(e) => onPaste?.(e.clipboardData.getData('Text'))} className={classNames(commonClassName, 'h-full')} onFocus={handleFocus} onBlur={handleBlur} required={required} placeholder={placeholder} onKeyDownCapture={onKeyDownCapture} /> {type === 'password' && !hideObscureToggle && ( setObscured((o) => !o)} /> )} {rightSlot}
); }); function validateRequire(v: string) { return v.length > 0; }