WordPress のメールフォームプラグイン Contact From 7。デフォルトでは、送信ボタンを押下した時のエラーメッセージや送信完了などのメッセージはフォームの下に表示されます。

この応答メッセージをフォームの上に表示して、エラーがある場合にフォームの先頭にスクロールさせるというカスタマイズがあります。よく紹介されているのが次のようなものです。

まずフォームのソースで応答メッセージを表示するショーコード [response] をフォームの先頭に入れます。

[response]
(フォーム要素)
[submit "送信する"]

そして、バリデーションエラー時に発火するイベント wpcf7invalid を使ってエラーメッセージの要素 '.wpcf7-response-output' へスクロールさせます。

document.addEventListener('wpcf7invalid', function(event) {
	var position = $('.wpcf7-response-output').offset().top;
	$('html, body').animate({
		scrollTop: position
	}, 300);
}, false );

確かにこれで送信ボタン押下時にエラーがあると、フォームの先頭に表示したエラーメッセージにスクロールするのですが、このままではフォーカス移動の問題があります。

マウス等のポインティングデバイスが使えない方がキーボードで操作している場合、フォームの先頭にスクロールしてもフォーカスは送信ボタンに残ったままなので、そのまま [Tab] キーを押すとページのフッターなど送信ボタンの次の要素にフォーカスが移動してしまうのです。

そこで、上記の JavaScript にフォーカスを移動させる処理を加えます。フォーカスを移動させるには、移動先にアンカー(id)が必要ですので、バリデーションエラー発生時にエラーメッセージに id を付与し、フォーカスできるようにします。

document.addEventListener('wpcf7invalid', function(event) {
	var $response = $('.wpcf7-response-output');

	// エラーメッセージ領域にIDを付与し、フォーカス可能にする
	$response.attr({
		id: 'cf7-error-summary',
		tabindex: '0'
	});

	var position = $response.offset().top;
	$('html, body').animate({
		scrollTop: position
	}, 300, function(){
		// フォーカスを移動
		$response.focus();
	});
});

これでスクロールに合わせてフォーカスもエラーメッセージに移動し、[Tab] キーを押すと最初のフォーム要素に移動します。(最初のエラーがある要素に移動した方がいいのかもしれませんが。)

ただ、このままでは一つ問題があり、もともと Contact From 7 自体にもスクリーンリーダー対応がなされていて、バリデーションエラーがあると以下のような HTML が出力されるのですね。

<div class="screen-reader-response">
	<p role="status" aria-live="polite" aria-atomic="true">入力内容に問題があります。確認して再度お試しください。</p>
	<ul>
		<li id="wpcf7-f24-p25-o1-ve-your_name"><a href="#your_name">入力してください。</a></li>
		<li id="wpcf7-f24-p25-o1-ve-your_email"><a href="#your_email">入力してください。</a></li>
		<li id="wpcf7-f24-p25-o1-ve-your_body"><a href="#your_body">入力してください。</a></li>
	</ul>
</div>

この HTML は画面上は表示されないようになっているのですが、エラーがあると「入力内容に問題があります。確認して再度お試しください。」の部分が aria-live 属性によって読み上げられますので、画面上に表示されているフォーカス移動したエラーメッセージと合わせて二重に読み上げられるのです。

ここで一つ疑問が。画面上に表示されるエラーメッセージには aria-hidden="true" が付いています。にも関わらず、なぜ二重に読み上げられるのか? どうやら aria-hidden="true" が付いていても、JavaScriptでその要素に focus() を当てるとスクリーンリーダーは読み上げてしまうというのが仕様のようです。

二重に読み上げてしまうことの対策として、画面上のエラーメッセージの直前にフォーカス用のダミーアンカーを追加し、そこにフォーカスするようにしました。

document.addEventListener('wpcf7invalid', function(event) {
	var $response = $('.wpcf7-response-output');

	// エラーメッセージ領域の上にダミーアンカーを追加し、フォーカス可能にする
	if (!$response.prev().is('#cf7-error-summary')) {
		$response.before('<span id="cf7-error-summary" tabindex="-1"></span>');
	}
	var $target = $('#cf7-error-summary');
	
	$target.attr({
		id: 'cf7-error-summary',
  		tabindex: '-1'
	});

	var position = $target.offset().top - 90;
	$('html, body').animate({
		scrollTop: position
	}, 300, function(){
		// フォーカスを移動
		$target.focus();
	});
});

これで、スクロールに合わせてエラーメッセージの上にフォーカスが移動し、Contact From 7 で出力される <div class="screen-reader-response"> の中の <p role="status">…</p> のエラーメッセージだけが読み上げられます。また、もともと画面上に表示されるエラーメッセージには aria-hidden="true" が付いていますので [Tab] キーを押すとこれを飛ばして最初のフォーム要素に移動します。

ちなみに、上記のバリデーションエラー時に Contact Form 7 で出力される HTML のリスト部分は、それぞれのリストの id の値が、エラーがあるフォーム要素に付与される aria-describedby 属性の値と関連付けされていて、エラーがあるフォーム要素にフォーカスが移動すると、ラベル等と合わせてこのリストの “入力してください。” が読み上げられるようになっています。
このとき、画面上に表示されている「入力してください。」は aria-hidden="true" で読み上げないようになっていますので、フォーム要素の下にあってもスクリーンリーダー上は関係ありません。