タブ切り替えを実装する時の注意点

JavaScript, Accessibility

タブ切り替えを実装する際に躓いたところ。

タブの定義

https://www.w3.org/TR/wai-aria-practices/#tabpanel

(キーボード操作については省略)

tablistのaria-label

role="tablist" を持つ要素が、VoiceOver 上でグループとして認識されなかった。
対処法としては、tablistの要素に aria-label 属性を使って適切なグループ名を与える。
これで、グループとして読み上げられるようになる。

aria-label はあらゆる要素、role に使用することができる。
aria-label 属性の使用 - アクセシビリティ | MDN

tabの選択

タブのwrapperとしてよく ul 要素が使われる。
ただし、下記のマークアップを行うと、支援技術のコントロールが li の中に入るまでボタンが押せない。

<ul role="tablist">
  <li role="tab" aria-controls="panel01" aria-selected="false">
    <button type="button">タブ1のボタン</button>
  </li>
  ...省略
</ul>

li はフォーカス対象外の要素なので、tabキーでフォーカスすることはないけれど、
role="tab" がついているので、支援技術のコントロール対象になってしまっている。
li ではなく、中の button 要素に role="tab" を指定することで回避できた。

そもそも ul 要素にする必要がなく、実際WAI-ARIA Authoring Practicesだと div 要素 1 つで囲んでいる。
以下の記事でも ul を使用しているけど、 lirole="presentation"を指定することで役割を消している。

WAI-ARIA対応のタブ型UIの作り方(React編) - ICS MEDIA

aria-controlsのエラー

下記のコードは a11y のチェッカーでエラーになる。

<>
  <div role="tablist" aria-label="タブ選択">
    {items.map(item => (
      <button
        role="tab"
        aria-controls={item.id}
        aria-selected={item.id === panel.id}
        onClick={setPanelId}
        type="button"
      >
        タブ{item.id}
      </button>
    ))}
  </div>
  <div role="tabpanel" id={`panel${panel.id}`}>パネル{panel.id}だよ</div>
</>

aria-controls に関わらず、id を参照するものは要素があらかじめ存在する必要がある。
Accessibility Tree が DOM を元に生成する都合からだと思われる。
上記のパネルの id は動的に変更され、aria-controls も変更先のものを参照できない。

対処法としてはあらかじめ全てのパネル要素を生成させた上で、
aria-hidden属性や、CSS で display: none;visibility: hidden; で非表示にするぐらい。

参照が id というのもネックだったりするけど、 将来的には Accessibility Object Model で対応できそう。
Accessibility Object Model の日本語訳と HTML5 Conference 2018 登壇 | masuP.net
masuP 氏による Accessibility Object Model 日本語訳