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

## Stimulusとは
<a href="https://stimulus.hotwired.dev/" target="_blank" rel="noopenner">Stimulus</a>とは、JavaScriptで書かれたクライアントサイドのライブラリです。
有名どころだと<a href="https://stackoverflow.design/product/guidelines/javascript/#using-the-stacks-javascript" target="_blank" rel="noopenner">Stack Overflow</a>が採用していたりします。

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

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

## タブとは
<q cite="https://www.sociomedia.co.jp/104">1画面に収まり切らない、または同時に見せるべきではない複数の情報のまとまりを、（画面遷移やスクロールを発生させずに）すばやく表示するため</q>のUIです。
<a href="https://www.sociomedia.co.jp/104" target="_blank" rel="noopenner">ソシオメディア | タブの並列切替</a>にて詳細な説明が見れます。

### a11yを考慮したタブ
<a href="https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/" target="_blank" rel="noopenner">ARIA Authoring Practices Guide (APG)</a>に全ての答えが載っています。
我々はそれに従いただ実装するのみです。

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

## HTMLを書いてみる
```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>
```

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

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

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

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

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

```html:ボタン
<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>
```
```html:パネル
<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>
```
## タブに求められる要件
<code>data-action</code>属性を付与して、要件を満たしていきます。
基本的にはボタンをクリックしたら、該当するパネルが表示される、という振る舞いでOKです。
ここではそれ以外のキーボードインタラクションを見てみます。

|キーボード操作|説明|
|---|---|
|<code>Tab</code>|タブリストにフォーカスが移動すると、アクティブなタブボタンにフォーカスされる。<br>タブボタンの次はタブパネル全体にフォーカスされる。<br><code>Shift + Tab</code>はその逆。|

|タブボタン上でのキーボード操作|説明|
|---|---|
|<code>ArrowRight</code>|次の（右の）タブボタンにフォーカスを移動する。<br>最後の（一番右の）タブボタンにフォーカスがある場合、最初の（一番左の）タブボタンにフォーカスを移動する。|
|<code>ArrowLeft</code>|前の（左の）タブボタンにフォーカスを移動する。<br>最初の（一番左の）タブボタンにフォーカスがある場合、最後の（一番右の）タブボタンにフォーカスを移動する。|
他はオプションなので省略します。
詳しくは<a href="https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/#keyboard-interaction-21" target="_blank" rel="noopenner">TabsのKeyboard Interaction</a>を参照ください。

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

## Stimulusでの実装例
<p class="codepen" data-height="300" data-default-tab="js,result" data-slug-hash="OJgXPpw" data-user="yoruaki" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/yoruaki/pen/OJgXPpw">
  QIITA_STIMULUS_ACCESIBLE_TABS</a> by yoruaki (<a href="https://codepen.io/yoruaki">@yoruaki</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

基本的にやっていることは、<code>{name}Targets</code>で対象要素を取得し、アクティブな要素に必要な属性値を付け替えているだけです。
強いて挙げるとすれば、パネルの非表示を<code>is-hide</code>というクラス名でCSSで制御しているのですが、それをJS側で<code>elm.classList.add('is-hide')</code>みたいに書くのではなく、Class APIを用いてあくまで状態はHTML側が持っているという体にしています。
<blockquote cite="https://qiita.com/nazomikan/items/4dabc63b883aa71ce83c#class%E3%82%92%E7%AE%A1%E7%90%86%E3%81%99%E3%82%8B" title="Stimulusをはじめよう#Classを管理する"><p>これは非常に単純な作用に見えますが、非常に奥深く、機能の汎化に役立ちます。</p><p>JavaScript内で直に特定のclassの付与してしまっていては、このcontrollerは同名のclassをふる時にしか再利用できませんが、class apiを利用することでHTML側を変えるだけでJavaScriptはそのまま再利用できるという汎用性を手にできるのです。</p></blockquote>

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

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

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