/*! Strand UI | MIT License | dillingerstaffing.com */ import type { ComponentChildren, VNode } from "preact"; import { cloneElement, isValidElement, toChildArray } from "preact"; import { forwardRef } from "preact/compat"; import { cx } from "../../internal/index.js"; /** The single wrapped control, described by whichever message is showing (cf: formfield-describedby). */ export function describeControl(children: ComponentChildren, messageId: string | undefined): ComponentChildren { if (!messageId) return children; const only = toChildArray(children); if (only.length !== 1) return children; const child = only[0]; if (!isValidElement(child)) return children; const existing = (child as VNode>).props?.["aria-describedby"]; return cloneElement(child as VNode>, { "aria-describedby": existing ? `${existing} ${messageId}` : messageId, }); } export interface FormFieldProps { label: string; /** The control's id. */ htmlFor: string; hint?: string; /** Replaces the hint; announced assertively. */ error?: string; /** A checked-and-good value; replaces the hint, yields to `error`; announced politely. */ success?: string; required?: boolean; className?: string; /** The wrapped form control. */ children: ComponentChildren; } /** * Label, one message slot (error over success over hint) and the control they describe. * * @example * */ export const FormField = forwardRef( ({ label, htmlFor, hint, error, success, required = false, className = "", children }, ref) => { const messageId = error ? `${htmlFor}-error` : success ? `${htmlFor}-success` : hint ? `${htmlFor}-hint` : undefined; return (
{describeControl(children, messageId)}
{error ? ( ) : success ? (

{success}

) : hint ? (

{hint}

) : null}
); }, ); FormField.displayName = "FormField";