ブログ
UIにおける見えるけど利用できない非活性な領域の実装とinert属性について
モーダルダイアログのようなUIには、それが出現している間はダイアログの外の領域が操作不能になっているという慣習があります。ダイアログが取り扱うタスクだけにユーザーを集中させるために、あえてダイアログ以外の操作ができないようになっています。
またモーダルダイアログをはじめとしてディスクロージャーなど、ユーザーの操作に応じて表示と非表示が切り替わるUIもあります。こうしたUIは視覚的には隠れているようでも、実装としては、つねにDOM上に存在しているHTML要素の属性だけを書き換えてレンダリング結果を制御するのが一般的です。
こうしたケースでは、特定の領域を操作できないように実装を行う必要があります。CSSでdisplay:none
かvisibility:hidden
が指定されたHTML要素はそれだけで操作できない状態になりますが、それ以外の方法で非表示になったHTML要素は、利用状況によっては意図しない不完全な形で操作できてしまい、ユーザーにとっての混乱や操作の非効率性を招くことがあります。
この記事ではモーダルダイアログを例に、こういった領域をいかにして「非活性」に実装するかを解説します。
フォーカス管理の要件
ウェブページ上でキーボードのTabキーを入力すると、いくつかの種類のHTML要素はフォーカスされた状態になります。具体的にはリンクのほか、ボタン・チェックボックス・テキストフィールド等のフォームコントロールなどが対象です。フォーカスされたHTML要素はキーボードによって操作可能な状態になり、文字を入力したり、リンク先にEnterキーで移動したりできます。Tabキーを入力するたびに、ページのDOM上での並び順に従って次のフォーカス可能なHTML要素へフォーカスが移動するので、Tabキーによる操作だけでサイトを巡回することもできます。キーボードでの操作に慣れたユーザーによく利用されているほか、これがなければサイトの利用に困難をきたすこともあるアクセシビリティ的にも軽視できない機能です。
このようなキーボードによる操作は、適切なHTML要素を選択してマークアップしていればある程度は担保されますが、既存のHTML要素だけでは表現できない振る舞いについては開発者が個別にケアしなければいけません。
モーダルダイアログについて言えば、フォーカスに関しては次のような要件があります。
モーダルダイアログが開いているとき:
- 開いたタイミングで、モーダルダイアログ自体またはその中のHTML要素にフォーカスを移動させる
- モーダルダイアログの外のHTML要素にはフォーカスされないようにする
モーダルダイアログが閉じているとき:
- 閉じたタイミングで、モーダルダイアログが開く前にフォーカスされていたHTML要素にフォーカスする(フォーカス位置を復元する)
- モーダルダイアログ自体またはその中のHTML要素にはフォーカスされないようにする
キーボード操作としては、Escapeキーを入力したときにモーダルダイアログが閉じられることも必要です。そのほかの詳細な要件については「3.9 Dialog (Modal) § WAI-ARIA Authoring Practices 1.2」(日本語訳)を参考にできます。
単に開いたタイミングや閉じたタイミングでフォーカスを移動させるだけであれば、HTMLElement.focus()
でそれほど難しくなく実装できるのであまり問題にはなりません。一方で特定の領域にフォーカスされないようにするためにはいくつかのアプローチがあります。
フォーカストラップ
ユーザーのフォーカスが特定の領域から外に出られないようにJavaScriptで制御する「フォーカストラップ」と呼ばれるテクニックがあります。実装としては、ユーザーのキーボードイベントを監視し、Tabキーが入力されたタイミングでフォーカスされているHTML要素がフォーカストラップの領域内でのフォーカス順が最後だった場合には、その領域の外にあるHTML要素をフォーカスさせずに、領域内でのフォーカス順が最初のHTML要素にフォーカスさせるというものです。
たとえば次のような構造のHTMLがあるとき、「2」から「3」、「3」から「4」へのフォーカスの移動は通常通り行われますが、「4」の次は「5」でなく「2」に戻る。つまりフォーカス順がループするような実装になります。Shiftキーを併用しながらTabキーを入力すると前方向にフォーカスが移動しますが、その場合も同様に「2」から「1」ではなく「4」に移動します。
<body>
<a href="1">1</a>
<div id="dialog">
<a href="2">2</a>
<a href="3">3</a>
<a href="4">4</a>
</div>
<a href="5">5</a>
</body>
ここで少し気になる点が通常のページとのフォーカスの移動先の違いです。というのも普通、ページ内にある最後のフォーカス可能なHTML要素がフォーカスされた状態でTabキーを入力すると、フォーカスはページの先頭でなくページの外にあるアドレスバーなどのブラウザのUIへ移動します。ですがこのフォーカストラップのテクニックを利用すると、フォーカスはページの外に出ることができません。
またスクリーンリーダーではTabキー以外の操作でもフォーカスを移動できるので、フォーカストラップの対象の外の要素にもフォーカスできてしまいます。そのため外のHTML要素のフォーカスを完全には無効にできていません。
スクリーンリーダーはコンピュータの画面上の情報を読み上げる、おもに視覚障害者が利用する支援技術と呼ばれるソフトウェアです。
tabindex
の無効化
tabindex
属性を指定すると、対象のHTML要素がTabキーによってフォーカス可能になるかどうかを制御できます。属性値が0
の場合はフォーカス可能になり、-1
の場合はフォーカス不可能になります。
tabindex="-1"
を指定するとフォーカス自体ができなくなるわけではなく、Tabキーによるフォーカスが無効になるだけです。HTMLElement.focus()
などによってフォーカスすることはできます。
たとえば前述したフォーカストラップの例と同じような構造のHTMLがあるとき、フォーカスの対象にしたい「2」・「3」・「4」以外の「1」・「5」にtabindex="-1"
を指定すると、tabindex="-1"
が指定されたHTML要素はフォーカスされなくなります。
<body>
<a href="1" tabindex="-1">1</a>
<div id="dialog">
<a href="2">2</a>
<a href="3">3</a>
<a href="4">4</a>
</div>
<a href="5" tabindex="-1">5</a>
</body>
a
要素はデフォルトでtabindex="0"
が設定されているのと同じ状態になっているため、tabindex
属性を明示的に指定する必要はありません。
このようにフォーカス対象外のHTML要素にtabindex="-1"
を指定すると、結果的にフォーカストラップの意図と同様の目的が達成できて、かつページの外にもフォーカスが移動する状態を担保できます。ただしこの時点においてはまだ、スクリーンリーダーでは外のHTML要素にもフォーカスできるようになっています。
ポインターイベントの無効化
tabindex="-1"
を指定してHTML要素がTabキーによってフォーカスされない状態になっても、クリックやタップなどの操作は無効化されていません。操作に反応しないように対象外のHTML要素のイベントは無効化します。
<body>
<a
href="1"
tabindex="-1"
style="pointer-events: none;">1</a>
<div id="dialog">
<a href="2">2</a>
<a href="3">3</a>
<a href="4">4</a>
</div>
<a
href="5"
tabindex="-1"
style="pointer-events: none;">5</a>
</body>
CSSでpointer-events:none
を指定すると、そのHTML要素のポインターにまつわるイベントが発生しないようになります。リンクの場合はクリックしてもリンク先へ移動されなくなります。
モーダルダイアログの実装であれば背面に半透明のレイヤーを配置することが多く、そのレイヤーの存在によって後ろ側に配置されたHTML要素のイベントは遮断されるため、pointer-events
プロパティを利用した対応は不要になる場合もあります。
テキストの範囲選択の無効化
ページにあるテキスト上をドラッグすると特定の範囲を選択できますが、モーダルダイアログの外側の要素も選択できてしまうのは意図に反するので、この機能も無効化します。
<body>
<a
href="1"
tabindex="-1"
style="
pointer-events: none;
-webkit-user-select: none;
user-select: none;
">1</a>
<div id="dialog">
<a href="2">2</a>
<a href="3">3</a>
<a href="4">4</a>
</div>
<a
href="5"
tabindex="-1"
style="
pointer-events: none;
-webkit-user-select: none;
user-select: none;
">5</a>
</body>
user-select:none
を指定されたHTML要素はテキストの範囲選択ができなくなります。このプロパティはSafariではベンダープリフィックス付きの実装になっています。
スクリーンリーダーからの隠蔽
Tabキーによるフォーカスとポインターイベントを無効化しても、スクリーンリーダーではほぼ変わりなく利用できるままです。これにはブラウザが支援技術のために生成するアクセシビリティツリーの存在が関係しています。
おもにWAI-ARIAと呼ばれる仕様のセマンティクスにもとづいて、ブラウザによってDOMツリーとCSSから変換されたものがアクセシビリティツリーです。支援技術はOSのアクセシビリティAPIを介してアクセシビリティツリーとやり取りすることでユーザーにページの情報を提示します。多くのユーザーはページの視覚的にレンダリングされた結果から情報を取得しているのに対して、支援技術のユーザーはアクセシビリティツリーとのやり取りによってそれを行なっています。
WAI-ARIAでは、UIの種類や取り扱うテキスト情報の内容・状態・属性などを支援技術に伝えるためのセマンティクスとして、HTMLのrole
属性とaria-*
属性が定義されています。これらの指定はページの通常の振る舞いには影響を与えず、アクセシビリティツリーのみに作用します。
いくつかのHTML要素や属性はデフォルトでrole
属性やaria-*
属性と同等のセマンティクスを持ち合わせており、たとえば次の2つのマークアップは同じアクセシビリティツリーに変換されます。
<nav>...</nav>
<div role="navigation">...</div>
これらはImplicit WAI-ARIA Semantics(暗黙のWAI-ARIAセマンティクス)と呼ばれ、「ARIA in HTML」にて定義を一覧できます。
目的とするrole
属性やaria-*
属性などのWAI-ARIAセマンティクスがネイティブのHTML要素や属性に含まれている場合、明示的にWAI-ARIAの属性を指定するのではなく、ネイティブのものを利用することが推奨されています。逆に言えばWAI-ARIAを意識せずとも、HTMLの仕様に則って普通にマークアップできていれば、ある程度のWAI-ARIAセマンティクスは担保されている状態になるということでもあります。
しかし例に挙げているモーダルダイアログの外側の実装についてはそれに当てはまりません。この実装では、スクリーンリーダーにおいてもそれらを無効化するためにaria-hidden
属性を指定します。aria-hidden
属性は指定したHTML要素をアクセシビリティツリーから除外し、存在しないように取り扱うためのものです。情報がまったく読み取れない状態になってしまうので、一般に利用の際には細心の注意を払ってください。
<body>
<a
href="1"
tabindex="-1"
style="
pointer-events: none;
-webkit-user-select: none;
user-select: none;
"
aria-hidden="true">1</a>
<div id="dialog">
<a href="2">2</a>
<a href="3">3</a>
<a href="4">4</a>
</div>
<a
href="5"
tabindex="-1"
style="
pointer-events: none;
-webkit-user-select: none;
user-select: none;
"
aria-hidden="true">5</a>
</body>
こうしてスクリーンリーダーでもモーダルダイアログに則した操作性が実現できました。
inert
属性
ここまで最小限の構造のマークアップを例に解説してきましたが、実際のページは例よりもずっと複雑になるはずです。そのためこれらの要件を満たそうとすると、かなり煩雑な実装になってしまう場合もあるでしょう。
たとえばフォーカス可能なHTML要素の属性を操作するためには、まずそれらのHTML要素のリストを取得する必要があります。フォーカス可能なHTML要素はa
要素のほかにも、フォームコントロールやスクロール可能な要素などさまざまで、面倒なクエリセレクタを書くことになります。また別の用途でtabindex
属性が指定されているHTML要素があった場合には、値を書き換えた後に正しく復元するためにもとの値を記憶しておかなければいけません。aria-hidden
属性も仕様の難しさを踏まえると使わずに済むのが望ましいように思えます。
Tabキーによるフォーカスが可能なHTML要素を取得するtabbableというライブラリもありますが、完璧ではありません。
こうしたモチベーションがあり、これらの機能をひとまとめにしたinert
属性がWICGから提案されています。Chromeではフラグ付きで実装されているほか、Firefoxでの試験的実装も始まっています。このinert
属性を利用すると、モーダルダイアログが開いているときは次のようなマークアップになるように実装するだけでここまで解説してきた要件が満たせます。
<body>
<a href="1" inert>1</a>
<div id="dialog">
<a href="2">2</a>
<a href="3">3</a>
<a href="4">4</a>
</div>
<a href="5" inert>5</a>
</body>
またinert
属性は、属性が指定されたHTML要素のみではなく、子孫にまでその性質が継承されることも大きな特徴です。そのためフォーカス可能なHTML要素だけを選択するような処理も不要で、それらの根元のHTML要素の属性値だけを書き換えれば事足ります。
<body>
<div id="base" inert>
<a href="a">a</a>
<a href="b">b</a>
</div>
<div id="dialog">
<a href="x">x</a>
<a href="y">y</a>
<a href="z">z</a>
</div>
</body>
そしてこれはモーダルダイアログだけに限らず、CSSのdisplay:none
やvisibility:hidden
を用いずにHTML要素を非表示にしたい場面においても有効です。
たとえばコンテンツを開閉するディスクロージャーでは、height
プロパティの値を操作してアニメーションを実装することが多いですが、それが閉じられているときにはdisplay:none
かvisibility:hidden
も指定しなければその中のコンテンツが操作できてしまいます。これらのプロパティを指定しないのであれば、モーダルダイアログの外側と同様の処理をしてディスクロージャーの中のコンテンツの操作を無効化しなければいけません。その際にもinert
属性を利用すると簡単に「非活性化」できます。
<div class="disclosure">
<button
class="disclosure__button"
type="button"
aria-expanded="false">...</button>
<div class="disclosure__body" inert>...</div>
</div>
またドロワーなど、ページの可視領域の外からスライドするようなアニメーションを行うこともよくあります。そうした場合にも、閉じられている間はinert
属性を指定しておくことで意図しない操作を防げます。
<div class="drawer" inert>...</div>
ブラウザでのinert
属性の実装はまだしばらく安定しませんが、この機能を先駆けて試用できるようにポリフィルが用意されています。このポリフィルを読み込めば、ここまで紹介してきたような方法でinert
属性が利用できるようになります。