'use client' import * as React from 'react' import { SvgVisible, SvgInvisible1 } from '@chainlink/blocks-icons' import { cn } from '../../utils/cn' import { FieldError } from '../Field' import { type FieldSize, useField } from '../Field/Field' import { ClearIcon, InvalidIcon, LoadingSpinner, ValidIcon } from './icons' import { getInputWidhtStyles, Input, type InputProps } from './Input' import { useInputClear } from './useInputClear' // --- Context --- interface FieldInputContextValue { inputRef: React.RefObject size: FieldSize fieldItemId?: string fieldDescriptionId?: string handleChange: (e: React.ChangeEvent) => void handleClear: () => void handleTogglePassword?: () => void hasValue: boolean /** When false, the hover clear control is not shown (default true). */ clearable: boolean canShowPasswordToggle: boolean showPassword: boolean type?: React.HTMLInputTypeAttribute error?: boolean disabled?: boolean value?: string | number | readonly string[] } const FieldInputContext = React.createContext< FieldInputContextValue | undefined >(undefined) export const useFieldInputContext = () => { return React.useContext(FieldInputContext) } // --- Left Icon Component --- interface FieldInputIconProps { children: React.ReactNode className?: string size?: FieldSize } /** * Left-side icon slot inside FieldInputRoot. Positions the icon absolutely * and adjusts padding accordingly. Must be used inside FieldInputRoot. */ export const FieldInputIcon = ({ children, size }: FieldInputIconProps) => { const context = useFieldInputContext() if (!context) { throw new Error('FieldInputIcon must be used within FieldInputRoot.') } const finalSize = size ?? context.size if (!children) return null return (
{children}
) } FieldInputIcon.displayName = 'FieldInputIcon' interface FieldInputActionProps { children?: React.ReactNode className?: string /** * Called when the clear button is pressed. Receives the internal `clearInput` * function so you can run side effects before or after clearing. */ onClear?: (clearInput: () => void) => void /** Shows a loading spinner. Takes priority over valid/invalid states. */ isLoading?: boolean /** Shows a success icon. Shown when isLoading is false. */ isValid?: boolean /** Shows an error icon with a clear action. Shown when isLoading and isValid are false. */ isInvalid?: boolean } /** * Right-side action slot inside FieldInputRoot. Renders state indicators with * this priority: password toggle → loading → valid → invalid → custom children → clear-on-hover. * Hidden when the input is disabled. */ export const FieldInputAction = ({ children, className, onClear: onClearProp, isLoading, isValid, isInvalid, }: FieldInputActionProps) => { const context = useFieldInputContext() // Early return if no context and no custom children if (!context && !children) { return null } const disabled = context?.disabled const onTogglePassword = context?.handleTogglePassword const canShowPasswordToggle = context?.canShowPasswordToggle ?? false const showPassword = context?.showPassword ?? false const hasValue = context?.hasValue ?? false // Don't render if disabled if (disabled) { return null } // Handler that supports composition pattern const handleClear = () => { if (onClearProp && context?.handleClear) { onClearProp(context.handleClear) } else { context?.handleClear() } } let content: React.ReactNode = null // Priority order (highest to lowest): // 1. Password toggle (always shows for password inputs) // 2. Loading state // 3. Valid state // 4. Invalid state // 5. Custom children (if provided) // 6. Clear button on hover (if has value) if (canShowPasswordToggle && onTogglePassword) { // Password toggle always takes precedence content = ( ) } else if (isLoading) { content = } else if (isValid) { content = } else if (isInvalid) { content = ( ) } else if (children) { // Custom children only show if no state indicators content = children } else if (hasValue && (context?.clearable ?? true)) { content = ( ) } if (!content) { return null } return (
{content}
) } FieldInputAction.displayName = 'FieldInputAction' // --- Input Control Component (Context-Aware Input) --- interface FieldInputControlProps extends Omit< InputProps, 'error' | 'type' | 'size' | 'width' | 'onChange' | 'value' > { error?: string | boolean } /** * Context-aware Input that reads size, id, aria props, and value from * FieldInputRoot. Use this inside FieldInputRoot instead of a bare Input. * Throws if used outside FieldInputRoot. */ export const FieldInputControl = ({ className, ...inputProps }: FieldInputControlProps) => { const context = useFieldInputContext() if (!context) { throw new Error( 'FieldInputControl must be used within FieldInputRoot. For standalone inputs, use the Input component directly.', ) } return ( ) } FieldInputControl.displayName = 'FieldInputControl' // --- Main FieldInput Component --- export interface FieldInputProps extends Omit { children?: React.ReactNode /** Shows a loading spinner in the action slot. */ isLoading?: boolean /** Shows a success icon in the action slot. */ isValid?: boolean /** Shows an error icon with a clear action in the action slot. */ isInvalid?: boolean /** * Error state for the input. * - `boolean`: applies error styles only. * - `string`: applies error styles and renders an inline FieldError message below the input. */ error?: string | boolean /** When false, the hover clear control is not shown (default true). */ clearable?: boolean } export interface FieldInputRootProps extends FieldInputProps { ref?: React.Ref } /** * Low-level composition root for FieldInput. Sets up the context that wires * together FieldInputControl, FieldInputIcon, and FieldInputAction. Use this * when you need to compose a custom input layout. For standard usage, use the * FieldInput convenience component instead. */ export const FieldInputRoot = ({ children, className, type, size, width, error, disabled, value, defaultValue, onChange, clearable = true, ref, }: FieldInputRootProps) => { const { hasValue, canShowPasswordToggle, showPassword, handleChange, handleClear, handleTogglePassword, inputRef, } = useInputClear({ value, defaultValue, onChange, type, }) const fieldContext = useField() const fieldSize = size ?? fieldContext?.size ?? 'default' const fieldItemId = fieldContext?.fieldItemId const fieldDescriptionId = fieldContext?.fieldDescriptionId // Merge refs React.useImperativeHandle(ref, () => inputRef.current!, [inputRef]) const contextValue: FieldInputContextValue = React.useMemo( () => ({ inputRef, size: fieldSize, fieldItemId, fieldDescriptionId, handleChange, handleClear, handleTogglePassword, hasValue, clearable, canShowPasswordToggle, showPassword, type: type === 'password' && showPassword ? 'text' : type, error: !!error, disabled, value, }), [ inputRef, fieldSize, fieldItemId, fieldDescriptionId, handleChange, handleClear, handleTogglePassword, hasValue, clearable, canShowPasswordToggle, showPassword, type, error, disabled, value, ], ) return (
{children}
{error && typeof error === 'string' && ( {error} )}
) } FieldInputRoot.displayName = 'FieldInputRoot' export interface FieldInputComponentProps extends FieldInputProps { ref?: React.Ref } /** * Opinionated input with built-in icon slot, action slot (loading, valid, * invalid, clear), password toggle, and inline error message. Reads `size` * and ARIA attributes from an enclosing Field when available. For custom * layouts, use FieldInputRoot, FieldInputIcon, FieldInputControl, and * FieldInputAction directly. */ export const FieldInput = ({ isLoading, isValid, isInvalid, type, size, width, className, children, error, clearable, ref, ...inputProps }: FieldInputComponentProps) => { return ( {children} ) } FieldInput.displayName = 'FieldInput'