1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Formについて学ぶ(少しだけアクセシビリティ)

Last updated at Posted at 2024-12-11

Formについて学ぶ(少しだけアクセシビリティ)

よく考えたらFormのこと何もしらなかったので

注意

想像で書いてる部分もあるので、間違ってたら直します
見つけたら教えていただければと思います

検証用環境

以下のリポジトリ、環境を使います
https://github.com/stopod/rr-chakra-conform-sample
https://qiita.com/stopod/items/42ac91a67037e849e97f
※HTMLの基礎みたいなことなのでここはなんでもいいです

やっていくこと

やらないこと

  • css関連(例外はある)
  • わからなさすぎることの深堀り

本題(Formについて)

ここからはまず以下を読みます
しばらくは下記の章ごとに進めます
https://developer.mozilla.org/ja/docs/Learn/Forms

初めてのフォーム

ひとまずサンプルのフォームを書きました

import { Form } from "react-router";
import { Route } from "./+types/sample-form";
import { Input, Stack, Textarea, Button } from "@chakra-ui/react";

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();
  console.log(formData);
  return formData;
}

export default function FormTry() {
  return (
    <Form action="/sample-form" method="post">
      <Stack gap={4} m={8}>
        <label htmlFor="name">Name:</label>
        <Input type="text" id="name" name="user_name" />

        <label htmlFor="mail">Email:</label>
        <Input type="email" id="mail" name="user_email" />

        <label htmlFor="msg">Message:</label>
        <Textarea id="msg" name="user_message" />
      </Stack>

      <Button type="submit">メッセージを送信</Button>
    </Form>
  );
}

適宜コンポーネントはreact-routerのだったりChakraのだったりに置き換えていますが、似たような挙動(だと思っている)だと思っています

以下がまとめです

  • <Form>actionに設定されたURLへデータが送信される
    (このURLにあるスクリプトがデータを受け取る)
  • nameを設定しないと送信されない
  • <labl>for(reactならhtmlFor)と<input>idを同一にしておくと、ラベルをクリックしてもinputがアクティブになる
    <label>の子要素にしても同様のことが出来るが、idは紐づけたほうが良い
    https://developer.mozilla.org/ja/docs/Web/HTML/Attributes/for

フォームの構築方法

(ここから<Form>内のコードを書くときは<Form>タグ以下を書いていきます)

<Stack gap={8}>
  <Box>
    <Heading>Payment form</Heading>
    <Text>
      Required fields are followed by
      <Mark aria-label="required" variant={"plain"} fontWeight={"bold"}>
        *
      </Mark>
      .
    </Text>
  </Box>
  <section>
    <Heading>Contact information</Heading>
    <fieldset>
      <legend>Title</legend>
      <RadioGroup name="title">
        <HStack gap={4}>
          <Radio id="title_1" value="A">
            Ace
          </Radio>
          <Radio id="title_2" value="K">
            King
          </Radio>
          <Radio id="title_3" value="Q">
            Queen
          </Radio>
        </HStack>
      </RadioGroup>
    </fieldset>
    <Box>
      <label htmlFor="name">
        <Text>
          Name:
          <Mark aria-label="required" variant={"plain"} fontWeight={"bold"}>
            *
          </Mark>
        </Text>
      </label>
      <Input type="text" id="name" name="username" required />
    </Box>
    <Box>
      <label htmlFor="mail">
        <Text>
          Email:
          <Mark aria-label="required" variant={"plain"} fontWeight={"bold"}>
            *
          </Mark>
        </Text>
      </label>
      <Input type="email" id="mail" name="usermail" required />
    </Box>
    <Box>
      <label htmlFor="pwd">
        <Text>
          Password:
          <Mark aria-label="required" variant={"plain"} fontWeight={"bold"}>
            *
          </Mark>
        </Text>
      </label>
      <Input type="password" id="pwd" name="password" required />
    </Box>
  </section>
  <section>
    <Heading>Payment information</Heading>
    <Box>
      <label htmlFor="card">
        <Text>Card type: </Text>
      </label>
      <NativeSelectRoot>
        <NativeSelectField id="card" name="usercard">
          <option value="visa">Visa</option>
          <option value="mc">Mastercard</option>
          <option value="amex">American Express</option>
        </NativeSelectField>
      </NativeSelectRoot>
    </Box>
    <Box>
      <label htmlFor="number">
        <Text>
          Card number:
          <Mark aria-label="required" variant={"plain"} fontWeight={"bold"}>
            *
          </Mark>
        </Text>
      </label>
      <Input type="tel" id="number" name="cardnumber" required />
    </Box>
    <Box>
      <label htmlFor="expiration">
        <Text>
          Expiration date:
          <Mark aria-label="required" variant={"plain"} fontWeight={"bold"}>
            *
          </Mark>
        </Text>
      </label>
      <Input
        type="text"
        id="expiration"
        name="expiration"
        required
        placeholder="MM/YY"
        pattern="^(0[1-9]|1[0-2])\/([0-9]{2})$"
      />
    </Box>
  </section>
  <section>
    <Button type="submit">Validate the payment</Button>
  </section>
