All files / src/components/ui/FormSelect index.tsx

98.23% Statements 167/170
40% Branches 4/10
100% Functions 0/0
98.23% Lines 167/170

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 1711x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x 124x       124x 124x 372x 372x 372x 372x 372x 372x 372x 124x 124x 124x 124x 124x 1x 1x 1x 1x 1x  
'use client';
 
/**
 * FormSelect Component
 *
 * A select dropdown component designed to work with the form validation framework.
 * Wraps the base Input component with touched state support and
 * optimized error display.
 *
 * Supports both legacy props (error, disabled) and standard props
 * (isError, isDisabled, errorMessage) for backward compatibility.
 */
 
import { forwardRef } from 'react';
import { Input } from '../Input';
import { cn } from '@/lib/core';
import type { CommonSize, StateProps } from '../types';
 
export interface SelectOption {
  /** Option value */
  value: string;
  /** Display label */
  label: string;
  /** Whether option is disabled */
  disabled?: boolean;
}
 
export interface FormSelectProps
  extends StateProps,
    Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
  /** Select label */
  label: string;
  /** Select options */
  options: SelectOption[];
  /**
   * Validation error message (standard prop)
   * Takes precedence over legacy `error` prop
   */
  errorMessage?: string;
  /**
   * @deprecated Use `errorMessage` instead
   * Validation error message (legacy prop)
   */
  error?: string | null;
  /** Whether the field has been touched/blurred */
  touched?: boolean;
  /** Helper text shown below the select */
  helperText?: string;
  /** Placeholder text for empty selection */
  placeholder?: string;
  /** Select size variant */
  size?: CommonSize;
  /** Additional wrapper class name */
  wrapperClassName?: string;
}
 
/**
 * FormSelect Component
 *
 * A select dropdown component that integrates with the form validation framework.
 * Only shows errors when the field has been touched.
 *
 * @example
 * ```tsx
 * // With standard props
 * <FormSelect
 *   label="Country"
 *   name="country"
 *   options={[
 *     { value: 'us', label: 'United States' },
 *     { value: 'ca', label: 'Canada' }
 *   ]}
 *   value={country}
 *   onChange={handleChange}
 *   isError={!!errors.country}
 *   errorMessage={errors.country}
 *   required
 * />
 *
 * // With form validation framework
 * const { errors, touched, getFieldProps } = useForm({ ... });
 *
 * <FormSelect
 *   label="Country"
 *   options={countryOptions}
 *   {...getFieldProps('country')}
 *   error={errors.country}
 *   touched={touched.country}
 *   required
 * />
 * ```
 */
export const FormSelect = forwardRef<HTMLSelectElement, FormSelectProps>(
  (
    {
      label,
      options,
      // Standard props
      errorMessage,
      isLoading,
      isDisabled,
      isError,
      // Legacy props
      error,
      touched,
      // Common props
      helperText,
      placeholder,
      size = 'md',
      wrapperClassName,
      className,
      id,
      required,
      disabled,
      value,
      ...props
    },
    ref
  ) => {
    // Resolve error message (prefer standard prop, convert null to undefined)
    const resolvedErrorMessage = errorMessage ?? error ?? undefined;
 
    // Only show error if the field has been touched (when using touched prop)
    // or if isError is explicitly set
    const hasError = isError ?? (touched ? !!resolvedErrorMessage : false);
    const displayedError = hasError ? resolvedErrorMessage : undefined;
 
    // Resolve disabled state (combine standard and legacy)
    const resolvedDisabled = isDisabled ?? disabled;
 
    return (
      <div className={cn('form-select-wrapper', wrapperClassName)}>
        <Input
          ref={ref as React.Ref<HTMLSelectElement>}
          as="select"
          id={id}
          label={label}
          error={displayedError}
          helperText={helperText}
          size={size}
          required={required}
          disabled={resolvedDisabled || isLoading}
          fullWidth
          className={className}
          value={value}
          {...props}
        >
          {placeholder && (
            <option value="" disabled>
              {placeholder}
            </option>
          )}
          {options.map((option) => (
            <option
              key={option.value}
              value={option.value}
              disabled={option.disabled}
            >
              {option.label}
            </option>
          ))}
        </Input>
      </div>
    );
  }
);
 
FormSelect.displayName = 'FormSelect';
 
export default FormSelect;