10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Stimulusでa11yを考慮したタブを実装する

Last updated at Posted at 2021-09-06

Stimulusとは

Stimulusとは、JavaScriptで書かれたクライアントサイドのライブラリです。
有名どころだとStack Overflowが採用していたりします。

StimulusはHTMLを中心に考えられていて、JavaScriptで書かれた振る舞いをHTMLから呼び出せるように設計されています。
それゆえ私のようなHTMLコーダー上がりの人間にとっても、とっつきやすいライブラリだと思います。

また、アクセシブルな実装は基本HTMLありきなので、その辺の親和性も高いと思っています。

タブとは

1画面に収まり切らない、または同時に見せるべきではない複数の情報のまとまりを、(画面遷移やスクロールを発生させずに)すばやく表示するためのUIです。
ソシオメディア | タブの並列切替にて詳細な説明が見れます。

a11yを考慮したタブ

ARIA Authoring Practices Guide (APG)に全ての答えが載っています。
我々はそれに従いただ実装するのみです。

前提として、今回はタブボタンがアクティブになると、関連する内容(タブパネル)が自動で切り替わる「Tabs With Automatic Activation」で考えていきます。

HTMLを書いてみる

<div>
  <ul role="tablist">
    <li role="presentation">
      <button type="button"
              role="tab"
              aria-selected="true"
              aria-controls="tab1-panel"
              id="tab1-btn"
              tabindex="0"
      >タブ1</button>
    </li>
    <li role="presentation">
      <button type="button"
              role="tab"
              aria-selected="false"
              aria-controls="tab2-panel"
              id="tab2-btn"
              tabindex="-1"
      >タブ2</button>
    </li>
    <li role="presentation">
      <button type="button"
              role="tab"
              aria-selected="false"
              aria-controls="tab3-panel"
              id="tab3-btn"
              tabindex="-1"
      >タブ3</button>
    </li>
  </ul>
  <div>
    <div role="tabpanel"
         aria-labelledby="tab1-btn"
         id="tab1-panel"
         tabindex="0"
    >
      <p>あいうえお</p>
    </div>
    <div role="tabpanel"
         aria-labelledby="tab2-btn"
         id="tab2-panel"
         tabindex="0"
    >
      <p>かきくけこ</p>
    </div>
    <div role="tabpanel"
         aria-labelledby="tab3-btn"
         id="tab3-panel"
         tabindex="0"
    >
      <p>さしすせそ</p>
    </div>
  </div>
</div>

要素の説明

要素 属性 説明
<div> ラッパー。
<ul> role="tablist" tabロールの付いた要素の親要素。
<li> role="presentation" <li>要素には暗黙のlistitemロールがデフォルトで付いており、listロールの付いた要素の子要素でなければならない。
今回はタブ用にtablistロールを付与しているので、listロールの子要素になれなくなってしまったので、アクセシビリティツリー上意味を持たないpresentationロールを付与している。
<button> type="button" ボタン。
role="tab" tabpanelロールを制御する。
tablistロールの付いた要素の子要素である必要がある。
aria-selected="boolean" 制御しているtabpanelロールが開いていればtrue、閉じていればfalse
aria-controls="tab1-panel" どのtabpanelロールを制御しているか。
tabpanelロールの付いた要素のidに紐づく。
id="tab1-btn" ID。
tabpanelロールの付いた要素のaria-labelledbyに紐づく。
tabindex="0|-1" 開いているパネルのボタンはフォーカス可能(0)、閉じているパネルのボタンはフォーカス不可(-1)。
<div> ラッパー。
<div> role="tabpanel" メインで見せたい情報。
aria-labelledby="tab1-btn" どのtabロールにラベリングされているか。
tabロールの付いた要素のidに紐づく。
id="tab1-panel" ID。
tabロールの付いた要素のaria-cotrolsに紐づく。
tabindex="0" 開いているパネルはフォーカス可能。
閉じている(display: none;された)パネルはそもそもフォーカスできないので、-1にする必要はない。

以上で意味的にはアクセシブルなタブの完成です。
次はこれにStimulusで振る舞いをアタッチしていきます。

Tabs Controller

与えたい振る舞いとしてはボタンのクリックやキーボード操作による挙動、それによって連動するパネルの表示非表示です。
タブ全体のラッパーにdata-controller="tabs"を付与して、Tabs Controllerを作ります。

<div data-controller="tabs">
  <ul role="tablist">
    .
    .
    .
  </ul>
</div>

次に取得したい要素にdata-tabs-target属性を付与します。
今回の対象要素はボタンとパネルなので、<button><div role="tabpanel">に付与します。

ボタン
<button type="button"
        role="tab"
        data-tabs-target="btn"
>タブ1</button>
<button type="button"
        role="tab"
        data-tabs-target="btn"
>タブ2</button>
<button type="button"
        role="tab"
        data-tabs-target="btn"
>タブ3</button>
パネル
<div role="tabpanel"
     data-tabs-target="panel"
>タブ1の内容</div>
<div role="tabpanel"
     data-tabs-target="panel"
>タブ2の内容</div>
<div role="tabpanel"
     data-tabs-target="panel"
>タブ3の内容</div>

タブに求められる要件

data-action属性を付与して、要件を満たしていきます。
基本的にはボタンをクリックしたら、該当するパネルが表示される、という振る舞いでOKです。
ここではそれ以外のキーボードインタラクションを見てみます。

キーボード操作 説明
Tab タブリストにフォーカスが移動すると、アクティブなタブボタンにフォーカスされる。
タブボタンの次はタブパネル全体にフォーカスされる。
Shift + Tabはその逆。
タブボタン上でのキーボード操作 説明
ArrowRight 次の(右の)タブボタンにフォーカスを移動する。
最後の(一番右の)タブボタンにフォーカスがある場合、最初の(一番左の)タブボタンにフォーカスを移動する。
ArrowLeft 前の(左の)タブボタンにフォーカスを移動する。
最初の(一番左の)タブボタンにフォーカスがある場合、最後の(一番右の)タブボタンにフォーカスを移動する。
他はオプションなので省略します。
詳しくはTabsのKeyboard Interactionを参照ください。

data-action属性の値は{event}->{controller}#{method_name}のフォーマットで記述しますが、今回使うイベントはclickkeydownの2つでOKです。

Stimulusでの実装例

See the Pen QIITA_STIMULUS_ACCESIBLE_TABS by yoruaki (@yoruaki) on CodePen.

基本的にやっていることは、{name}Targetsで対象要素を取得し、アクティブな要素に必要な属性値を付け替えているだけです。
強いて挙げるとすれば、パネルの非表示をis-hideというクラス名でCSSで制御しているのですが、それをJS側でelm.classList.add('is-hide')みたいに書くのではなく、Class APIを用いてあくまで状態はHTML側が持っているという体にしています。

これは非常に単純な作用に見えますが、非常に奥深く、機能の汎化に役立ちます。

JavaScript内で直に特定のclassの付与してしまっていては、このcontrollerは同名のclassをふる時にしか再利用できませんが、class apiを利用することでHTML側を変えるだけでJavaScriptはそのまま再利用できるという汎用性を手にできるのです。

最後に

Stimulusでa11yを考慮したタブを実装してみました。

アクセシビリティを考慮した実装をするとなると、ランドマークロールやウィジェット属性の付与、キーボード操作などの挙動や場合によってはDOMの見直しなども発生するかもしれません。

そんな中でも学習コストが低く、HTMLに寄り添ったライブラリであるStimulusは、強い味方になってくれるはずです。

10
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?