</Stack>

前の章と同様に適宜Chakraのコンポーネントを使用しています
<Field, <Fieldset>がChakraにも用意されてますがこの段階ではいったん無視します

基本的なネイティブフォームコントロール

↓こんな感じでラジオボタンは複数のinputをくっつけて1つの入力要素としてる
image.png

HTML5 の入力型

<Stack gap={4} m={8}>
  <label htmlFor="email">Email:</label>
  <Input type="email" id="email" name="email" />
  
  <label htmlFor="search">Search:</label>
  <Input type="search" id="search" name="search" />
  
  <label htmlFor="tel">Tel:</label>
  <Input type="tel" id="tel" name="tel" />
  
  <label htmlFor="url">Url:</label>
  <Input type="url" id="url" name="url" />
  
  <label htmlFor="datetime-local">Datetime-local:</label>
  <Input type="datetime-local" name="datetime" id="datetime" />
  
  <label htmlFor="month">Month:</label>
  <Input type="month" name="month" id="month" />
  
  <label htmlFor="time">Time:</label>
  <Input type="time" name="time" id="time" />
  
  <label htmlFor="week">Week:</label>
  <Input type="week" name="week" id="week" />
  
  <label htmlFor="color">Color:</label>
  <Input type="color" name="color" id="color" />
  
  <Button type="submit">Enter</Button>
</Stack>

<input>にはたくさんのtypeがあるって話ですね

こんな見た目になりました
image.png

送信されるデータは以下です
image.png
weekは1週間目を基準としてそこからひたすらに加算していく感じみたいですね、
いつ使うんでしょう?でも標準に存在しているということは一定の需要はあるということなんでしょうね

telはテンキーが出てくるだけで特に入力制御はないみたいですね、スマホなどでは入力の仮想キーボードが数値だけとかになるっぽいです キーボード入力してるとちょっとよくわからないですね

日付関連は意外と細かく用意されています、週まであるのは驚きです
見た目さえ気にしなければ標準のものを使うのが良い気持ちです
image.png

その他のフォームコントロール

