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}
のフォーマットで記述しますが、今回使うイベントはclick
とkeydown
の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は、強い味方になってくれるはずです。