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

タブ切り替えを実装する際に躓いたところなどです。
適切でないものがあればマサカリいただけると幸いです。

タブの定義

以下のURLを参照。
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キーでフォーカスすることはないけれど、
tab roleがついているので、支援技術のコントロール対象になってしまっている。
liではなく、中のbutton要素にtab roleを指定することで回避できた。

そもそもul要素にする必要がない気もして、実際WAI-ARIA Authoring Practicesだとdiv要素1つで囲んでいる。
ICS MEDIAの記事でもulを使用しているけど、liにrole="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 日本語訳