<Stack gap={4} m={8}>
  {/* テキストエリア */}
  <label htmlFor="textarea">Textarea:</label>
  <Textarea id="textarea" name="textarea" />
  {/* いつものセレクトボックス */}
  <label htmlFor="simple">SimpleSelect:</label>
  <NativeSelectRoot>
    <NativeSelectField id="simple" name="simple" defaultValue={"さくらんぼ"}>
      <option>バナナ</option>
      <option>さくらんぼ</option>
      <option>レモン</option>
    </NativeSelectField>
  </NativeSelectRoot>
  
  {/* グループ化されたセレクトボックス */}
  <label htmlFor="groups">GroupSelect:</label>
  <NativeSelectRoot>
    <NativeSelectField id="groups" name="groups" defaultValue={"さくらんぼ"}>
      <optgroup label="果物">
        <option>バナナ</option>
        <option>さくらんぼ</option>
        <option>レモン</option>
      </optgroup>
      <optgroup label="野菜">
        <option>人参</option>
        <option>茄子</option>
        <option>馬鈴薯</option>
      </optgroup>
    </NativeSelectField>
  </NativeSelectRoot>
  
  {/* 複数選択可能なセレクトボックス */}
  <label htmlFor="multi">MultiSelect:</label>
  <NativeSelectRoot>
    <NativeSelectField id="multi" name="multi" multiple>
      <optgroup label="果物">
        <option>バナナ</option>
        <option>さくらんぼ</option>
        <option>レモン</option>
      </optgroup>
      <optgroup label="野菜">
        <option>人参</option>
        <option>茄子</option>
        <option>馬鈴薯</option>
      </optgroup>
    </NativeSelectField>
  </NativeSelectRoot>
  
  {/* 自動補完されるセレクトボックス、入力が可能 */}
  <label htmlFor="myFruit">好きな果物は何ですか?</label>
  <Input type="text" name="myFruit" id="myFruit" list="mySuggestion" />
  <datalist id="mySuggestion">
    <option>リンゴ</option>
    <option>バナナ</option>
    <option>ブラックベリー</option>
    <option>ブルーベリー</option>
    <option>レモン</option>
    <option>ライチ</option>
    <option></option>
    <option></option>
  </datalist>
  
  {/* 自動補完されるセレクトボックス、古いブラウザ対応版 */}
  <label htmlFor="myFruit2">好きな果物は何ですか?(代替手段付き)</label>
  <Input type="text" id="myFruit2" name="fruit" list="fruitList" />
  <datalist id="fruitList">
    <label htmlFor="suggestion">または果物を選択</label>
    <NativeSelectRoot>
      <NativeSelectField id="suggestion" name="altFruit">
        <option>リンゴ</option>
        <option>バナナ</option>
        <option>ブラックベリー</option>
        <option>ブルーベリー</option>
        <option>レモン</option>
        <option>ライチ</option>
        <option></option>
        <option></option>
      </NativeSelectField>
    </NativeSelectRoot>
  </datalist>
  
  <label htmlFor="meter">meter</label>
  <meter min="0" max="100" value="75" low={33} high={66} optimum={0} id="meter">
    75
  </meter>
  
  <label htmlFor="progress">progress</label>
  <progress max="100" value="75" id="progress">
    75/100
  </progress>
  <Button type="submit">Submit</Button>
</Stack>

テキストエリア

image.png
素だとchildrenの位置にvalueを持つけどChakraだと違うみたいです
(こっちの方がわざわざ閉じタグ付けなくていいので便利)

あとcolsrowsで縦幅横幅を行、文字数で管理できるっぽいです
ただ<Stack>配下にてcolsを設定しても<Stack>のfullサイズになってるような気がします
<Stack>から外れた場所で設定したらcolsの設定通りに幅が変わった)

wrapの設定で折り返しの方法を指定できるらしい(改行コードを入れるかどうかとか)

セレクトボックス

image.png
標準にグループ化できるタグがあったんですね...
この例ではvalueを指定していないのでchildrenの値が送信されますが、valueを設定してたらvalueの値が送信されます (valueを明示的に設定しなかったらchildrenがvalueの値される、が正しい?)

複数選択可能なセレクトボックス

image.png

<select>タグにmultipleを付けると出来ます、見える量はsizeにて変更可能
multipleを付けると見え方が若干変わって、クリックしたら一覧がばって出てくるわけじゃないみたいです

ChakraのNativeSelectを使うとsizeが大きさ変更で枠を埋められてるので実質的に使用不可(使えるけど見た目的にアレ)
Chakraでこれを使うならNativeSelectじゃない方のSelectを使う必要があります

自動補完のボックス

image.png
一番意味がわからないです、Edgeだと最初はそれっぽく表示されましたが、リストにない文字列を一度入力して送信すると、それ以降リストが表示されなくなりました どういうこと
Chromeだと表示されました、でもちょっと見た目がアレ
image.png

見た目に関してはCSS関係を何もしてないだとも思いますが、おとなしくreact-selectとか違うライブラリを使った方が良い気持ちになりました、ただ標準で<datalist>なるタグがあることに気づけたのは良かったです





メーターとプログレスバーは割愛します、プログレスバーはChakraにもありました
この辺は適宜思い出して使えば良いと思います

ウェブフォームへのスタイル設定

今回は学びたいことから逸れるので省きます

フォームへの高度なスタイル設定

今回は学びたいことから逸れるので省きます

UI疑似クラス

今回は学びたいことから逸れるので省きます
省きますが読んだ感想としてはreactとかChakraとかconformとかって外部ライブラリって便利だなと
これを読んだあとだと一層ありがたみを感じますね

クライアント側のフォーム検証

フォーム検証についてをまとめます
ここではクライアント側の検証について述べられてます

image.png
※組込みフォーム検証の下に置いたスクリーンショットは「組み込みフォーム検証の利用」から引っ張りました

