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; }