Reactコンポーネントの構造
各Reactコンポーネントがどのような外観であるべきか、私の考え
421
13 分。
SSCF規格は、お問い合わせフォームのセマンティクス、アクセシビリティ、および動作ロジックに関する要件を規定しています。
form要素を使用する。フォームの意味付け、キーボード操作、ブラウザー標準の送信機能が利用できる。fieldsetとlegendでグループ化する。fieldsetはフィールドが論理的なまとまりを形成する場合に使用し、個々のフィールドを一つずつ囲む必要はない。legendはグループの名前を示し、fieldsetの最初の子要素として配置する。POSTでデータを送信する場合、formにmethod="post"を明示し、送信先をactionで指定する。ファイルを含むフォームにはenctype="multipart/form-data"も設定する。idと意味のあるnameを設定する。idはラベルとの関連付けに使い、nameは送信データ内のフィールド名となる。inputにはtext、email、tel、checkbox、file、number、rangeなど、適切なtypeを指定する。selectとtextareaにtype属性はなく、要素自体が操作方法を定める。requiredは、そのシナリオで本当に必須となるフィールドにのみ付ける。任意項目には使用しない。autocompleteにはname、email、tel、given-name、family-nameなど適切なトークンを指定する。ブラウザーによる保存済みデータの入力候補を支援し、フィールドの目的を示せる。placeholderは任意である。短い形式例を示す用途には使えるが、常に見えるlabelの代わりや、唯一の説明としては使わない。labelは、forとidを一致させるか、フィールドをラベル内に含めて関連付ける。ラベルを操作すると対応するコントロールにフォーカスが移る。セマンティックなフォーム構造の例:
<form action="/ja/contact" method="post" enctype="multipart/form-data" data-contact-form>
<fieldset>
<legend>連絡先</legend>
<div class="form-field">
<label for="name">名前</label>
<input
id="name"
name="name"
type="text"
autocomplete="name"
placeholder="例:アンナ"
required
/>
</div>
<div class="form-field">
<label for="email">メールアドレス</label>
<input
id="email"
name="email"
type="email"
autocomplete="email"
placeholder="name@example.com"
required
/>
</div>
<div class="form-field">
<label for="message">メッセージ</label>
<textarea
id="message"
name="message"
placeholder="お問い合わせ内容を簡潔に入力"
required
></textarea>
</div>
</fieldset>
<div class="form-consent">
<label class="form-consent__label">
<input id="data-consent" name="dataConsent" type="checkbox" required />
<span>データ処理への同意</span>
</label>
<a href="/ja/policy" target="_blank" rel="noopener noreferrer">
データ処理条件(新しいタブで開く)
</a>
</div>
<button class="form-submit" type="submit">送信</button>
<p role="status" aria-live="polite" data-form-status></p>
</form>チェックボックスとラベルのテキストをlabelで囲んでいるため、テキストを押してもチェック状態を切り替えられる。a要素はラベルの外に置き、独立した操作要素として扱う。同一サイト内のページを新しいタブで開くのは必要な場合に限り、その動作を事前に明示する。target="_blank"を使う場合はrel="noopener noreferrer"を設定する。
aria-invalid="true"を設定する。aria-describedbyでエラーメッセージを該当フィールドに関連付ける。問題と修正方法を示し、メールアドレスに必要な形式などを説明する。aria-invalid="true"を設定しない。値が修正されたら状態を更新または解除する。エラーメッセージ付きフィールドの例:
<div class="form-field form-field--error">
<label for="email">メールアドレス</label>
<input
id="email"
name="email"
type="email"
autocomplete="email"
aria-invalid="true"
aria-describedby="email-error"
required
/>
<p id="email-error">有効な形式のメールアドレスを入力してください。</p>
</div>required、type="email"、min、max、patternなどのHTML標準制約は、シナリオに合う場合に使用する。独自検証は必要なルールだけを追加する。300 msのdebounceを使えるが、フォーカス離脱時や送信前の即時検証の代わりにはならない。disabledはエラーメッセージの代わりにはならない。16px以上にし、フォーカス時のページ自動拡大を避ける。name、email、telなど正確なautocomplete値を設定する。:autofillと:-webkit-autofillに十分なコントラストの文字色と背景色を設定する。読みやすさやフォーカス表示を損なう形でシステム表示を隠さない。フィールドと自動入力の基本スタイル:
.form-field input,
.form-field select,
.form-field textarea,
.phone-field input {
font-size: 16px;
}
.form-field input:focus-visible,
.form-field select:focus-visible,
.form-field textarea:focus-visible,
.phone-field input:focus-visible {
outline: 2px solid #2457d6;
outline-offset: 2px;
}
.form-field [aria-invalid="true"] {
border-color: #b42318;
}
.form-field input:autofill,
.form-field input:-webkit-autofill,
.phone-field input:autofill,
.phone-field input:-webkit-autofill {
-webkit-text-fill-color: #161616;
box-shadow: 0 0 0 1000px #fff inset;
}例の色はコントラスト確認済みのデザイントークンに置き換える。16pxは入力欄内の文字に適用する。ラベルやプレースホルダーのサイズを変えても入力文字のサイズは変わらない。
type="tel"とautocomplete="tel"を使用する。inputmode="tel"を追加すると、モバイル端末に適切なソフトウェアキーボードを提案できる。autofocusとは異なる。ロシアでは国内プレフィックス8と国際プレフィックス+7は同じマスクとして扱わず、選択された形式に正規化する。Reactプロジェクトでは国際電話入力に@react-input/maskではなくreact-international-phoneを使用する。提示されたuseMask({ mask, replacement })は@react-input/maskのAPIであり、電話入力パッケージのAPIではない。電話入力にはPhoneInputを使い、番号の書式化、国の選択、カーソル位置の管理を行う。IPによる国判定はパッケージの組み込み機能ではなく、番号検証やユーザー自身の選択の代わりにもならない。
import { useState } from 'react';
import { PhoneInput } from 'react-international-phone';
import 'react-international-phone/style.css';
export const PhoneField = () => {
const [phone, setPhone] = useState('');
return (
<div className="phone-field">
<label htmlFor="phone">電話番号</label>
<PhoneInput
defaultCountry="ru"
forceDialCode
value={phone}
onChange={(phone) => setPhone(phone)}
inputProps={{
id: 'phone',
name: 'phone',
type: 'tel',
autoComplete: 'tel',
inputMode: 'tel',
required: true,
}}
/>
</div>
);
};react-international-phoneは入力と書式化を担当するが、現在のドキュメントでは電話番号の検証ライブラリとしては扱われていない。番号計画に基づく検証にはgoogle-libphonenumberなどの専用ライブラリを使い、サーバー側でも検証する。
import { PhoneNumberUtil } from 'google-libphonenumber';
const phoneUtil = PhoneNumberUtil.getInstance();
const isValidPhone = (phone) => {
try {
return phoneUtil.isValidNumber(phoneUtil.parseAndKeepRawInput(phone));
} catch (error) {
return false;
}
};形式や番号計画の確認だけでは、その番号が有効であることや送信者本人のものであることは証明できない。
<select>を使用する。キーボード操作やプラットフォーム標準の挙動が組み込まれている。<select>では満たせない具体的な要件がある場合に限り、独自のリストを実装する。適切なARIAパターンに沿って、アクセシブルな名前、展開・選択状態、キーボード操作、予測可能なフォーカス管理を実装する。<textarea>のサイズ変更を可能にする。縦方向の変更を制限する場合はresize: verticalなど妥当な方法を使う。無条件のresize: noneは長文入力を難しくする場合がある。accept属性はファイル選択画面で候補を示すだけであり、安全性を検証するものではない。<input type="range">を使う。意味付け、フォーカス、キーボード操作、ポインター操作が標準で備わっている。type="number"フィールドを併設できる。範囲、ステップ、現在値は一つの状態を元に同期する。min、max、step、初期valueを設定する。位置だけでは十分に理解できない場合は現在値をテキストでも示す。cursor: pointerは操作可能であることを示す補助として使える。cursor: grabやcursor: grabbingは実際にドラッグできる場合に限って使い、タッチやキーボード操作の代わりにしない。数値入力とスライダーを連動させる例:
<div class="form-field">
<label for="budget-range">予算</label>
<input
id="budget-range"
type="range"
min="0"
max="100000"
step="5000"
value="50000"
aria-describedby="budget-current-value"
/>
<output id="budget-current-value" for="budget-range">50,000円</output>
<label for="budget-value">金額を直接入力(円)</label>
<input
id="budget-value"
name="budget"
type="number"
min="0"
max="100000"
step="5000"
value="50000"
inputmode="numeric"
required
/>
</div>両方のコントロールはインターフェース内の同じ値を更新する。この例では数値フィールドだけにnameがあるため、値は一度だけ送信される。
ネイティブコントロールを同期する例:
const rangeInput = document.querySelector('#budget-range');
const numberInput = document.querySelector('#budget-value');
const budgetOutput = document.querySelector('#budget-current-value');
const formatBudget = (value) => new Intl.NumberFormat('ja-JP').format(Number(value)) + '円';
const updateBudgetOutput = () => {
budgetOutput.value = formatBudget(rangeInput.value);
};
rangeInput.addEventListener('input', () => {
numberInput.value = rangeInput.value;
updateBudgetOutput();
});
numberInput.addEventListener('change', () => {
if (!numberInput.validity.valid) return;
rangeInput.value = numberInput.value;
numberInput.value = rangeInput.value;
updateBudgetOutput();
});スライダーを動かすと数値フィールドが更新される。入力した数値は、入力確定後にmin、max、stepの制約を満たす場合に限りスライダーと同期する。
input[type="range"] {
cursor: pointer;
}
input[type="range"]:active {
cursor: grabbing;
}TabとShift+Tabで予測可能な順序に操作できるようにし、可視フォーカスを消さない。<select>はOSとブラウザーのキーボード規則に従う。独自リストでは選択肢間の移動や選択を含め、一貫したキーボード操作を実装する。Spaceで切り替える。ネイティブ要素が状態を伝えるため、重複したARIA属性は不要である。<label>を使用する。aria-labelまたはaria-labelledbyは可視ラベルがない場合や不十分な場合に限って使用する。aria-labelまたはaria-labelledbyで分かりやすい名前を付ける。<label>で囲むと、テキスト全体を押して状態を切り替えられる。ポリシーへのリンクはラベルの外に置き、操作可能なリンクを中に入れない。role="status"とaria-live="polite"を持つ独立した領域を使用できる。disabledを設定し、フォームの状態をaria-busy="true"で示すことができる。role="status"の領域で通知する。無効化されたボタン内のテキスト変更は、支援技術で確実に通知されない場合がある。ボタンを実際に無効化する場合は、利用可能な状態と視覚的に区別する。カーソルや透明度は視覚的な補助にすぎず、送信できない理由はテキストでも理解できるようにする。
.form-submit:disabled {
cursor: not-allowed;
opacity: 0.6;
}disabledの値、画面上の制約を信用しない。POSTで送信する。CSRF対策、送信頻度制限、冪等性などの適用は、脅威モデルと処理先の認証方式に応じて判断する。Promise.allSettled()は複数の独立したPromiseの結果を報告するが、処理を原子的なトランザクションにしたり、一部失敗時に完了済み処理を取り消したりはしない。何を受付完了とするか、失敗した配信をどう再試行するか、重複をどう防ぐかはサーバー側で定義する。サーバー側の処理先へフォームを送信する例:
const form = document.querySelector('[data-contact-form]');
const submitButton = form.querySelector('[type="submit"]');
const formStatus = form.querySelector('[data-form-status]');
let isSubmitting = false;
form.addEventListener('submit', (event) => {
event.preventDefault();
if (isSubmitting || !form.reportValidity()) return;
isSubmitting = true;
submitButton.disabled = true;
form.setAttribute('aria-busy', 'true');
formStatus.textContent = '送信中…';
fetch(form.action, {
method: 'POST',
body: new FormData(form),
headers: { Accept: 'application/json' },
})
.then((response) => {
if (!response.ok) throw new Error('Form submission failed');
form.reset();
formStatus.textContent = 'フォームを送信しました。';
})
.catch(() => {
formStatus.textContent = 'フォームを送信できませんでした。接続を確認して、もう一度お試しください。';
})
.then(() => {
isSubmitting = false;
submitButton.disabled = false;
form.removeAttribute('aria-busy');
});
});この例では、マークアップにdata-contact-form、送信ボタン、状態通知領域があることを前提とする。実際の処理ではサーバーエラー、タイムアウト、再試行も扱う。クライアント側のコードから外部連携先へ直接アクセスしない。
<form>が正しく使われ、<fieldset>と<legend>が関連するフィールドだけをグループ化していることを確認する。id、該当する場合は適切なtype、送信対象ならnameがあることを確認する。required、入力形式、autocompleteの動作、ラベルと任意のplaceholderの違いを確認する。Tab、Shift+Tab、チェックボックスのSpace、ネイティブと独自selectのキーボード操作も含める。各Reactコンポーネントがどのような外観であるべきか、私の考え
421
13 分。
SST規格は、タブのセマンティクス、アクセシビリティ、および動作ロジックに関する要件を規定しています
62
11 分。
SSF-U規格は、フルスクリーン表示のセマンティクス、アクセシビリティ、および動作ロジックに関する要件を規定しています
248
3 分。
SSA規格は、アコーディオンのセマンティクス、アクセシビリティ、および動作ロジックに関する要件を定めています
258
4 分。
ウェブデザインにおける致命的なミスの分析。スライダー、自動再生、重いページがコンバージョン率やGoogleでの検索順位を低下させる理由
232
2 分。
SSP規格は、ページネーションのセマンティクス、アクセシビリティ、および動作ロジックに関する要件を規定しています
240
3 分。