CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-types--shared

Shared TypeScript type definitions for React Spectrum components and hooks, providing common interfaces for DOM interactions, styling, accessibility, internationalization, and component behavior across the React Spectrum ecosystem

Pending
Overview
Eval results
Files

input-handling.mddocs/

Input Handling and Validation

Comprehensive input management system including validation, value handling, form integration, and help text support for building robust form components.

Capabilities

Base Input Properties

Core input properties that apply to all input components.

/**
 * Base input properties for all input components
 */
interface InputBase {
  /** Whether the input is disabled */
  isDisabled?: boolean;
  /** Whether the input can be selected but not changed by the user */
  isReadOnly?: boolean;
}

/**
 * Base properties for inputs with text placeholders
 */
interface TextInputBase {
  /** Temporary text that occupies the text input when it is empty */
  placeholder?: string;
}

/**
 * Spectrum-specific text input base properties
 * @deprecated placeholder - Please use help text instead
 */
interface SpectrumTextInputBase {
  /** Temporary text that occupies the text input when it is empty */
  placeholder?: string;
}

Value Management

Generic value handling for controlled and uncontrolled input components.

/**
 * Base value management for input components
 * @template T The type of the input value
 * @template C The type passed to the change handler (defaults to T)
 */
interface ValueBase<T, C = T> {
  /** The current value (controlled) */
  value?: T;
  /** The default value (uncontrolled) */
  defaultValue?: T;
  /** Handler that is called when the value changes */
  onChange?: (value: C) => void;
}

/**
 * Range value type for inputs like sliders
 * @template T The type of the range values
 */
interface RangeValue<T> {
  /** The start value of the range */
  start: T;
  /** The end value of the range */
  end: T;
}

/**
 * Base properties for range inputs
 * @template T The type of the range values
 */
interface RangeInputBase<T> {
  /** The smallest value allowed for the input */
  minValue?: T;
  /** The largest value allowed for the input */
  maxValue?: T;
  /** The amount that the input value changes with each increment or decrement */
  step?: T;
}

Validation System

Comprehensive validation system with multiple validation behaviors and error reporting.

/** Validation state indicator */
type ValidationState = "valid" | "invalid";

/** Validation error can be a single string or array of strings */
type ValidationError = string | string[];

/** Record of validation errors keyed by field name */
type ValidationErrors = Record<string, ValidationError>;

/**
 * Function that validates a value
 * @template T The type of value being validated
 */
type ValidationFunction<T> = (value: T) => ValidationError | true | null | undefined;

/**
 * Core validation properties
 * @template T The type of value being validated
 */
interface Validation<T = unknown> {
  /** Whether user input is required on the input before form submission */
  isRequired?: boolean;
  /** Whether the input value is invalid */
  isInvalid?: boolean;
  /** @deprecated Use isInvalid instead */
  validationState?: ValidationState;
  /**
   * Whether to use native HTML form validation to prevent form submission
   * when the value is missing or invalid, or mark the field as required
   * or invalid via ARIA
   * @default "aria"
   */
  validationBehavior?: "aria" | "native";
  /**
   * A function that returns an error message if a given value is invalid.
   * Validation errors are displayed to the user when the form is submitted
   * if validationBehavior="native". For realtime validation, use the isInvalid
   * prop instead.
   */
  validate?: (value: T) => ValidationError | true | null | undefined;
}

/**
 * Results of input validation
 */
interface ValidationResult {
  /** Whether the input value is invalid */
  isInvalid: boolean;
  /** The current error messages for the input if it is invalid, otherwise an empty array */
  validationErrors: string[];
  /** The native validation details for the input */
  validationDetails: ValidityState;
}

/**
 * Spectrum-specific field validation properties
 * @template T The type of value being validated
 */
interface SpectrumFieldValidation<T> extends Omit<Validation<T>, "isInvalid" | "validationState"> {
  /** Whether the input should display its "valid" or "invalid" visual styling */
  validationState?: ValidationState;
}

Help Text and Error Messages

Support for help text, descriptions, and error messages.

/**
 * Properties for help text and error messages
 */
interface HelpTextProps {
  /** A description for the field. Provides a hint such as specific requirements for what to choose */
  description?: ReactNode;
  /** An error message for the field */
  errorMessage?: ReactNode | ((v: ValidationResult) => ReactNode);
}

/**
 * Spectrum-specific help text properties
 */
interface SpectrumHelpTextProps extends HelpTextProps {
  /** Whether the description is displayed with lighter text */
  isDisabled?: boolean;
  /** Whether an error icon is rendered */
  showErrorIcon?: boolean;
}

Labelable Components

Properties for components that can be labeled.

/** Label positioning options */
type LabelPosition = "top" | "side";

/** Label alignment options */
type Alignment = "start" | "end";

/** How to indicate required fields */
type NecessityIndicator = "icon" | "label";

/**
 * Basic labelable properties
 */
interface LabelableProps {
  /** The content to display as the label */
  label?: ReactNode;
}

/**
 * Spectrum-specific labelable properties
 */
