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さんの記事です。お楽しみに。