クライアント側の検証はユーザーのためにやっておいてね、サーバー側の検証は自分たちのためにやりなねってことですね(超要約)

組み込みフォーム検証

ひとまず組み込みフォーム検証を種類ごとに見ていきます

required

必須項目にするやつです

<Stack gap={4} m={8}>
  <label htmlFor="choose">banana と cherry のどちらが好き?</label>
  <Input id="choose" name="i-like" required />
  <Button type="submit">Submit</Button>
</Stack>
input:invalid {
  border: 2px dashed red;
}

input:valid {
  border: 2px solid black;
}

入力した状態
image.png

入力せずsubmitした状態
image.png

こんな感じで試しました
requiredを付与すると、見た目に変化はありませんが空でsubmitをしても送信されません
つまり元から<input>に備わってる機能(= 組み込み)だということなのでしょう

また、cssをあてることで空の時(= バリデーションに引っかかってる状態)では赤枠となり、それ以外では黒枠となりました
Chakra等のUIライブラリを使ってると当たり前のようにこの辺りをぽいぽいやってますが、自分で書くと地味に面倒ですね

ちなみにvalidが有効という意味で、invalidが無効という意味です
なのでcssは有効の時は黒枠で無効の時は赤枠にしてねってことですね

追加で下記のようにします

input:invalid {
  border: 2px dashed red;
}

/* ↓追加 */
input:invalid:required {
  background-image: linear-gradient(to right, pink, lightgreen);
}

input:valid {
  border: 2px solid black;
}

入力していない状態
image.png

このように<input>が無効の時のrequiredが付いてるやつという指定も出来るみたいです

pattern

入力された文字が求めてるものかをチェックするやつです

requiredに加えてpatternを追加します

<Stack gap={4} m={8}>
  <label htmlFor="choose">banana と cherry のどちらが好き?</label>
  <Input id="choose" name="i-like" required pattern="[Bb]anana|[Cc]herry" />
  <Button type="submit">Submit</Button>
</Stack>

何も入力していない状態
image.png

正規表現で引っかかった状態
image.png

正規表現で引っかかってsubmitした状態
image.png

正規表現で問題なかった状態
image.png

入力された内容に応じてバリデーションが出来るようになりました

minLength, maxLengtn

入力文字数制限です

2文字以上、10文字以下制限にしました

<Stack gap={4} m={8}>
  <label htmlFor="len">len:</label>
  <Input id="len" name="len" minLength={2} maxLength={10} />
  <Button type="submit">Submit</Button>
</Stack>

未入力状態
image.png

1文字入力状態
image.png

2文字入力状態
image.png

10文字以上入力しようとしている状態
ここから確定(Enter)すると10文字に入力が切られます
image.png

でもMDNを読んでると、これより良い実装方法があるらしく -> ここ

ブラウザーはよくテキストフィールドに期待している以上に入力させないことがあります。単に maxlength を使うよりも良いユーザーエクスペリエンスは、入力文字数のフィードバックを提供してサイズ以下でコンテンツを編集できるようにもしておくことです。 この例のひとつが、ソーシャルメディアに投稿する際の文字数制限です。これは JavaScript と maxlength を使った解決策の組み合わせ実現できます。

X(旧Twitter)にあるようなあの140文字以上入力できるけれど、140文字以上は赤字になって送信できなくなる機能のことですかね
たしかにこっちの方が「コピペ->整形->投稿」がしやすいかもしれません

min, max

<input>typenumberの際の最小値、最大値を制限するやつです
2以上10以下に設定します

<Stack gap={4} m={8}>
  <label htmlFor="num">num:</label>
  <Input type="number" id="num" name="num" min={2} max={10}/>
  <Button type="submit">Submit</Button>
</Stack>

入力していない状態
image.png

1を入力した状態
image.png

2を入力した状態
image.png

11を入力した状態
image.png

11を入力してsubmitした状態
image.png

なんか途中で気づいたんですけれども、マウスのホバー時もエラーが表示されています
minLentgth, maxLength, patternでも確認したら同様でした
入力した直後にも検証されているのと、submit時にも検証しており、
エラーを出力する箇所が2つあると見られますね

ちなみnumberにすると数値を上下するカチカチが右側につくのですが、min, maxの値を超えて変化することはなさそうです ただキーボード入力だと入力できます

