import type { JSX, ValidComponent } from "@solidjs/web";
import { dynamic } from "@solidjs/web";
import { createSignal, omit, untrack } from "solid-js";
import type { FieldValidationMode } from "../internals/field-constants";
import {
FormContext,
type FormContextValue,
type FormErrors,
type FormFieldEntry,
} from "../internals/form-context";
import { mergeRootProps } from "../internals/merge-root-props";
import { resolveClassStyle } from "../internals/resolve-class-style";
import { createStableCallback } from "../internals/stable-callback.ts";
import type { BaseUIComponentProps } from "../internals/types";
import { useValueChanged } from "../internals/use-value-changed.ts";
/**
* Focus helper mirroring React's `focusControl` in `Form.tsx`: focuses the
* element and, when it's an ``, selects its contents so the user can
* immediately re-type.
*/
function focusInvalidControl(control: HTMLElement | null): void {
if (!control) return;
control.focus();
if (control.tagName === "INPUT") {
(control as HTMLInputElement).select();
}
}
/**
* Pick the "first" invalid field. Prefers DOM order via
* `compareDocumentPosition` when the controls are connected to a document;
* falls back to registration order (the Map's insertion order — which matches
* mount order and therefore DOM order for most apps) when either control is
* detached or the positions are identical.
*/
function firstInvalidInDomOrder(
invalidFields: readonly FormFieldEntry[],
): FormFieldEntry | undefined {
if (invalidFields.length === 0) return undefined;
if (invalidFields.length === 1) return invalidFields[0];
let best = invalidFields[0];
for (let i = 1; i < invalidFields.length; i++) {
const candidate = invalidFields[i];
const a = best?.controlRef.current;
const b = candidate?.controlRef.current;
if (!a || !b) continue;
// Node.DOCUMENT_POSITION_FOLLOWING (0x04) — `b` follows `a`, keep `a`.
// Node.DOCUMENT_POSITION_PRECEDING (0x02) — `b` precedes `a`, swap.
const position = a.compareDocumentPosition(b);
if (position & 0x02) {
best = candidate;
}
}
return best;
}
export type { FormContextValue, FormErrors, FormFieldEntry };
export type FormValidationMode = FieldValidationMode;
// The form has no state exposed to children — state-driven `class`/`style`
// callbacks will receive an empty object. Using `Record`
// communicates "intentionally empty shape" (biome's `noEmptyInterface` /
// `noEmptyBlockStatements` don't flag it).
export type FormState = Record;
export interface FormProps extends BaseUIComponentProps<"form", FormState> {
/**
* Tag/component to render. Defaults to `"form"`.
*/
as?: ValidComponent;
/**
* Determines when the form should be validated. The `validationMode`
* prop on `` takes precedence over this.
*
* @default "onSubmit"
*/
validationMode?: FormValidationMode | undefined;
/**
* Validation errors returned externally (e.g. from server submission).
* Keys correspond to ``'s `name` attribute; values are the
* error(s) for that field.
*/
errors?: FormErrors | undefined;
/**
* Called when the consumer submits the form AND every registered field
* passed validation. The native submit event is passed through — the
* consumer is free to call `preventDefault()` as needed.
*/
onSubmit?: JSX.EventHandler | undefined;
/**
* Called when submission is blocked because one or more registered
* fields failed validation. Base UI already calls `preventDefault()` on
* the submit event before invoking this handler.
*/
onSubmitValidationFailed?:
| ((event: SubmitEvent, invalidFields: FormFieldEntry[]) => void)
| undefined;
}
/**
* A native `