キーボード操作に優しくないドロップダウンの例
公開:
先日、おもしろい不具合を起こしているドロップダウンUIを見かけたので、どうしてその不具合が起こるのかを残しておきます。
まずは実際に見たUIの仕組みを再現したデモページを見てください。
「リンク集」と記載のある部分のクリックをトリガーとしてドロップダウンが動作します。
おそらく、マウスやタッチなどのポインティング・デバイスでは何事もなく動作するでしょう。
ところが、キーボードによるナビゲーションを使っていると、ドロップダウンにフォーカスした瞬間、ドロップダウンのテキストが見えなくなります。
ブラウザー下部などに表示されるリンク先URLは表示されますので、ドロップダウン内部のリンクにフォーカスが当たっているようです。
リンクとして動作はしても、リンク・テキストは見えず、操作しづらいのは間違いありません。
この不具合の面白いところは、スクリーン・リーダーでは大きな問題とならないところです。
ユーザーのアクションを元に折りたたみまたは展開するという機能は動作しませんが、リンク自体は何事もなく読み上げられます。
問題となるのは、視覚的にブラウジングしているキーボード・ユーザーです。
では、このドロップダウンのコードがどうなっているのかを見ます。
いつものとおり重要ではない部分は省略してありますので、全部見たい方はデモページでソースを表示してください。
<div class="example">
<dl class="dropdown">
<dt class="dropdown-trigger">リンク集</dt>
<dd class="dropdown-content">
<a href="...">...</a>
<!-- ... -->
</dd>
</dl>
</div>
.dropdown {
background-color: white;
height: 2rem;
overflow: hidden;
transition-property: all;
transition-duration: 0.2s;
}
.dropdown.open {
height: 14rem;
}
.dropdown-trigger {
line-height: 2rem;
}
.dropdown-content {
opacity: 0;
transition-property: all;
transition-duration: 0.2s;
}
.dropdown.open .dropdown-content {
opacity: 1;
}
.dropdown-content a {
display: block;
line-height: 2rem;
}
const dropdown = document.querySelector('.dropdown')
const trigger = dropdown.querySelector('.dropdown-trigger')
trigger.addEventListener('click', event => {
event.preventDefault()
dropdown.classList.toggle('open')
})
見てのとおり.dropdown-trigger
のクリックをトリガーに.dropdown
にクラスを付け外ししているだけです。
.dropdown
は高さが指定されており、展開時には高さが変更されます。また、折りたたみ時のドロップダウン外への影響を考慮してか、overflow: hidden
も指定されています。
ドロップダウンの中身である.dropdown-content
は、折りたたみ時はopacity: 0
によって不透明度が0
となって視覚的には隠され、展開時は不透明度が1
になって視覚的に表示されます。
それで、どうしてキーボード操作時に不具合が起きるのかです。
タブキーによるナビゲーション中にドロップダウン内部のリンクにフォーカスがあたることで、overflow: hidden
になっている.dropdown
内が強制的にスクロールされ、該当リンクが表示可能領域に移動してきます。
フォーカスが当たったリンクが.dropdown
の表示可能領域に移動してきても、それはopacity: 0
の状態なので、視覚的には何も表示されなくなる、というわけです。
そして、トリガーとなる.dropdown-trigger
はタブキーによるフォーカスが行われませんので、ドロップダウンUIの表示可能領域に戻ってこれなくなります。
opacity
プロパティは不透明度を変更するだけのプロパティなので、その値を0
にしても視覚的に見えなくなるだけで、要素ボックス自体はそこにあり、操作もできます。
また、overflow: hidden
な要素の表示領域から飛び出た要素であっても、フォーカス可能要素ならタブキーによるキーボード操作で到達できます。さらに今回の場合はそれがスクロールによって描画可能な場所にありました。
これら2つの動作を想定していないために前述のドロップダウンは不具合を起こしているのです。
これをキーボード操作でも問題なく動作するように修正する方法はいくつもあるでしょうが、とりあえず応急処置的にHTMLとCSSだけ調整してみましょう。
修正したデモをご覧ください。
<div class="example">
<dl class="dropdown">
<dt>
<button type="button" class="dropdown-trigger">リンク集</button>
</dt>
<dd class="dropdown-content">...</dd>
</dl>
</div>
.dropdown-trigger {
/* button要素特有のスタイルを消す */
}
.dropdown-content {
visibility: hidden;
opacity: 0;
transition-property: all;
transition-duration: 0.2s;
}
.dropdown.open .dropdown-content {
visibility: visible;
opacity: 1;
}
トリガーをdt
からbutton
要素へと変更しました。
これにより、トリガーにもキーボード操作でフォーカスが当たるようになりました。
今回の場合、dt
からbutton
へのクラス名の移動はしなくてもいいのですが、名前と実態が合わなくなるので移動しました。
.dropdown-content
にvisibility
プロパティを使うことで、視覚的に見えないときは非表示という状態にしてフォーカスが当たらなくなりました。visibility
プロパティはアニメーション可能なので、transition
プロパティによっていい感じに遷移アニメーションしてくれるでしょう。
トリガーはdt
要素のままでも、tabindex
属性とイベント処理でbutton
要素と似た動作を作ることはできるのですが、面倒なのでおすすめできません。
HTMLを変更できるならそちらのほうが楽です。
たとえば、button
要素が持っている機能の一部をリストで書き出してみます。
- タブキーでフォーカス可能
disabled
な状態を持てる- エンターキーでクリックできる(
keydown
時) - スペースキーでクリックできる(
keyup
時) - スペースキーはスクロール動作を起こす(
keydown
時)が、button
要素にフォーカスしているときは発生しない
今回のドロップダウンに関係するものだけでもこのようになります。
これをJavaScriptで書いていくより、button
要素を使ったほうが早いでしょう。
HTMLもCSSも、またはJavaScriptも、もっとよくできるだろう、という意見もあると思います。
今回はとりあえずドロップダウンを動作させることだけを目的にしたので、このような修正を行いました。
overflow: hidden
と、visibility
プロパティを伴わないopacity
プロパティの組み合わせで発生し、視覚的ブラウジングをするキーボード操作のユーザーのみに発生する不具合というものは珍しかったので、とても参考になりました。
また興味をひかれるUIの不具合を見かけたら紹介したいと思います。