ちなみに3.4など、値の範囲内の数値でも小数点ありですとエラーにみなされます
image.png

これは以下のようにstepを小数点単位にすると解消されます
image.png

これはここのメモに書いてあります

ちなみになんですが、Chakraには<NumberInput>というコンポーネントが用意されています
Webで確認した感じこちらは特に何もしなくても小数点も範囲内であれば良さそうに見えます
(実際に書いてないので不明瞭ではある)

JavaScript を使用したフォーム検証

制約検証API

以下があるよと紹介されています

  • HTMLButtonElement
  • HTMLFieldSetElement
  • HTMLInputElement
  • HTMLOutputElement
  • HTMLSelectElement
  • HTMLTextAreaElement

正確にはHTML DOM APIと呼ぶ方が正しいのでしょうか?
上記各種に以下のようなプロパティが存在してます

  • validationMessage: エラーの時のメッセージを返す
  • validity: 各種バリデーションチェックの状態やメッセージの更新等が出来る

詳細はこちらを読んでください
https://developer.mozilla.org/ja/docs/Learn/Forms/Form_validation#%E5%88%B6%E7%B4%84%E6%A4%9C%E8%A8%BC_api

エラーメッセージの書き換え

標準で出力されるエラーメッセージを変更してみます

以下がコードです

useEffect(() => {
  const email = document.getElementById("mail") as HTMLInputElement;
  if (email) {
    email.addEventListener("input", (event) => {
      if (email.validity.typeMismatch) {
        email.setCustomValidity("メールアドレスを入力してください。");
      } else {
        email.setCustomValidity("");
      }
    });
  }
}, []);

return (
  <Form action="/sample-form" method="post">
    <Stack gap={4} m={8}>
      <label htmlFor="mail">メールアドレスを教えてください:</label>
      <Input type="email" id="mail" name="mail" />
      <Button type="submit">Submit</Button>
    </Stack>
  </Form>
);

(※このコードだとレンダリングのたびにaddEventListenerがはしって追加しまくるかも
unmount?のタイミングでremoveEventListenerをしたほうが良いのでしょうね クリーンアップとかって呼ばれてるやつだと思います)

標準だとこのようなメッセージですが
image.png

書きかえることでメッセージが書き換わります
image.png

これはMDNのサンプルをReact用に書き換えたのですが、メッセージを見るとホバーの時のメッセージとsubmitした後のメッセージが異なるようにみえます

console.logを入れてブラウザから確認してみると、メッセージは次のようになっていました
(まったくわからない)
image.png
image.png

ちなみに公式ドキュメントもそうなってます
image.png

ちょっと調べたくらいだと分からなかったのでスルーします、ひとまずエラーメッセージはこのように書き換えられるということです
でもちょっとこの方法で変えるのは、美しくはないかもしれません

エラーメッセージを要素に出力する

useEffect(() => {
  const form = document.querySelector("form") as HTMLFormElement;
  const email = document.getElementById("mail") as HTMLInputElement;
  const emailError = document.querySelector("#mail + span.error")!;
  
  const handleInput = (event: Event) => {
    if (email.validity.valid) {
      emailError.textContent = "";
      emailError.className = "error";
    } else {
      showError();
    }
  };
  
  const handleSubmit = (event: Event) => {
    if (!email.validity.valid) {
      showError();
      event.preventDefault();
    }
  };
  
  const showError = () => {
    if (email.validity.valueMissing) {
      emailError.textContent = "You need to enter an email address.";
    } else if (email.validity.typeMismatch) {
      emailError.textContent = "Entered value needs to be an email address.";
    } else if (email.validity.tooShort) {
      emailError.textContent = `Email should be at least ${email.minLength} characters; you entered ${email.value.length}.`;
    }
    emailError.className = "error active";
  };
  
  email.addEventListener("input", handleInput);
  form.addEventListener("submit", handleSubmit);
  
  return () => {
    email.removeEventListener("input", handleInput);
    form.removeEventListener("submit", handleSubmit);
  };
}, []);

return (
  <Form action="/sample-form" method="post" noValidate>
    <Stack gap={4} m={8}>
      <label htmlFor="mail">
        <span>メールアドレスを入力してください。</span>
        <Input type="email" id="mail" name="mail" required minLength={8} />
        <span className="error" aria-live="polite"></span>
      </label>
      <Button type="submit">Submit</Button>
    </Stack>
  </Form>
);
input[type="email"] {
  appearance: none;

  width: 100%;
  border: 1px solid #333;
  margin: 0;

  font-family: inherit;
  font-size: 90%;

  box-sizing: border-box;
}

