プロンプトと適用されるコンテキスト
メールアドレス、国、郵便番号、配送日、および任意の割引コードを含むチェックアウトフォームのバリデーションフローを構築してください。郵便番号のルールは国に依存します。割引コードはリモートサービスによって検証されます。価格、在庫、住所ルール、およびコードの有効性に関しては、サーバーが一貫して信頼できる情報源(authoritative)であり続けます。
このフォームは、キーボードおよび支援技術、200%のズーム表示、そしてサーバーとの往復通信(round trip)後でも機能しなければなりません。エラーを色だけで伝えてはなりません。送信に失敗した場合、入力された値を保持し、対処が必要なすべてのエラーを特定し、直接的なリカバリー経路を提供する必要があります。ネットワークの障害、期限切れのコード、および新しい非同期レスポンスが古いレスポンスを追い越す現象も、この課題の一部です。
ここでの目的は、フォームライブラリを独自開発することではありません。回答では、状態、バリデーションのタイミング、セマンティックな関係性、フォーカスポリシー、およびクライアント・サーバー間のエラーコントラクトを定義し、それらの決定事項がどのように検証されるかを示す必要があります。ネイティブのHTML制約をベースラインとし、カスタムの挙動はブラウザのセマンティクスを損なうことなく明確さを追加するものでなければなりません。
面接官が評価するポイント
第1のシグナルは「責務の分離」です。クライアント側のバリデーションは迅速なフィードバックを提供し、不要なリクエストを防ぎます。サーバー側のバリデーションは注文を受け付けるかどうかを決定します。クライアント側のrequired属性や割引後の合計金額を信用する候補者は、セキュリティと正確性に脆弱性を残すことになります。
第2のシグナルは検知ではなく「リカバリー(回復)」です。無効であることを示す境界線だけでは不十分です。各エラーにはプレーンテキスト、対応するコントロールとのプログラム的な関連付け、および修正のための指示が必要です。送信時、ユーザーには概要と予測可能なフォーカス位置が必要です。入力された有効な値はそのまま保持されなければなりません。
第3のシグナルは「タイミング」です。ユーザーがメールアドレスを入力している最中に「無効」と表示することはノイズを生み出します。まずは送信時にバリデーションを行い、フィールドでエラーが発生した後は、blur(フォーカスが外れた時)または意味のある変更時に再検証することで、修正が機能したことをユーザーが確認できるようにします。非同期バリデーションには保留中(pending)の状態と、古いレスポンスに対する保護が必要です。
最後のシグナルは「検証(テスト)」です。自動化されたアクセシビリティスキャンはラベルの欠落を検出できますが、適切な読み上げ、フォーカスの順序、サーバーレスポンス後のリカバリー、または順序が入れ替わった割引コードのレスポンスに対する保護を証明することはできません。これらにはインタラクションテストが必要です。
回答前に明確にすべき質問
- どの検証がローカルで実行されますか? 必須チェック、フォーマット、および単純なフィールド間ルールはローカルで実行できます。価格、在庫、利用資格、および最終的な割引の有効性はサーバーが管理します。
- フィードバックはいつ表示すべきですか? 送信時にすべてのブロッキングエラーを表示します。すでにエラーとなったフィールドは、blur時または意図的な変更後に再検証される場合があります。キーストロークごとには読み上げないでください。
- サーバーはページを再レンダリングしますか? 拡張されたフォーム送信と通常のフォーム送信の双方が、値を保持し、同じ構造化されたエラーをレンダリングする必要があります。
- 1つのエラーが複数のコントロールに影響することはありますか? 国と郵便番号で1つのルールを形成します。共有の指示はグループの近くに配置し、サマリーからは修正を開始するコントロールにリンクします。
- 割引コードのバリデーション中は何が起こりますか? 保留中の状態は情報提供であり、エラーではありません。現在の値とリクエストの世代(generation)によって、レスポンスがまだ有効であるかどうかが決まります。
- 失敗後、フォーカスはどこに移動すべきですか? 複数のエラーがある場合はフォームの前のサマリーにフォーカスし、単一エラーのコンパクトなケースでは無効なコントロールにフォーカスすることも許容されます。ポリシーを1つ選択し、同じ変更を2回読み上げないようにしてください。
- フィールドエラーではない失敗には何がありますか? ネットワークの停止や在庫の変更は、再試行パスを備えたフォームレベルのメッセージに属します。これらをメールアドレスや郵便番号に関連付けないでください。
- このフローは機密情報を開示しますか? 認証やアカウント復旧のフォームでは、汎用的なサーバーメッセージが必要になる場合があります。親切な修正案内によってアカウントの存在有無が漏洩してはなりません。
30秒の回答フレームワーク
「私はラベル付けされたネイティブコントロールと制約から始めますが、信頼できる情報源は常にサーバーです。無効なコントロールにはそれぞれaria-invalid="true"が付与され、aria-describedbyで参照される安定したテキストエラーが設定されます。色やアイコンは補助に過ぎません。初回送信が失敗すると、すべての無効なフィールドへのリンクを含むエラーサマリーがレンダリングされ、そのサマリーにフォーカスが移動します。入力された値はそのまま保持されます。
バリデーションは送信時に行い、その後は失敗したフィールドに対してのみblur時または意味のある変更時に再検証します。割引コードのチェックはデバウンスされ、キャンセル可能で、世代番号でタグ付けされるため、古いレスポンスが新しい値を上書きすることはありません。サーバーは既知のフィールドエラーとフォームレベルのエラーを構造化された形式で返します。自動チェックに加えて、キーボードおよびスクリーンリーダーでのリカバリー、ズームとハイコントラスト(forced colors)、サーバーとの往復通信、JavaScript無効時の送信、非同期レースのテストを実施します。」
ステップバイステップの詳細解説
ステップ 1: 状態と不変条件(Invariants)を定義する
入力値はバリデーション状態とは切り離して管理します。各フィールドは、未操作(untouched)、保留中(pending)、有効(valid)、または無効(invalid)の状態を取り、ローカライズされたテキストにマッピングされるエラーコードを持ちます。フォームレベルのエラーは個別に扱います。UIが入力途中のフィールドをエラーとして表示しないように、送信試行フラグを保持します。
不変条件は次の通りです:ラベルは常に可視であること、エラーの前に説明が存在すること、表示されるすべてのフィールドエラーがプログラム的に関連付けられていること、非表示のエラーは参照されないこと、1回の送信で明確なフォーカス移動が1回だけ発生すること、有効な入力は失敗後も維持されること、そして古い非同期レスポンスが現在の状態を変更できないこと。
ステップ 2: ARIAの前にセマンティックなコントロールを構築する
label、最も具体的なinputタイプ、required、autocomplete、および適切な長さやパターンの制約を使用します。国に依存する郵便番号ルールにはsetCustomValidity()を使用できます。値が有効になったら、直ちにカスタムメッセージを空文字列でクリアしてください。そうしないと、ブラウザが送信をブロックし続けます。
<label for="email">Email</label>
<input id="email" name="email" type="email"
aria-invalid="true"
aria-describedby="email-hint email-error">
<p id="email-hint">name@example.com</p>
<p id="email-error">Enter a valid email address.</p>checkValidity()は制約をチェックし、reportValidity()はブラウザにフィードバックの表示も要求します。プロダクトが独自のアクセシブルなメッセージをレンダリングする場合は、競合する2つのエラーシステムを表示するのではなく、invalid状態を一貫してインターセプトします。
ステップ 3: バリデーションのタイミングを慎重に選択する
初回の送信時に、すべてのフィールドを検証し、すべてのブロッキングエラーを表示します。その後は、無効なフィールドをblur時に再検証します。選択項目や、明確で完全な形式を持つ修正された値については、変更イベント(change)でより早くエラーをクリアできます。日付やメールアドレスが不完全な入力の最中に、ライブリージョン(live region)の読み上げを繰り返し送信しないでください。
フィールド間ルールは、いずれかの依存関係が変更されたときに実行されます。国が変更された場合は、郵便番号を再検証し、表示されている説明を更新します。正規化が安全かつ可逆的でない限り、ユーザーの入力をサイレントに書き換えてはなりません。前後の空白をトリムすることと、郵便番号のフォーマットを推測して書き換えることは別物です。
ステップ 4: インラインエラーとナビゲート可能なサマリーを提示する
各フィールドの横に簡潔なテキストをレンダリングし、無効な間のみaria-invalidを設定して、エラーの安定したIDを参照します。ハイコントラストモードでも視覚的な処理が利用できるように維持し、赤い境界線や警告アイコンだけでなく言葉を含めます。
複数のエラーがある送信が失敗した後は、フォームの前にタイトルの付いたサマリーを挿入します。サマリーに一時的なフォーカスターゲットを設定し、一度だけフォーカスを当て、エラー数を明記し、関連するコントロールへのリンクを提供します。リンクテキストにはフィールド名と修正内容を含める必要があります。同じイベント内で最初のフィールドにもフォーカスを移動させるようなことは避けてください。サマリーの確認が困難になります。
ステップ 5: 非同期バリデーションをレースコンディションに対して安全にする
割引コードの入力が完了するまで待機し、リクエストをデバウンスします。値が変更されたときは直前のリクエストを中断(abort)し、完了時には単調増加する世代番号を比較します。キャンセル処理が完了した後にレスポンスの処理が進んでしまうことがあるため、両方のチェックが重要です。
politeなライブリージョンを通じて、近くに「確認中」のステータスを表示します。チェックが保留されている間は割引の適用を無効化しますが、無関係なフィールドは無効化しないでください。タイムアウトはプロダクトのコントラクトに従って、再試行可能なフォームレベルまたはコードレベルのステータスとして扱い、「無効なコード」として報告してはなりません。結果は、それらを有効とする価格コンテキストと有効期限を紐付けてのみキャッシュします。
ステップ 6: 信頼できるサーバーエラーを調整・反映する
生の値をフォームの通常のアクションまたは拡張されたリクエスト経由で送信します。サーバーは再度正規化と検証を行い、合計金額を計算し、フィールドエラーコード、フォームエラーコード、および受け入れられた値などの結果を返します。クライアントは許可リスト(allowlist)にあるフィールド名のみをマッピングします。未知のキーは、セレクターやマークアップのインジェクション機会となるのを防ぐため、安全なフォームレベルのエラーとして扱います。
リジェクトされた場合は、機密情報以外のすべての入力を保持し、クライアント側の推測をサーバーの確定情報で置き換え、同じサマリーをレンダリングしてそこにフォーカスを当てます。成功した場合は明確な確認を表示し、重複した実行を防止します。送信後にレスポンスが失われた場合は、再度課金を促す前に冪等性キー(idempotency key)または注文照会を使用します。
ステップ 7: スナップショットではなくリカバリー経路を検証する
空送信、エラー1件、エラー複数件、国/郵便番号の依存関係、期限切れの配送日、割引のタイムアウト、無効な割引、コードの連続した素早い変更、サーバー側のみのリジェクト、および成功レスポンスの消失をテストします。値が保持されること、サマリーのリンクが正しいコントロールにフォーカスすること、修正されたエラーが消えること、および古い非同期レスポンスが無視されることをアサートします。
キーボードのみおよびスクリーンリーダーを使用して、手動でフローを完了させます。200%のズーム、リフロー、可視フォーカス、ハイコントラスト、ブラウザの自動入力、およびクライアントJavaScriptなしでの通常のサーバー送信を確認します。自動化されたアクセシビリティテストや単体テストはこれらのチェックを補完するものであり、読み上げやフォーカスのテストに取って代わるものではありません。
説得力のある回答例
「私はフィールドの値を、操作済み(touched)、保留中(pending)、およびエラーの状態とは独立してモデル化します。ネイティブのラベル、inputタイプ、autocompleteトークン、および制約がベースラインを提供します。カスタムのフィールド間ルールにはsetCustomValidity()を使用し、有効になった際には常にメッセージをクリアします。クライアントチェックはスピードを向上させ、サーバーは注文を受け付ける前に価格、在庫、住所、割引を再検証します。
初回の送信ですべてのブロッキングエラーが表示されます。各無効な入力はaria-invalid="true"を受け取り、aria-describedbyで視覚的な修正テキストを参照します。複数のエラーがある場合はフォームの前にサマリーをレンダリングし、一度だけフォーカスを当て、各項目を対応するコントロールにリンクします。有効な値はそのまま維持されます。その送信後は、失敗したフィールドはキー入力ごとのライブ通知なしで、blurまたは意味のある変更時に再検証されます。
割引のバリデーションはデバウンスされ、中断シグナル(abort signal)と世代番号の両方を保持します。現在の値と一致するレスポンスのみがUIを更新できます。保留中および利用不可は、無効(invalid)とは区別されます。サーバーのフィールドコードは許可リストを介してマッピングされ、全体的な失敗はフォームレベルにとどまります。自動チェックに加えて、キーボード、スクリーンリーダー、ズーム、スクリプト無効でのサーバー往復通信、競合状態、および二重送信のテストをパスした後にのみリリースします。」
よくある間違い
- 赤い境界線のみを使用する → 一部のユーザーが状態を認識できない → 修正テキスト、プログラム的な関連付け、および色以外の視覚的手がかりを追加する。
- キーストロークごとに検証する → 不完全な入力が絶え間ないノイズを生む → 送信時に検証し、その後は失敗したフィールドを有用なタイミングで再検証する。
setCustomValidity()を設定したまま放置する → 修正された入力が無効なままになる → ルールに合格したら空文字列に設定する。- サマリーと最初のフィールドの両方にフォーカスする → 読み上げとフォーカスが競合する → 意図的なフォーカス移動を1回だけ行い、ナビゲーション用のリンクを提供する。
- タイムアウトを無効なコードとして扱う → インフラの障害がユーザーの過失として誤って帰属される → 保留中、利用不可、無効を個別に表現する。
- 最後に受信したレスポンスを受け入れる → 古いバリデーションが現在の入力を上書きする → 古い処理を中断し、リクエストの世代を比較する。
- クライアントの合計金額を信用する → リクエストがUIをバイパスする可能性がある → サーバー上で再計算および検証を行う。
- 失敗後にフォームをクリアする → リカバリーが再入力作業になってしまう → 有効で非機密な値を保持し、エラー状態のみを置き換える。
フォローアップの質問と回答
フォローアップ 1: サマリーはrole="alert"を使用すべきですか?
動的に挿入されるエラーの場合、アラートまたは適切なライブリージョンがサマリーを読み上げることができます。フォーカスを当てることでもアクセス可能になります。フォーカスとassertiveなアラートの組み合わせは同じ内容を2回読み上げる可能性があるため、選択した組み合わせをテストしてください。サーバーでの完全なページ遷移時は、ページタイトルとメインの見出しにエラー数を含めることで、より早いコンテキストを提供できます。
フォローアップ 2: フォームが有効になるまで送信ボタンを無効化(disabled)にしてはいけないのはなぜですか?
無効化されたボタンは何が間違ったままなのかを覆い隠す可能性があり、一部のナビゲーションモードからは到達不能になります。ユーザーが完全なバリデーション結果を要求できるように、送信経路を利用可能なままにしておきます。実際の送信処理が実行中である間のみ無効化し、その状態を説明し、失敗した場合は復旧できるようにします。
フォローアップ 3: ライブリージョンよりもaria-describedbyが好まれるのはどのような場合ですか?
aria-describedbyは、ユーザーがそのフィールドに到達したときに、その時点でのヒントとエラーを提供します。ライブリージョンは、フォーカスがなくても意味のある動的変更を通知します。静的なインラインエラーのすべてにライブリージョンが必要なわけではありません。すべてのフィールドを一度に読み上げるのはノイズになります。まとめて通知するにはサマリーを使用し、真に非同期な変更にはpoliteなステータスを使用します。
フォローアップ 4: JavaScriptなしでサーバーレンダリングによるバリデーションをどのようにサポートしますか?
本物のフォームアクションを使用します。サーバーは、保持された値、フィールドエラーコード、フォーム前のサマリー、およびタイトルまたは見出し内のエラー数を含む同じページを返します。サマリーのリンクには安定したコントロールIDを使用します。クライアント側の拡張機能も同じエラーコントラクトを使用し、2つのモード間で乖離が生じないようにします。
フォローアップ 5: 非同期のレースコンディションを確定的にテストするにはどうすればよいですか?
2つのバリデーションPromiseを制御します。リクエストAを開始し、値を変更してからリクエストBを開始します。Bを有効として解決し、その後にAを無効として解決します。UIが依然としてBおよび現在の値を反映していることをアサートします。中断、タイムアウト、アンマウント、および保留中の再送信についても同様にテストを繰り返します。