interface SpectrumLabelableProps extends LabelableProps {
  /**
   * The label's overall position relative to the element it is labeling
   * @default "top"
   */
  labelPosition?: LabelPosition;
  /**
   * The label's horizontal alignment relative to the element it is labeling
   * @default "start"
   */
  labelAlign?: Alignment;
  /**
   * Whether the required state should be shown as an icon or text
   * @default "icon"
   */
  necessityIndicator?: NecessityIndicator;
  /** Whether the label is labeling a required field or group */
  isRequired?: boolean;
  /** A ContextualHelp element to place next to the label */
  contextualHelp?: ReactNode;
}

Removable Items

Support for items that can be removed.

/**
 * Properties for removable items
 * @template T The type of the item value
 * @template R The return type of the remove handler
 */
interface Removable<T, R> {
  /** Whether the item can be removed */
  isRemovable?: boolean;
  /** Handler called when the item is removed */
  onRemove?: (value: T, event?: SyntheticEvent) => R;
}

Usage Examples:

import { 
  InputBase, 
  ValueBase, 
  Validation, 
  HelpTextProps, 
  LabelableProps,
  ValidationResult 
} from "@react-types/shared";

// Basic text input with validation
interface TextFieldProps 
  extends InputBase, 
          ValueBase<string>, 
          Validation<string>, 
          HelpTextProps, 
          LabelableProps {
  type?: "text" | "email" | "password";
}

function TextField({ 
  value, 
  defaultValue, 
  onChange, 
  isDisabled, 
  isReadOnly,
  isRequired,
  isInvalid,
  validate,
  description,
  errorMessage,
  label,
  type = "text" 
}: TextFieldProps) {
  const [inputValue, setInputValue] = useState(defaultValue || "");
  const [validationResult, setValidationResult] = useState<ValidationResult>({
    isInvalid: false,
    validationErrors: [],
    validationDetails: {} as ValidityState
  });

  const handleChange = (newValue: string) => {
    setInputValue(newValue);
    onChange?.(newValue);
    
    // Run validation if provided
    if (validate) {
      const result = validate(newValue);
      if (result !== true && result != null) {
        const errors = Array.isArray(result) ? result : [result];
        setValidationResult({
          isInvalid: true,
          validationErrors: errors,
          validationDetails: {} as ValidityState
        });
      } else {
        setValidationResult({
          isInvalid: false,
          validationErrors: [],
          validationDetails: {} as ValidityState
        });
      }
    }
  };

  return (
    <div>
      {label && <label>{label}{isRequired && " *"}</label>}
      <input
        type={type}
        value={value ?? inputValue}
        onChange={(e) => handleChange(e.target.value)}
        disabled={isDisabled}
        readOnly={isReadOnly}
        required={isRequired}
        aria-invalid={isInvalid || validationResult.isInvalid}
      />
      {description && <div>{description}</div>}
      {(isInvalid || validationResult.isInvalid) && errorMessage && (
        <div role="alert">
          {typeof errorMessage === "function" 
            ? errorMessage(validationResult) 
            : errorMessage}
        </div>
      )}
    </div>
  );
}

// Range input example
interface SliderProps 
  extends InputBase, 
          ValueBase<number>, 
          RangeInputBase<number>, 
          LabelableProps {
  formatValue?: (value: number) => string;
}

function Slider({ 
  value, 
  defaultValue, 
  onChange, 
  minValue = 0, 
  maxValue = 100, 
  step = 1,
  isDisabled,
  label,
  formatValue 
}: SliderProps) {
  const [sliderValue, setSliderValue] = useState(defaultValue || minValue);

  const handleChange = (newValue: number) => {
    setSliderValue(newValue);
    onChange?.(newValue);
  };

  const displayValue = value ?? sliderValue;

  return (
    <div>
      {label && <label>{label}: {formatValue?.(displayValue) || displayValue}</label>}
      <input
        type="range"
        min={minValue}
        max={maxValue}
        step={step}
        value={displayValue}
        onChange={(e) => handleChange(Number(e.target.value))}
        disabled={isDisabled}
      />
    </div>
  );
}

// Validation helper functions
const validators = {
  required: (message = "This field is required"): ValidationFunction<string> => 
    (value) => !value?.trim() ? message : true,
    
  email: (message = "Please enter a valid email"): ValidationFunction<string> => 
    (value) => {
      const emailRegex = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
      return !value || emailRegex.test(value) ? true : message;
    },
    
  minLength: (min: number, message?: string): ValidationFunction<string> => 
    (value) => {
      if (!value || value.length >= min) return true;
      return message || `Must be at least ${min} characters`;
    }
};

// Usage with validation
function ContactForm() {
  return (
    <form>
      <TextField
        label="Name"
        isRequired
        validate={validators.required()}
        description="Enter your full name"
        errorMessage="Name is required"
      />
      
      <TextField
        label="Email"
        type="email"
        isRequired
        validate={(value) => {
          const required = validators.required()(value);
          if (required !== true) return required;
          return validators.email()(value);
        }}
        description="We'll use this to contact you"
        errorMessage={(result) => result.validationErrors.join(", ")}
      />
    </form>
  );
}

Install with Tessl CLI

npx tessl i tessl/npm-react-types--shared

docs

collections.md

design-tokens.md

dom-aria.md

drag-drop.md

events.md

index.md

input-handling.md

labelable.md

refs.md

selection.md

styling.md

tile.json