Stimulusでa11yを考慮したハンバーガーボタンを実装する

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

最近巷で話題のStimulusですが、バージョン3.2が11月29日にリリースされました。

私は中でも<a href="https://qiita.com/nazomikan/items/cee53c58162c51efdfae#%E3%82%AD%E3%83%BC%E3%83%9C%E3%83%BC%E3%83%89%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%81%AE%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0%E6%A7%8B%E6%96%87" target="_blank">キーボードイベントのフィルタリング構文</a>がとても使いやすいと思っていて、StimulusはHTMLを見ただけで大体の振る舞いが分かるよう実装されるので、アクセシビリティを考慮した実装がさらにやりやすくなったと感じています。

## ハンバーガーボタンとは
よく右上とか左上とかにある三本線のアレです。
ハンバーガーボタン、みんな大好きですよね。

でも実装方法は人によって様々あるように感じています。
今回はこのハンバーガーボタン（とハンバーガーメニュー）を、アクセシビリティを考慮しつつ、Stimulusで実装してみたいと思います。

### アクセシビリティを考慮したハンバーガーボタン
ハンバーガーボタンとは言っても、要はボタンとメニューの表示方法が少し特徴的なモーダルダイアログだと言えます。
つまり<a href="https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/" target="_blank">ARIA Authoring Practices GuideのDialog (Modal)</a>に従って実装すればOKです。

## HTMLを書いてみる
```html
<div>
  <div class="overlay"></div>
  <button type="button"
          aria-label="メニューを開く"
          aria-haspopup="dialog"
          aria-controls="hamburgerMenu"
  ><img src="https://example.com/bar.png" alt=""></button>
  <div role="dialog"
       aria-labelledby="menuTitle"
       aria-modal="true"
       aria-hidden="true"
       id="hamburgerMenu"
  >
    <h2 id="menuTitle">EXAMPLE MENU</h2>
    <ul>
      <li><a href="https://example.com/" tabindex="-1">MENU 1</a></li>
      <li><a href="https://example.com/" tabindex="-1">MENU 2</a></li>
      <li><a href="https://example.com/" tabindex="-1">MENU 3</a></li>
      <li><a href="https://example.com/" tabindex="-1">MENU 4</a></li>
      <li><a href="https://example.com/" tabindex="-1">MENU 5</a></li>
    </ul>
    <button type="button"
            aria-label="メニューを閉じる"
            tabindex="-1"
    ><img src="https://example.com/cross.png" alt=""></button>
  </div>
</div>
```

### 要素の説明
|要素|属性|説明|
|---|---|---|
|<code>&lt;div&gt;</code>||全体のラッパー。
|<code>&lt;div&gt;</code>|<code>class="overlay"</code>|メニュー表示時のレイヤー。
|<code>&lt;button&gt;|<code>type="button"</code>|ボタン。|
||<code>aria-label="メニューを開く"</code>|ボタンの説明。|
||<code>aria-haspopup="dialog"</code>|このボタンをクリックすると何が起こるのか。<br>今回はダイアログが開くことを示している。|
||<code>aria-controls="hamburgerMenu"</code>|どのモーダルダイアログを制御しているか。<br>その要素の<code>id</code>に紐づく。|
|<code>&lt;img&gt;</code>|<code>alt=""</code>|ハンバーガーアイコン。<br>親の<code>&lt;button&gt;</code>要素の<code>aria-label</code>で説明されているので、<code>alt</code>は空でOK。|
|<code>&lt;div&gt;|<code>role="dialog"</code>|<a href="https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA/Roles/dialog_role" target="_blank">dialogロール</a>。<br>いわゆるハンバーガーメニュー。|
||<code>aria-labelledby="menuTitle"</code>|どの要素にラベリングされているか。<br>後述の<code>&lt;h2 id="menuTitle"&gt;</code>に紐づく。<br>つまり、このメニューのラベルは「EXAMPLE MENU」となる。|
||<code>aria-modal="true&#124;false"</code>|その要素がモーダルダイアログ（<code>true</code>）かそうじゃない（<code>false</code>）か。<br><a href="https://accessible-usable.net/2021/06/entry_210606.html" target="_blank">モーダルウィンドウのコンテナ要素に aria-modal 属性を適用する</a>が分かりやすい。<br>今回この要素はモーダルなので<code>true</code>をセット。|
||<code>aria-hidden="true&#124;false"</code>|アクセシビリティツリーから消すか消さないか。<br>消すなら<code>true</code>、消さないなら<code>false</code>。<br>最初は閉じているので、音声読み上げもされないように初期値は<code>true</code>をセット。|
||<code>id="hamburgerMenu"</code>|どの<code>&lt;button&gt;</code>要素から制御されているかを示すためのID。|
|<code>&lt;h2&gt;|<code>id="menuTitle"</code>|どの要素から参照されているか（今回の場合は<code>aria-labelledby</code>を示すためのID。|
|<code>&lt;ul&gt;</code>||順序なしリスト。|
|<code>&lt;li&gt;</code>||リストアイテム。|
|<code>&lt;a&gt;</code>|<code>href="https://example.com/"</code>|リンク。リンク先は例。|
||<code>tabindex="0&#124;-1"</code>|フォーカス可能（<code>0</code>）か不可（<code>-1</code>）か。<br>メニューが閉じているときはフォーカスされたくないので、初期値は<code>-1</code>をセット。|
|<code>&lt;button&gt;|<code>type="button"</code>|ボタン。|
||<code>aria-label="メニューを閉じる"</code>|ボタンの説明。|
||<code>tabindex="0&#124;-1"</code>|フォーカス可能（<code>0</code>）か不可（<code>-1</code>）か。<br>メニューが閉じているときはフォーカスされたくないので、初期値は<code>-1</code>をセット。|

