role=”tab”で実装したタブのキーボード操作対応

キーボード操作に対応したタブを作るポイントです。

See the Pen Keyboard-navigable tab by webdev (@webdev-jp-net) on CodePen.light

タブ押下をキーボードで実行可能にする

role="tab"liなどクリッカブルではない要素もタブにできます。

role定義により、セマンティック的にはliをタブとして宣言できるため
スクリーンリーダではタブとして認識させるようになりますが
PCブラウザのキーボード操作ではrole="tab"要素を
abuttonのようにフォーカスしEnterSpaceで操作できません。(2020年7月現在)

tabindexでフォーカス可能にしkeydownでclickをdispatchする

要素にtabindex="0"属性を追加すると、キーボードのtabキーでフォーカスをもたせられるようになります。

これをrole="tab"とあわせてキーボード操作できるタブUIを実装しようとすると、このようになりますが
このままではタブキーによるフォーカスができるようになっただけで、ボタンやセレクトボックスなどのように、スペースキーを押したらclickイベントが実行できるようにはまだなっていません。

<ul role="tablist">
  <li id="tab-a" aria-controls="tabpanel-a" role="tab" tabindex="0" aria-selected="true" >TAB A</li>
  <li id="tab-b" aria-controls="tabpanel-b" role="tab" tabindex="0" aria-selected="false" >TAB B</li>
  <li id="tab-c" aria-controls="tabpanel-c" role="tab" tabindex="0" aria-selected="false" >TAB C</li>
</ul>
<div id="tabpanel-a" aria-labelledby="tab-a" role="tabpanel" aria-hidden="false">tabpanel a</div>
<div id="tabpanel-b" aria-labelledby="tab-b" role="tabpanel" aria-hidden="true">tabpanel b</div>
<div id="tabpanel-c" aria-labelledby="tab-c" role="tabpanel" aria-hidden="true">tabpanel c</div>

参考:
tabindex – HTML: HyperText Markup Language | MDN
ARIA: Tab Role – Accessibility: Ensuring content is usable by and for everyone | MDN

keydownイベントもaddEventListenerで紐付けておく

role="tab"要素には、表示を切り替えるためのfunctionをclickだけでなくkeydownにも紐付けておくと
キーボードで選択できるようになります。

表示切替イベント内では、イベントタイプでclickkeydownを判別し分岐をいれています。

// clickなら処理を実行
let isValid = e.type === 'click';
const isValidKey = (e.code === "Space" || e.key === "Spacebar" || e.code === "Enter" || e.key === "Enter");
// keydownかつEnterかSpaceなら処理を実行
if((e.type === 'keydown') &amp;&amp; isValidKey) {
  isValid = true;
  e.preventDefault(); // spaceでの画面スクロールを阻止
}
if(isValid) {
  // タブの切替処理
}

tabキーでのフォーカス移動順を制御

role="tab"要素をkeydownイベントで選択した場合には、子要素内の最初のフォーカス可能要素へフォーカスを移動させます。

role="tab"要素と、紐づくtabpanel子要素のフォーカス可能な要素をtabキーで移動する場合、このように回遊させます。

  1. role="tab"要素を選択
  2. 紐づくtabpanel子要素でフォーカス可能なものの先頭にフォーカス移動
  3. tabpanel子要素内の末尾のフォーカス可能なものまでフォーカス移動
  4. 親となるrole="tab"要素をにフォーカスが戻る

keydownイベントをpreventDefaultで差し止め任意の要素へfocus()させる

tabpanel子要素でフォーカス可能な末尾の要素でtabキーが押下された場合は
preventDefaultでネイティブのフォーカス移動をキャンセルし
親となるrole="tab"要素へのfocus()を設定することで順番を制御できます。

tabキー押下とは反対の、shift + tab キーの遡る操作も同様に設定します。

shift + tab を判定

tabキーとshiftキーが同時に押下されているかは
shiftキーの押下を監視する専用のフラグを設けて判定します。

withShiftKey = false;

要素.addEventListener('keydown', this.setWithShift.bind(this), false);
要素.addEventListener('keyup', this.setWithShift.bind(this), false);

setWithShift(e) {
  if (e.code.match(/Shift/g) || e.key === "Shift") {
    if(e.type === 'keydown')  this.withShiftKey = true;
    if(e.type === 'keyup')  this.withShiftKey = false;
  }
}

keydownkeyupを監視し、keyupでshift押下フラグwithShiftKeyfalseにします。

あとはtabpanel子要素のフォーカス可能な先頭の要素と末尾の要素へkeydownイベントを設定し
先頭かつ shift押下中 かつ tab ならば、親となるrole="tab"にフォーカス移動
末尾かつ shift非押下 かつ tab ならば、親となるrole="tab"にフォーカス移動
となるよう設定します。

(2020年2月の記事にサンプルソースを追加したリライト)

関連記事