input:invalid {
  border-color: #900;
  background-color: #fdd;
}

input:focus:invalid {
  outline: none;
}

.error {
  width: 100%;
  padding: 0;

  font-size: 80%;
  color: white;
  background-color: #900;
  border-radius: 0 0 5px 5px;

  box-sizing: border-box;
}

.error.active {
  padding: 0.3em;
}

(※今回はクリーンアップをしてみました)

初期状態
image.png

一度入力して空にした状態
image.png

1文字入力した状態
image.png

文字数が足りない状態
image.png

制約に引っかかってない状態
image.png

初期状態だと<span>に文字は表示されていませんが、<input>の背景が赤になってますね
これは<input>requiredが付与されており、初期状態は入力がない(= エラー)ためcssで背景色等を変えてるからですかね
とはいえ初期状態は入力がないのは当たり前な気がするので、これは不自然とかもしれません

1つずつ見ていきます

<Form>noValidateが付きました

これで<Form>のsubmit時の検証をOFFにしています
image.png
ONにすると標準の検証が走ります(当たり前ですね)
image.png
OFFだとメッセージは出力されないので、この2箇所のメッセージはsubmitに関連しているということでしょうか?
ただ小さな方のメッセージがちょっとよくわかりませんね、こちらはONのときマウスカーソルをホバーするだけで表示されます うーん... やはりホバーした際のエラーメッセージの挙動がいまいちつかめませんね

エラーメッセージ出力用の<span>要素を追加

image.png
classにerrorと付けて、errorの要素をとれるようにすることが目的だと思います
また、aria-liveというオプションが付きました これは要素が更新されることを意味するそうです
https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA/Attributes/aria-live

javascriptによるエラーメッセージの出力

image.png

const emailError = document.querySelector("#mail + span.error")!;

useEffectでイベントを追加しています
ここで該当span要素のElementを取得して、制約検証APIのプロパティを参照してそれによってtextに値をいれてる感じですね やはりちょっと面倒に感じます

標準の制約のみで対応すると細かいエラーメッセージや出力箇所をいじれず、かといっていじる際はsubmitまでの一連の流れの手順の面倒を見る必要があるように思えますね

制約検証APIを使用しない例

https://developer.mozilla.org/ja/docs/Learn/Forms/Form_validation#%E7%B5%84%E3%81%BF%E8%BE%BC%E3%81%BF_api_%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%AA%E3%81%84%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E6%A4%9C%E8%A8%BC
この部分ですが、面倒なのでコードを実際に書いたりはしません
素直にバリデーション用のライブラリを使った方が良さそうですね

フォームデータの送信

今回は学びたいことから逸れるので省きます
でもactionmethod, enctypeを頭に入れておけば良さそうです

読み終えて

いろいろと考えるのが難しいので簡単にしたい気持ちがありますね
ちょっと自分の認識を言語化していきます

自分の考えるフォームの構成

フォームには大雑把に以下の要素を持つと認識しました

  • ラベル
  • 入力フィールド
  • エラーメッセージ(標準の出力、もしくは自分で要素を作る)

また、Formの要素としてはfieldsetも存在していました

こんな感じでしょうか

  • Field -> 単一のinput等から構成されるFormの入力要素の塊
  • Fieldset -> 複数のinputから構成されるFormの入力要素(というかradio, checkbox)の塊、または任意でグループ化したい場合

以下のような形です
image.png
image.png

こうするとこんな感じの実装をしておけば間違いないでしょうか

<Form action="/sample-form" method="post">
  <Stack gap={4} m={8}>
    <label htmlFor="name">
      <Text>
        Name:
        <Mark aria-label="required" variant={"plain"} fontWeight={"bold"}>
          *
        </Mark>
      </Text>
    </label>
    <Input type="text" id="name" name="username" required />
    
    <fieldset>
      <legend>Title</legend>
      <RadioGroup name="title">
        <HStack gap={4}>
          <Radio id="title_1" value="A">
            Ace
          </Radio>
          <Radio id="title_2" value="K">
            King
          </Radio>
          <Radio id="title_3" value="Q">
            Queen
          </Radio>
        </HStack>
      </RadioGroup>
    </fieldset>
    <Button type="submit">submit</Button>
  </Stack>