以上で意味的にはアクセシブルなモーダルダイアログの完成です。
次にStimulusで振る舞いをアタッチしていきます。
この振る舞い（動き）によって、このモーダルダイアログがいわゆるハンバーガーボタンになっていくのです。

## Hamburger Controller
与えたい振る舞いとしてはボタンのクリックやキーボード操作による挙動、範囲外クリックによるモーダルの表示非表示です。
全体のラッパーに<code>data-controller="hamburger"</code>を付与して、Hamburger Controllerを作ります。

「ハンバーガーコントローラー」ってなんだよって思いますよね。私もそう思います。

```html
<div data-controller="hamburger">
  <div class="overlay"></div>
  .
  .
  .
</div>
```

次に取得したい要素に<code>data-hamburger-target</code>属性を付与します。
今回の対象要素はオーバーレイ、ボタン、メニュー、メニュー内のフォーカス可能な要素（リンクとボタン）です。

```html:オーバーレイとボタン
<div class="overlay"
     data-hamburger-target="overlay"
></div>
<button type="button"
        aria-label="メニューを開く"
        aria-haspopup="dialog"
        aria-controls="hamburgerMenu"
        data-hamburger-target="openBtn"
>
```
```html:メニュー
<div role="dialog"
     aria-labelledby="menuTitle"
     aria-modal="true"
     aria-hidden="true"
     id="hamburgerMenu"
     data-hamburger-target="menu"
>
  <h2 id="menuTitle">EXAMPLE MENU</h2>
  <ul>
    <li>
      <a href="https://example.com/"
         tabindex="-1"
         data-hamburger-target="tabbableElm"
      >MENU 1</a>
    </li>
    .
    .
    .
  </ul>
  <button type="button"
          aria-label="メニューを閉じる"
          tabindex="-1"
          data-hamburger-target="tabbableElm closeBtn"
  ><img src="https://example.com/cross.png" alt=""></button>
</div>
```
## モーダルダイアログ（ハンバーガーボタン）に求められる要件
次に<code>data-action</code>属性を付与して要件を満たしていきます。
基本的には「開くボタン」をクリックしたらメニューが開く、「閉じるボタン」をクリックしたらメニューが閉じる、メニュー外をクリックしたらメニューが閉じる、という振る舞いでOKです。

次にキーボード操作による振る舞いですが、以下でモーダルダイアログのキーボードインタラクションについて見ていきます。

|キーボード操作|説明|
|---|---|
|<code>Tab</code>|メニュー内の次のフォーカス可能な要素にフォーカスを移動する。<br>メニュー内の最後のフォーカス可能な要素にフォーカスがある場合、最初のフォーカス可能な要素にフォーカスを移動する。|
|<code>Shift + Tab</code>|メニュー内の前のフォーカス可能な要素にフォーカスを移動する。<br>メニュー内の最初のフォーカス可能な要素にフォーカスがある場合、最後のフォーカス可能な要素にフォーカスを移動する。|
|<code>Escape</code>|メニューを閉じる。|

