i-Willink
ドキュメントメニュー

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 でエラー状態を強制できます。
FormFieldLabelLabel をラップし、htmlFor をコントロールへ自動接続。required など Label の props を継承します。
FormFieldControlRadix 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" で読み上げます。

使い方

tsx
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 で強制できます。

tsx
<FormField invalid>
  <FormFieldLabel>Email</FormFieldLabel>
  <FormFieldControl>
    <Input type="email" />
  </FormFieldControl>
  <FormFieldError>サーバー側で検証に失敗しました。</FormFieldError>
</FormField>

次は Storybook の FormField で各状態を確認するか、 アクセシビリティ で DS 全体の a11y 契約を確認する。