import type { JSX } from "@solidjs/web"; import { type Accessor, createContext, useContext } from "solid-js"; import { createDefaultValidityData, DEFAULT_FIELD_ROOT_STATE, DEFAULT_FIELD_STATE_ATTRIBUTES, type FieldRootState, type FieldValidationMode, type FieldValidityData, } from "./field-constants"; // Re-export the shapes callers traditionally imported from this module so // we don't break any existing barrel consumers. export type { FieldRootState, FieldValidationMode, FieldValidityData, FieldValidityState, } from "./field-constants"; export { createDefaultValidityData, DEFAULT_FIELD_ROOT_STATE, DEFAULT_FIELD_STATE_ATTRIBUTES, DEFAULT_VALIDITY_STATE, getCombinedFieldValidityData, } from "./field-constants"; /** * Validate function signature exposed by ``. Mirrors * React: the consumer receives the current value plus a snapshot of every * sibling field's value (keyed by field name) and returns a string / list * of strings / `null` synchronously, or a Promise resolving to the same. */ export type FieldValidateFn = ( value: unknown, formValues: Record, ) => string | string[] | null | Promise; /** * Registration payload a control pushes into ``. Mirrors React's * `FieldControlRegistration`. */ export interface FieldRegistration { /** * Control DOM ref. The `` uses this for imperative work on * submit (focus, scroll-into-view, etc). Plain ref object — identity is * stable across the control's lifetime. */ controlRef: { current: HTMLElement | null }; /** Explicit getter, if the control needs to derive its current value. */ getValue?: (() => unknown) | undefined; /** Identifier the parent `
` uses to key this field in its map. */ id: string | undefined; /** Last-known value (used when `getValue` is absent). */ value: unknown; } /** * Mutable-by-design ref the field root uses to track whether the user has * interacted with the control (typed, cleared, etc). Exposed so validation * code can suppress "required" errors until the user has actually touched * the field — see `useFieldValidation.getState()`. Plain mutable object; do * NOT put a signal in here. */ export interface MutableRef { current: T; } /** * Props-factory return shape produced by `useFieldValidation`. Consumers * splat these onto their element (the root element + the native input). * * Kept as a plain object-builder rather than a reactive memo because Solid * already tracks the individual accessors the builders read from. */ export interface FieldValidationProps { /** * Adds a11y hooks (`aria-describedby` from description, `aria-invalid` * once validity is known). Call inside a `get prop()` getter or reactive * scope if the consumer wants live updates. */ getValidationProps: ( externalProps?: JSX.HTMLAttributes, ) => JSX.HTMLAttributes; /** * Same as {@link FieldValidationProps.getValidationProps}, plus an * `onInput`-equivalent that commits the value to the validity pipeline. */ getInputValidationProps: ( externalProps?: JSX.InputHTMLAttributes, ) => JSX.InputHTMLAttributes; /** Native input ref captured by the validation pipeline. */ inputRef: MutableRef; /** * Imperatively run validation against `value`. `revalidate` short-circuits * to "only revalidate if currently invalid" — see React's * `useFieldValidation.commit` for the full matrix. */ commit: (value: unknown, revalidate?: boolean) => Promise; } /** * The value handed down by `` to its children. * * Every reactive entry is an `Accessor` so that consumers pull the latest * value on demand and don't need to worry about top-level reactive reads. * Setters are plain functions (most of them wrapped through * `createStableCallback` so they're safe to capture in refs/closures). * * When `` is absent, consumers receive {@link NOOP_FIELD_CONTEXT} * which behaves like "no field" — accessors return defaults, setters do * nothing. */ export interface FieldContextValue { invalid: Accessor; name: Accessor; disabled: Accessor; validityData: Accessor; setValidityData: (next: FieldValidityData) => void; touched: Accessor; setTouched: (next: boolean) => void; dirty: Accessor; setDirty: (next: boolean) => void; filled: Accessor; setFilled: (next: boolean) => void; focused: Accessor; setFocused: (next: boolean) => void; state: Accessor; validate: Accessor; validationMode: Accessor; validationDebounceTime: Accessor; shouldValidateOnChange: Accessor; /** * Mutable latch flipped to `true` the first time the user meaningfully * touches the field. Consumed by `useFieldValidation` to gate * "valueMissing"-only errors. */ markedDirtyRef: MutableRef; /** * Register (or unregister, via `registration === undefined`) a control. * The `source` symbol is created once per control setup and used to * detect stale unregistrations. */ registerFieldControl: ( source: symbol, registration: FieldRegistration | undefined, ) => void; /** Props-factories + `inputRef` + `commit` from the validation pipeline. */ validation: FieldValidationProps; /** Marker — `true` when this context was provided by a real ``. */ readonly present: boolean; } const NOOP = () => {}; const NOOP_VALIDATION: FieldValidationProps = { getValidationProps: (props = {}) => props, getInputValidationProps: (props = {}) => props, inputRef: { current: null }, commit: async () => { // Intentional no-op — without a real field root there's no validity // signal to commit into. }, }; export const NOOP_FIELD_CONTEXT: FieldContextValue = { invalid: () => false, name: () => undefined, disabled: () => false, validityData: () => createDefaultValidityData(), setValidityData: NOOP, touched: () => DEFAULT_FIELD_STATE_ATTRIBUTES.touched, setTouched: NOOP, dirty: () => DEFAULT_FIELD_STATE_ATTRIBUTES.dirty, setDirty: NOOP, filled: () => DEFAULT_FIELD_STATE_ATTRIBUTES.filled, setFilled: NOOP, focused: () => DEFAULT_FIELD_STATE_ATTRIBUTES.focused, setFocused: NOOP, state: () => DEFAULT_FIELD_ROOT_STATE, validate: () => undefined, validationMode: () => "onSubmit", validationDebounceTime: () => 0, shouldValidateOnChange: () => false, markedDirtyRef: { current: false }, registerFieldControl: NOOP, validation: NOOP_VALIDATION, present: false, }; /** * Context carrying the field-root state. Use `` to * provide it (Solid 2's context-as-provider pattern). */ export const FieldContext = createContext(NOOP_FIELD_CONTEXT); /** * Read the current `` context. Returns the noop context when the * component is not wrapped in a field — callers can branch on `.present`. * * Named after React's `useFieldRootContext`. */ export function useFieldRootContext(): FieldContextValue { return useContext(FieldContext); }