<code>data-action</code>属性の値は<code>{event}->{controller}#{method_name}</code>のフォーマットで記述します。
今回使うイベントは<code>click</code>と<code>keydown</code>の2つです。
冒頭でも述べましたが、バージョン3.2になったことでキーボードイベントのフィルタ構文が使えるようになりました。
つまり、<code>keydown</code>イベントを書いてJS内でキーごとに条件分岐せずとも、今回の場合だと<code>keydown.tab</code>、<code>keydown.shift+tab</code>、<code>keydown.esc</code>とそれぞれ書くことができるのです。便利！</p>

## Stimulusでの実装例
基本的にやることは、<code>{name}Target</code> or <code>{name}Targets</code>で対象要素を取得し、アクティブ、非アクティブな要素に必要な属性値を付け替えることです。

あとは<a href="https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/#issue-container-generatedID-6" target="_blank">Dialog (Modal)のNOTE</a>に従って振る舞いをアタッチしていきます。
具体的には以下の2項目です。
1. メニューを開いたときにメニュー内の最初のフォーカス可能な要素にフォーカスを移動する
2. メニューを閉じたときに「開くボタン」にフォーカスを移動する

1.の理由は、メニューが開いたときにフォーカスを移動しないと、支援技術を利用しているユーザーにはメニューが開いたのかどうかが分からない、もしくはメニューがどこにあるのかが分からないからです。

2.の理由は、メニューを閉じたときにフォーカスを移動しないと、支援技術を利用しているユーザーには一瞬どこにいるのか分からなくなるからです。
このとき、「開くボタン」にフォーカスが移動すれば、メニューを開く直前の場所に戻ってきたことになるので、現在位置を把握しやすくなります。

また、独自に以下の2項目の振る舞いも追加します。
* メニュー内でのフォーカスのループ
* メニュー内をクリックしたか、メニュー外をクリックしたかの判定

<p class="codepen" data-height="400" data-default-tab="js,result" data-slug-hash="vYroWEX" 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/vYroWEX">
  QIITA_STIMULUS_ACCESIBLE_HAMBURGER_BUTTON</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>

状態を管理する<a href="https://stimulus.hotwired.dev/reference/values" target="_blank">Values API</a>を一部利用しているのですが、もう少しスマートに実装できたかもしれません。日々勉強ですね。

あとはCSSの話になりますが、ハンバーガーボタンをクリックすると、メニューがニョキッと出現するようにしています。
また、メニュー内の「閉じるボタン」以外に、メニュー外をクリックした際にもメニューが閉じるように（引っ込むように）しています。

これだけで一気にハンバーガーボタンっぽくなりますね。

### 今回追加した<code>data-action</code>属性
実装内容の詳細については説明しませんが、今回追加した<code>data-action</code>属性の値について列挙してみます。
これらの値をHTML上から見てみて、どんな振る舞いを持っているかがおおよそ判断できるなら、Stimulusとしての目的は果たせているのではないでしょうか。
|<code>data-action</code>属性の値|説明|
|---|---|
|<code>click->hamburger#detectClickArea</code>|クリックされたら、クリックエリアを判別する。|
|<code>click->hamburger#openMenu</code>|クリックされたら、メニューを開く。|
|<code>keydown.esc->hamburger#closeMenu</code>|Escapeキーが押されたら、メニューを閉じる。|
|<code>keydown.tab->hamburger#nextMoveFocus</code>|Tabキーが押されたら、次の要素にフォーカスを移動する。|
|<code>keydown.shift+tab->hamburger#prevMoveFocus</code>|Shift + Tabキーが押されたら、前の要素にフォーカスを移動する。|
|<code>click->hamburger#closeMenu</code>|クリックされたら、メニューを閉じる。|

## 最後に
Stimulusでアクセシビリティを考慮したハンバーガーボタンを実装してみました。

最近スマホサイトなどでよく見かけるようになりましたが、こういうインタラクションなUIの裏側で、きちんとアクセシビリティも考慮し、なるべく多くの人に情報を伝えていきたいですね。