All files / src/components/admin/forms AdminSelect.tsx

0% Statements 0/108
100% Branches 0/0
0% Functions 0/1
0% Lines 0/108

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                                                                                                                                                                                                                         
'use client';

import { SelectHTMLAttributes, forwardRef } from 'react';
import { cn } from '@/lib/core';
import { adminTokens } from '@/lib/admin/design-tokens';

interface SelectOption {
  value: string;
  label: string;
  disabled?: boolean;
}

interface AdminSelectProps extends Omit<SelectHTMLAttributes<HTMLSelectElement>, 'children'> {
  /** Select label */
  label: string;
  /** Options to display */
  options: SelectOption[];
  /** Error message */
  error?: string;
  /** Helper text */
  hint?: string;
  /** Hide label visually (still accessible) */
  hideLabel?: boolean;
  /** Placeholder option text */
  placeholder?: string;
}

/**
 * Styled select component for admin forms.
 *
 * @example
 * ```tsx
 * <AdminSelect
 *   label="Category"
 *   options={[
 *     { value: 'electronics', label: 'Electronics' },
 *     { value: 'clothing', label: 'Clothing' },
 *   ]}
 *   {...register('category')}
 * />
 * ```
 */
export const AdminSelect = forwardRef<HTMLSelectElement, AdminSelectProps>(
  ({ label, options, error, hint, hideLabel, placeholder, className, id, ...props }, ref) => {
    const selectId = id || `select-${label.toLowerCase().replace(/\s+/g, '-')}`;

    return (
      <div className="space-y-1">
        <label
          htmlFor={selectId}
          className={cn(
            'block text-sm font-medium',
            adminTokens.colors.form.label,
            hideLabel && 'sr-only'
          )}
        >
          {label}
          {props.required && <span className="text-red-500 ml-1">*</span>}
        </label>
        <select
          ref={ref}
          id={selectId}
          className={cn(
            'w-full px-3 py-2 rounded-md border',
            adminTokens.colors.form.input.bg,
            adminTokens.colors.form.input.text,
            'focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-900',
            error
              ? adminTokens.colors.form.input.error
              : cn(adminTokens.colors.form.input.border, adminTokens.colors.form.input.focus),
            'disabled:opacity-50 disabled:cursor-not-allowed',
            className
          )}
          aria-invalid={error ? 'true' : 'false'}
          aria-describedby={
            error ? `${selectId}-error` : hint ? `${selectId}-hint` : undefined
          }
          {...props}
        >
          {placeholder && (
            <option value="" disabled>
              {placeholder}
            </option>
          )}
          {options.map((option) => (
            <option key={option.value} value={option.value} disabled={option.disabled}>
              {option.label}
            </option>
          ))}
        </select>
        {error && (
          <p id={`${selectId}-error`} className={cn('text-sm', adminTokens.colors.form.error)}>
            {error}
          </p>
        )}
        {hint && !error && (
          <p id={`${selectId}-hint`} className={cn('text-sm', adminTokens.colors.form.hint)}>
            {hint}
          </p>
        )}
      </div>
    );
  }
);

AdminSelect.displayName = 'AdminSelect';

export default AdminSelect;