Unformed Building

キーボード操作に優しくないドロップダウンの例

公開:

パーマリンク

先日、おもしろい不具合を起こしているドロップダウン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-contentvisibilityプロパティを使うことで、視覚的に見えないときは非表示という状態にしてフォーカスが当たらなくなりました。
visibilityプロパティはアニメーション可能なので、transitionプロパティによっていい感じに遷移アニメーションしてくれるでしょう。

トリガーはdt要素のままでも、tabindex属性とイベント処理でbutton要素と似た動作を作ることはできるのですが、面倒なのでおすすめできません。
HTMLを変更できるならそちらのほうが楽です。
たとえば、button要素が持っている機能の一部をリストで書き出してみます。

  • タブキーでフォーカス可能
  • disabledな状態を持てる
  • エンターキーでクリックできる(keydown時)
  • スペースキーでクリックできる(keyup時)
  • スペースキーはスクロール動作を起こす(keydown時)が、button要素にフォーカスしているときは発生しない

今回のドロップダウンに関係するものだけでもこのようになります。
これをJavaScriptで書いていくより、button要素を使ったほうが早いでしょう。

HTMLもCSSも、またはJavaScriptも、もっとよくできるだろう、という意見もあると思います。
今回はとりあえずドロップダウンを動作させることだけを目的にしたので、このような修正を行いました。

overflow: hiddenと、visibilityプロパティを伴わないopacityプロパティの組み合わせで発生し、視覚的ブラウジングをするキーボード操作のユーザーのみに発生する不具合というものは珍しかったので、とても参考になりました。
また興味をひかれるUIの不具合を見かけたら紹介したいと思います。