import { forwardRef, useEffect, type ReactNode } from 'react'; import { Text, type TextProps, View, type ViewProps } from 'react-native'; import { ErrorIcon } from '@cdx-ui/icons'; import { FieldLabel as FieldLabelPrimitive, FieldRoot as FieldRootPrimitive, Slot, dataAttributes, mergeDataAttributes, useFormControlContext, type FieldLabelProps as FieldLabelPrimitiveProps, type FieldRootProps as FieldRootPrimitiveProps, } from '@cdx-ui/primitives'; import { cn } from '@cdx-ui/utils'; import { Icon } from '../Icon'; import { fieldErrorIconVariants, fieldErrorTextVariants, fieldErrorVariants, fieldHelperTextVariants, fieldHelperVariants, fieldLabelVariants, fieldRootVariants, type FieldErrorVariantProps, type FieldHelperVariantProps, type FieldLabelVariantProps, type FieldRootVariantProps, } from './styles'; // ============================================================================= // FIELD ROOT // ============================================================================= /** * Field wrapper + context. For **initial focus**, pass `autoFocus` on `Input.Field` (not here). */ export interface FieldRootProps extends FieldRootPrimitiveProps, FieldRootVariantProps {} const FieldRoot = forwardRef( ({ className, children, style, ...props }, ref) => { const computedClassName = cn(fieldRootVariants(), className); return ( {children} ); }, ); FieldRoot.displayName = 'Field'; // ============================================================================= // FIELD LABEL // ============================================================================= export interface FieldLabelProps extends FieldLabelPrimitiveProps, FieldLabelVariantProps {} const FieldLabel = forwardRef( ({ className, children, style, htmlFor, ...props }, ref) => { const { isLabelFocused, isInvalid } = useFormControlContext(); // Native: inject text-content-action via context (group-has CSS handles web). // native: prefix keeps this class inert on web so it never conflicts with text-content-primary. const labelClassName = cn( fieldLabelVariants(), isLabelFocused && !isInvalid && 'native:text-content-action', className, ); return ( {' *'} } {...props} {...mergeDataAttributes(props, dataAttributes({ slot: 'field-label' }))} > {children} ); }, ); FieldLabel.displayName = 'Field.Label'; // ============================================================================= // FIELD HELPER (styled-only — reads form context directly) // ============================================================================= export interface FieldHelperProps extends ViewProps, FieldHelperVariantProps { asChild?: boolean; className?: string; children?: ReactNode; } const FieldHelper = forwardRef( ({ asChild, className, children, style, ...props }, ref) => { const { helpTextId, setHasHelpText, isInvalid } = useFormControlContext(); useEffect(() => { if (isInvalid) { setHasHelpText?.(false); return undefined; } setHasHelpText?.(true); return () => { setHasHelpText?.(false); }; }, [setHasHelpText, isInvalid]); if (isInvalid) { return null; } const containerClassName = cn(fieldHelperVariants(), className); const textClassName = cn(fieldHelperTextVariants()); const Comp = asChild ? Slot : View; return ( {children} ); }, ); FieldHelper.displayName = 'Field.Helper'; // ============================================================================= // FIELD ERROR (styled-only — reads form context directly) // ============================================================================= const FieldErrorIcon = ({ className, style, ...props }: { className?: string; style?: TextProps['style']; }) => { const computedClassName = cn(fieldErrorIconVariants(), className); return ( ); }; FieldErrorIcon.displayName = 'Field.ErrorIcon'; export interface FieldErrorProps extends ViewProps, FieldErrorVariantProps { asChild?: boolean; className?: string; children?: ReactNode; } const FieldError = forwardRef( ({ asChild, className, children, style, ...props }, ref) => { const { isInvalid, feedbackId, setHasFeedbackText } = useFormControlContext(); const visible = Boolean(isInvalid && children); useEffect(() => { if (!visible) { setHasFeedbackText?.(false); return undefined; } setHasFeedbackText?.(true); return () => { setHasFeedbackText?.(false); }; }, [setHasFeedbackText, visible]); if (!visible) { return null; } const containerClassName = cn(fieldErrorVariants(), className); const textClassName = cn(fieldErrorTextVariants()); const Comp = asChild ? Slot : View; return ( {children} ); }, ); FieldError.displayName = 'Field.Error'; // ============================================================================= // COMPOUND EXPORT // ============================================================================= type FieldCompoundComponent = typeof FieldRoot & { Label: typeof FieldLabel; Helper: typeof FieldHelper; Error: typeof FieldError; }; export const Field = Object.assign(FieldRoot, { Label: FieldLabel, Helper: FieldHelper, Error: FieldError, }) as FieldCompoundComponent;