WAI-ARIA Authoring Practice Dialog(Modal)のフォーカス管理

この記事は、Web Accessibility Advent Calendar 2017 3日目の記事です。

はじめに

モーダルなダイアログを開いたときには、ダイアログの外側の要素は操作できないようにしなくてはなりません。マウス操作だけではなく、キーボードなどによるフォーカスもダイアログの外側にあたらないようにしなければなりません。
この記事では、WAI-ARIA Authoring Practiceの実装をもとにフォーカス管理の方法について学んだTipsをメモしておきます。

実装の基本方針

ダイアログの外側にフォーカスがあたったことを検出して、フォーカスを適切な位置に戻します。ダイアログの外側にフォーカスがあてるには、例えば以下のような操作が考えられます。

  • A) ダイアログの先頭の要素にフォーカスした状態でSHIFT+Tabを押す
  • B) ダイアログの末尾の要素にフォーカスした状態でTabを押す
  • C) ページの先頭にフォーカスしようとする (ブラウザのメニューバーなどからTabを連打するなど)

Aの場合は、フォーカスをダイアログの末尾の要素に移動させるのが望ましく、BやCの場合はフォーカスをダイアログの先頭の要素に移動させることが望ましいはずです。
つまり、もともとフォーカスしていた要素に応じて、ダイアログの外側にあたったフォーカスをどこに移動させるかが決まります。

ダイアログの外側にフォーカスをあてない

ダイアログの外側にフォーカスがあたることを防ぐには、いくつかの手法があります。

例えば「コーディングWebアクセシビリティ」という本には、ダイアログの外側のフォーカス可能な要素にvisibility:hidden;を指定する方法が紹介されています。
確かに、visibility:hiddenやdisplay:noneを指定すると、指定した要素にフォーカスはあたらなくなります。しかし、この方法は視覚的に要素が見えなくなってしまうため、デザインの観点から許容されないかもしれません。

また、ダイアログの外側にあるフォーカス可能な要素をすべて検出し、tabindex="-1"をつけるといった手法もありますが、DOMに大きく影響を与えることになります。

WAI-ARIA Authoring Practiceでは、documentオブジェクトに対するfocusイベントを検出して、イベントのコールバック関数で、ダイアログの外側にフォーカスしたかどうかを検出しています。この方法はダイアログの裏側の要素に特別なCSSや属性を割り当てることがなく、比較的簡単に実装できます。

aria.Dialog.prototype.addListeners = function() {
    document.addEventListener('focus', this.trapFocus, true);
};

aria.Dialog.prototype.trapFocus = function(event) {
    ...
    var currentDialog = aria.getCurrentDialog();
    if (currentDialog.dialogNode.contains(event.target)) {
      // ダイアログの内側にフォーカスがあたっている状態
    } else {
      // ダイアログの外側にフォーカスがあたっている状態
    }
    ...
}

addEventListenerの第3引数はtrueになっています。focusイベントはバブリングしないため、documentオブジェクトのfocusイベントはCapturing Phaseでlistenする必要があるからです。

フォーカスをループさせる

WAI-ARIA Authoring Practiceでは、ダイアログの外側にフォーカスが移動したときには、まずダイアログの内側にある最初のフォーカス可能な要素に、フォーカスを移動させます。

もしフォーカスする要素が変化していないときは、先頭要素からSHIFT+Tabを使ってダイアログの外側にフォーカスを移動させた状態であると考えられるため、ダイアログの末尾のフォーカス可能な要素に、フォーカスを移動させるようにしています。

ソースコードの該当部分

ドキュメント外へのフォーカスの移動を防ぐ

WAI-ARIA Authoring Practiceでは、ダイアログ要素の直前と直後にフォーカス可能な空のdiv要素を置いています。

<div tabindex="0"></div>
<div role="dialog" ...>
  <!-- ダイアログの内側 -->
</div>
<div tabindex="0"></div>

