Form / FormField
FormField はコンパウンド(複合)コンポーネントとして、ラベル・コントロール・補助テキスト・ エラーを 1 つのフィールドにまとめます。最大の価値は アクセシビリティ配線が自動で行われる点です。著者は id / htmlFor / aria-describedby / aria-invalid を一切書かずに、正しく関連付けられたフォームフィールドを得られます(ADR-0015)。
コンパウンドモデル
ルートの FormField は useId() で 1 つのベース ID を生成し、そこから controlId / descriptionId / errorId を派生させます。各サブコンポーネントはその ID を context 経由で受け取り、 自分の役割に応じた属性を自動で配線します。レイアウトは grid gap-2 の縦並びです。
構成パーツ
フィールドは次の 5 パーツで構成されます。FormFieldDescription と FormFieldError は FormField の直接の子 である必要があります(後述)。
| パーツ | 役割 |
|---|---|
FormField | ルート(div.grid gap-2)。useId() で ID を生成し context で配布。 invalid?: boolean でエラー状態を強制できます。 |
FormFieldLabel | Label をラップし、htmlFor をコントロールへ自動接続。required など Label の props を継承します。 |
FormFieldControl | Radix Slot として唯一の子(Input / Textarea / ネイティブコントロール / Radix トリガー)に id / aria-describedby(著者指定分とマージ)/ aria-invalid を注入します。 |
FormFieldDescription | 補助テキスト(p.text-sm text-muted)。任意。その id がコントロールの aria-describedby に自動で組み込まれます。 |
FormFieldError | エラーメッセージ(p[role="alert"].text-sm text-danger)。子が空でないときだけ描画され(常時マウントしたままにできる)、 コントロールの aria-invalid をセットし、role="alert" で読み上げます。 |
使い方
import {
FormField,
FormFieldLabel,
FormFieldControl,
FormFieldDescription,
FormFieldError,
Input,
} from "@willink-labs/react";
function EmailField({ error }: { error?: string }) {
return (
<FormField>
<FormFieldLabel required>Email</FormFieldLabel>
<FormFieldControl>
<Input type="email" />
</FormFieldControl>
<FormFieldDescription>会社のメールアドレスを入力。</FormFieldDescription>
{error && <FormFieldError>{error}</FormFieldError>}
</FormField>
);
}この 1 つのスニペットで、ラベルとコントロールの関連付け、補助テキストの aria-describedby 参照、エラー時の aria-invalid と読み上げまで、すべて自動で配線されます。
自動 a11y 配線
ADR-0015 が保証する a11y 契約は次の通りです。
- ID 衝突なし。1 フィールド=1 つの
useId()ベースなので、リスト描画で同じフィールドを多数並べても ID が衝突しません。 - 存在するノードだけ参照。
aria-describedbyは実際にレンダリングされた Description / Error のみを参照します(補助テキストや エラーが無いときは空の参照を作りません)。 - エラーの二重露出。
aria-describedbyによる関連付けに加えてrole="alert"のライブアナウンスでも伝えるため、検証エラーが確実に支援技術へ届きます。
「直接の子」制約
FormFieldDescription と FormFieldError は FormField の直接の子として配置してください。ルートはこれらの存在を検出して コントロールの aria-describedby と aria-invalid を組み立てるため、別のラッパー要素で囲むと配線が壊れます。なお FormFieldError は 子が空のあいだは描画されない設計なので、マウントしたままエラー文字列を出し入れできます。
サーバー側など外部由来のエラー状態は、ルートの invalid prop で強制できます。
<FormField invalid>
<FormFieldLabel>Email</FormFieldLabel>
<FormFieldControl>
<Input type="email" />
</FormFieldControl>
<FormFieldError>サーバー側で検証に失敗しました。</FormFieldError>
</FormField>次は Storybook の FormField で各状態を確認するか、 アクセシビリティ で DS 全体の a11y 契約を確認する。