</Form>

今までの章から切り貼りしただけですが、シンプルなFiledとFieldsetの想定です
これでフォームの最小限を持ってると言えます
ただこれですと、エラーメッセージは標準ですしユーザーには優しくないかもしれませんね

ChakraUIを使用してFormを作る

ChakraUIでこんなものを見つけました
https://www.chakra-ui.com/docs/components/field
https://www.chakra-ui.com/docs/components/fieldset

書き換えてみましょう

<Form action="/sample-form" method="post">
  <Stack gap={4} m={8}>
    <Field label={"name"} required>
      <Input type="text" name="username" />
    </Field>
    
    <Fieldset.Root>
      <Fieldset.Legend>Title</Fieldset.Legend>
      <Fieldset.Content>
        <RadioGroup name="title">
          <HStack gap={4}>
            <Radio value="A">
              Ace
            </Radio>
            <Radio value="K">
              King
            </Radio>
            <Radio value="Q">
              Queen
            </Radio>
          </HStack>
        </RadioGroup>
      </Fieldset.Content>
    </Fieldset.Root>
    <Button type="submit">submit</Button>
  </Stack>
</Form>

ちょっとすっきりしました
見た目も悪くはないです
image.png

開発者ツールからHTMLを確認してみます

<label>for<input>idが何もしなくても同一になっています
さらにrequired<Field>につけたら<input>へ反映されていますね
image.png

必須の際はaria-labelで音を書き換えてましたが、こちらはaria-hiddenを使ってそもそも隠してるようです
ここは用途によって分かれそうです でも変な挙動でもないのでこれはこれでアリな気がしています
image.png
https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA/Attributes/aria-hidden

<Feildset>のラジオボタンも<label><input>foridが一致しています
これは<Radio>の力でしょうね
image.png

エラーメッセージを考える

このままだとエラーメッセージは標準のままですが、この<Field>errorTextがあれば出力するようになっています
こんな感じで無理やり出力させてみます
image.png

ちゃんと出てきました
image.png
さらにはaria-liveまで設定されています、このMDNのFormに出てきたaria-hogeというモノに関しては網羅できていそうですね さらにはちゃんと赤枠になっており、cssを改めて記述する必要がなくなりました

エラーメッセージもちゃんと変えられるようにしましょう
ただ素直にJavaScriptを書いていくのはちょっと憚られるので、便利なFormライブラリとしてConformを使用します

書き換えたものがこちらです

// zodで定義したスキーマ ここで検証を設定
const schema = z.object({
  name: z.string({ required_error: "必須です-1" }),
  title: z.string({ required_error: "必須です-2" }),
});

// Hooks これで各種属性の設定や検証用のjavascriptを生成してるんじゃないかなって思った
const [form, fields] = useForm({
  constraint: getZodConstraint(schema),
  shouldRevalidate: "onInput",
  onValidate: ({ formData }) => parseWithZod(formData, { schema }),
});

// 本体 Conformの公式をみて書いた fieldsetの書き方はちょっとあってるかいまいちだけど
<Form action="/sample-form" method="post" {...getFormProps(form)}>
  <Stack gap={4} m={8}>
    <Field label={"name"} required invalid={!fields.name.valid} errorText={fields.name.errors}>
      <Input {...getInputProps(fields.name, { type: "text" })} />
    </Field>
    
    <Fieldset.Root invalid={!fields.title.valid} {...getFieldsetProps(fields.title)}>
      <Fieldset.Legend>Title</Fieldset.Legend>
      <Fieldset.Content>
        <RadioGroup name="title">
          <HStack gap={4}>
            <Radio value="A" invalid={!fields.title.valid}>Ace</Radio>
            <Radio value="K" invalid={!fields.title.valid}>King</Radio>
            <Radio value="Q" invalid={!fields.title.valid}>Queen</Radio>
          </HStack>
        </RadioGroup>
      </Fieldset.Content>
      <Fieldset.ErrorText>{fields.title.errors}</Fieldset.ErrorText>
    </Fieldset.Root>
    <Button type="submit">submit</Button>
  </Stack>
</Form>

バリデーションもちゃんと問題なさそうで、エラーの文言も変更できています
image.png
またマウスカーソルをホバーしたときのエラー、submitしたときに出るエラーの2種類でることもなくなって良くなっています これはFormに設定しているgetFormPropsの返り値にnoValidation: trueが存在してるからですね