ソースコードには、この要素を配置している箇所に以下のコメントがついています:

// Bracket the dialog node with two invisible, focusable nodes.
// While this dialog is open, we use these to make sure that focus never
// leaves the document even if dialogNode is the first or last node.

つまり、この要素はダイアログがいかなる箇所に描画されても、ドキュメントの外側にフォーカスが移動しないように設置されています。

また、ダイアログの外側の要素がfocusイベントをlistenしていた場合、フォーカスがダイアログの外側に外れたときに、そのイベントが発火してしまう可能性があります。この緩衝地帯はイベントの発火を防ぐこともできると思います。

おわりに

ダイアログのフォーカス管理について、学んだTipsをまとめてみました。ダイアログはフォーカス管理以外にも、WAI-ARIAの対応が多く必要とされ、アクセシビリティに配慮した実装が難しいコンポーネントのひとつです。この記事が実装の一助になれば幸いです。

次回は、MarcoNakazawaさんの記事です。お楽しみに。

「インクルーシブHTML+CSS&JavaScript」を献本していただきました

今月発売される「インクルーシブHTML+CSS&JavaScript」という本を献本していただきました。一読したので、自分が面白いと感じた点をいくつか紹介しておきたいと思います。

インクルーシブ HTML + CSS &amp; JavaScriptの表紙https://www.amazon.co.jp/dp/4862463878/

訳註が濃い

表紙に「監訳者注を大幅に加筆」と書かれている通り、この本の非常に多くの訳注があります。また、単なる翻訳の補足の領域を超えて、幅広い観点から原著の指摘を行なっています。

特に素晴らしかったのは、原著の考慮していない点や実装の問題点を多数指摘していることです。また、原著が書かれたあとで策定された仕様についても都度丁寧に解説されています。例えば、第4章「ブログ記事」では、「単一のページで複数の<main>要素を使ったり…」という原著の記述について、以下のような訳註がついています。

1つの文書に複数のmainを置くことの是非については、複数の立場があります。原著が出版された2016年10月の時点では、W3のHTMLの5.0勧告には、1つのドキュメント内で複数のmain要素を使ってはならないという趣旨の記述がありました。しかし、その後のHTML5.1では...

全体として、原著者と翻訳者、複数の先生から指導を受けているような気持ちで読み進めることができ、説得力の高い印象を受けました。

単純なパターンカタログではない

この本の副題には「多様なユーザニーズに応えるフロントエンドデザインパターン」とあります。確かに、ブログ記事、ナビゲーション、ボタンなど、よくあるパターンについてインクルーシブな実装方法が書かれています。

しかしこの本は、パターンの解となる実装だけを載せているわけではなく、それぞれの実装の背後にある考え方や理由の解説に多くの紙面を割いています。例えば、ある実装を行うことで、スクリーンリーダーにはこう読まれるとか、CSSJavaScriptが読み込まれていない環境ではフォールバックして使うことができる、などといったことです。再現性のある視点が多いので、書籍で解説されていないUIパーツなどを設計する際にも応用できる内容だと思います。

広範な技術領域をカバーしている

翻訳者の前著「コーディングWebアクセシビリティ」では、主にWAI-ARIAやスクリーンリーダーの対応に関して詳しく解説がなされていました。この本は、それらの技術もおさえつつ、より広くHTMLやCSSJavaScriptについて解説されています。

自分は仕事柄、アクセシブルなコーディング方法を初学者に学んでもらう機会がしばしばありますが、まずはこの本を読んでもらったあとで、より深くWAI-ARIAについて理解するために、コーディングWebアクセシビリティを読んでもらう、といった使い方ができそうだなと感じました。

こんな人におすすめかも

アクセシビリティは重要そう!でもどうやって実装したらいいかはよくわからない」といった方には是非おすすめしたいなと思いました。また、普段からアクセシビリティに従事している方にも少なからず新しい発見がある本だと思います。