組み込みの検証についてもzodのスキーマ定義で任せてしまえば良さそうです

最後にまた確認

一応開発者ツールからHTMLを確認してみます
image.png
<Field>の方は<label>for<input>idが異なってますね
これはConformのヘルパーで<input>idを上書きしちゃってるからでしょうか

ここは実際使うときにはChakraのを加工する必要がありそうです
スニペットをいじるだけなのでたいした労力ではないと思います

多少いじることはあるとはいえ、ライブラリの力を借りるととんでもなくありがたみを感じますね
よっぽどのことがない限りはUIライブラリやFormのライブラリを公式ドキュメントをよく読んで、
最終確認で開発者ツール等にてHTMLを一応見ておくってのが良さそうな感じでしょうか
特に複数のライブラリ(UIライブラリとFormのライブラリとか)を組み合わせて作るときはちゃんとHTMLを見ておいた方が良いよってとこですね

ちなみにConformのヘルパーからは以下のそれっぽいものが渡されます

たびたびaria-hogeみたいなものが登場していましたが、これらはWAI-ARIAと呼ばれるものです
https://developer.mozilla.org/ja/docs/Learn/Accessibility/WAI-ARIA_basics
(読み方は「うぇいありあ」っぽいです)

アクセシビリティのためにHTML要素へ追加で意味を提供する一連の属性、という認識をしました
MDNにも記載されていますが、「WAI-ARIAは必要な場合のみ使用する」が重要っぽいです
基本はHTMLの要素を適切に使うことが優先でしょうね

アクセシビリティの該当欄にも以下のように記載されています

既定では、HTML は正しく使用すればアクセシブルです。

ではなぜaria-invalidaria-describedbyはConformのヘルパーから返されるのでしょうね?(設定で返さなくするようにも可能)

どうやらここで一応会話されてるようです、もともとは標準ではOFFだったようです
https://github.com/edmundhung/conform/issues/213

aria-invalidは状態を説明する属性であり、標準では状態をもっていないので追加しているといった感じでしょうか?

aria-describedbyは何かと関連付ける属性であり、ConfromではerrorIdと紐づいてます

(↓こんな感じです、getInputPropsからerrorIdが込められたaria-describedbyが渡される)
image.png
このスクリーンショットはConformのチュートリアルから拝借しました
https://ja.conform.guide/tutorial

エラーメッセージのid<input>aria-describedbyが同一のidなので紐づいています
且つ、このaria-describedbyは入力でエラーが発生したときのみConformのヘルパーから返ってきます
なのでエラーが発生してメッセージが出力されたら紐づく、みたいな感じですかね
たしかに標準のHTMLだけではちょっと表現が難しいのかもしれません


ちょっと見返すと、先ほど書いたコードもエラーの時の<input>aria-describedbyとメッセージを出力している<span>idが異なっています
image.png
まぁaria-describedbyの値はConformのfield.hoge.errorIdとなるのですが、これを<input>にしか渡していないせいですね
ここを加工すれば一致させられるかと思います、たいした労力ではないです

こんな感じでエラーの際のイメージを更新します
image.png
image.png

ちなみに前回書いた記事で<Field>を加工した<ConformFiled><label>for<ErrorText>idを受け取れるようになってるので対応できてそうです
https://qiita.com/stopod/items/42ac91a67037e849e97f#%E3%83%95%E3%82%A9%E3%83%BC%E3%83%A0%E7%94%BB%E9%9D%A2%E3%81%AE%E4%BD%9C%E6%88%90

よくわからなくても公式ドキュメントを真似ておくのは良かったみたいです
(ちゃんと理解してから真似るべきではありますが、自戒です)

終わりに

知らないことがてんこ盛りでした
他にも付けておいた方が良いアクセシビリティがあるのかもしれませんが、適宜考えて使っていくことにします

ライブラリの力は偉大ですが、使わずに進めてライブラリはどうやって実現させてるのかを想像するのはためになりました

なんか読み返すとChakraとConformの回し者みたいになった気がします
他のライブラリを使う際も同様に確認しながら進めたいと思います

検証で使ったリポジトリはここです
https://github.com/stopod/rr-chakra-conform-sample/blob/form-test/app/routes/sample-form.tsx

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?