アクセシビリティに配慮したUIの実装を考えてみた〜モーダルとアコーディオン〜

みなさん、サイト制作で、モーダルとかアコーディオンって作ったことありますか？

この、何かを隠して、また表示して、また隠す。（ハンバーガーメニューとかもそう）
こういう系のUIって、実装するときめちゃめちゃ考えること多いですよね。（これに限ったことじゃないけど）

今回はアクセシビリティ的な観点から
サイト上で参戦率が高い上記２つのUIにフォーカスを当て
マークアップする際、どんなことに気をつけたら良いか、備忘録的にまとめてきました。

# 1. モーダル

## 必要な基本機能
モーダルの実装をする前に、一度押さえておきたい基本機能をリストアップします。

- モーダル表示のトリガー要素をクリック、またはフォーカスが当たっている時に`enter`か`space`ボタンでモーダルが表示される
- モーダルが表示されたら、モーダル内にフォーカスが移る
- スクリーンリーダーもモーダル内のコンテンツを読み上げてくれる
- モーダルが起動中は背景はスクロールできなくなる（時と場合によるかも）
- モーダルを閉じる時は閉じるボタンか背景クリックか`escape`
- モーダルが閉じた時、モーダルを開く前の場所にフォーカスが戻っている
- モーダルの中身が見えていない時は、スクリーンリーダーを無視させる

フォーカスのケアが結構大変かも。

## 気をつけよう、キーボード操作
モーダル起動中のキーボード操作は下記のようにまとめられます。(MDNから引用)

- tab
    - モーダル内の次のフォーカス可能な要素にフォーカスを移動
    - フォーカスがモーダル内の最後のフォーカス可能な要素にある場合、フォーカスをモーダル内の最初のフォーカス可能な要素に移動
- shift + tab
    - モーダル内の前のフォーカス可能な要素にフォーカスを移動
    - モーダル内の最初のフォーカス可能な要素にフォーカスがある場合、モーダル内の最後のフォーカス可能な要素にフォーカスを移動
- escape
    - モーダルを閉じる

なんだか周りくどいですが
要はモーダルが開いたら、フォーカスは、閉じるまでモーダル内で回遊するということです。

## いざ実装！ できるだけ楽したい！

ここまで、モーダルの基本機能やキーボード操作をまとめました。

モーダル開くボタン置いて〜、これ押したらモーダルの中身を `display: block;` にして〜
モーダルの中の閉じるボタンおしたら `display: none;` にしよ♫
じゃ、絶対ケアできないですね。

とはいえ、はじめから自分で全部実装するのは果てしないので
上記の基本機能を押さえたモーダルを、いかに楽に実装するかを考えましょう。
方法は2つあると思います。

### 1. ライブラリに頼る
私も、私の周りもよく[Micromodal.js](https://micromodal.vercel.app/)を利用しています。
採用される理由は主にこれです。
- JQuery非依存
- WAI-ARIAのガイドラインに準拠していてアクセシブル
- 軽量

Micromodal.jsのドキュメントにも書いてありますが
基本機能はこちらです。

- オーバーレイクリックでモーダルを閉じる
- `esc`ボタンを押すとモーダルを閉じる
- `aria-hidden`属性でモーダルの 表示 / 非表示 切り替え
- モーダル内でのタブ フォーカスのトラッピング（前述した、フォーカスがモーダル内だけで回遊するやつ）
- モーダル切り替え前後でフォーカス位置を維持する
- モーダル内の最初のフォーカス可能な要素にフォーカスする

じゃじゃ〜ん
さっき言ったやつ全部盛りですね。

さてさて、どんなHTML構造なのかな。。。

```html
<!-- [1] -->
<div id="modal-1" aria-hidden="true">
  <!-- [2] -->
  <div tabindex="-1" data-micromodal-close>
    <!-- [3] -->
    <div role="dialog" aria-modal="true" aria-labelledby="modal-1-title" >

      <header>
        <h2 id="modal-1-title">
          Modal Title
        </h2>
        <!-- [4] -->
        <button aria-label="Close modal" data-micromodal-close></button>
      </header>

      <div id="modal-1-content">
        Modal Content
      </div>

    </div>
  </div>
</div>
```

ふむふむ。
ほぼ`div`要素で構成されていて、`aria`属性でその`div`がなんの役割をしているのかマシンに理解らせてる感じですね。


[1]の`aria-hidden`でモーダルが表示されていない時はスクリーンリーダーが読み飛ばすようにしています。モーダルが表示された時、ここがfalseになります。
[3]の`role="dialog"`でこれがダイアログ要素だと明示しています。そして`aria-modal="true"`でこれがモーダルであると明示しています。
ん？？これってダブルミーニングでは？
と思いましたが、`aria-modal`属性でその要素がモーダルであることをスクリーンリーダーなどの支援技術に示し、それ以外のコンテンツの利用を排除する (インタラクション可能な範囲を、モーダルのコンテンツに限定する) というものです。
`aria-labelledby`はモーダルのタイトルですね。
[4]の`aria-label`はモーダル内でどんな役割を担った要素なのかを明示します。これが入っているとスクリーンリーダーはたとえ`button`の中にテキストが入っていたとしても読み飛ばし、`aria-label`の中のテキストを読み上げます。
よくある x アイコンだけの閉じるボタンとかには、スクリーンリーダーユーザー向けに必ず記述しておきましょう。

WAI-ARIAの仕様に準拠したHTML構造なのはわかりましたが、アクセシブルにするために忘れちゃいけない記述が多いなあという印象です。

### 2. dialog要素を使ってみる
`dialog`要素とは、ダイアログボックスや、消すことができるアラート、インスペクター、サブウィンドウ等のような対話型コンポーネントを表します。
WAI-ARIAの仕様にも、モーダルを実装するときは`dialog`要素で実装したほうがより一層セマンティックになって良いかも！と書いてました。


特徴としては`open`属性をもってて、このつけ外しで表示非表示を行います。
なので、最初から表示させとくモーダルとかには`open="open"`みたいにしとくってことですね。
```html
 <dialog id="js-dialog" class="dialog _1" open="open">
    <div aria-labelledby="ダイアログのタイトル">
      <p>ダイアログのコンテンツ</p>
      <button id="js-dialog-close" class="button" value="close" aria-label="ダイアログを閉じる">dialogを閉じる</button>
    </div>
　　</dialog>
```
疑似要素も備わっていて、`backdrop`疑似要素に`background-color`を付けてあげるだけで背景色が指定できます。
```scss
.dialog {
  &::backdrop {
    background-color: rgba(43, 62, 80, 0.7);
  }
}
```
3種類のメソッドも備わっていて、`show()`と`showModal()`がopen属性を付与しモーダルを表示させます。
両者の違いは、`show()`だとモーダル意外も操作可能なモーダル
`showModal()`だとモーダルの裏側のコンテンツは操作できなくなります。
`close()`はそのままで、このメソッドを実行するとopen属性が取り除かれモーダルが閉じます。
```js
      const modal = document.getElementById('js-dialog');
      const open = document.getElementById('js-dialog-open');
      const close = document.getElementById('js-dialog-close');

      open.addEventListener('click', () => {
        showModal();
      });

      close.addEventListener('click', () => {
        closeModal();
      });
```
各ブラウザの実装状況は[こちら](https://caniuse.com/?search=dialog)

FirefoxやSafariは2022年の春頃実装されたばっかりのバージョンなので
まだ`dialog`要素が機能しないブラウザを利用しているユーザーがいることに注意が必要です。


なんだ残念、まだ使えないじゃん…


そんな時のためにGoogleが未実装ブラウザのためのポリフィルを提供してくれてました。
https://github.com/GoogleChrome/dialog-polyfill

`dialog`要素の基本機能をまとめると
- キーダウン操作がケアできる
- フォーカス移動のケアができる
- 非表示の時、スクリーンリーダーが読み飛ばしてくれる
- 表示された時、スクリーンリーダーがモーダルが表示されたと理解してくれる（モーダルであると読み上げてくれる）

といった感じで、特別な`aria`属性をたくさん付与しなくても
簡単にアクセシブルなモーダルが実装できちゃいました。

ただ注意したいのが
フォーカス移動において
Micromodal.jsは**完全にモーダル内でのみ回遊する**のに対し
dialog要素は**モーダル内のフォーカスできる最後の要素に達した時、次に`tab`キーを押すとブラウザのURL入力欄**に飛びます。
これは使い勝手的にメリットになる場合とデメリットになる場合があると思うので
一概にデメリットとは言えない気がします。（みなさんはどう思いますか？）

上記以外にも気をつけることはあって
- 現段階（2022年12月）ではポリフィルを入れる必要がある
- 背景クリックでモーダルが閉じない
- open属性のつけ外しで表示非表示を判定するため、アニメーションが効かない

といった問題があります。
背景クリックや、表示非表示時のアニメーションは自前のJSが必要です。

## まとめ
ここまで、なるべく楽にアクセシブルなモーダルを実装するための機能と実装方法を紹介しました。
楽に、という点にフォーカスを当てるとライブラリに頼る方に軍配があがりそうです。


ところで、こんな記述をみつけました。
https://developer.mozilla.org/ja/docs/Learn/Accessibility/WAI-ARIA_basics
> 一点忘れてはいけないのが、 WAI-ARIA は必要な場合のみ使用するという点です。 理想的には、スクリーンリーダーのユーザーの理解に必要となる意味論の提供は、常に ネイティブの HTML 機能 を使用して行うべきです。 しかし、コードの制御が限定されていたり、 HTML 要素への実装が容易ではない複雑なものを作っているなどの理由で、これが困難となるケースがあります。 そのような場合、 WAI-ARIA はアクセシビリティを向上させる上で価値のあるツールとなります。
もう一度言いますが、必要な時だけ使ってください！

これはMDN、「WAI-ARIAの基本」というページの「`いつ WAI-ARIA を使うべき？`」という項目に記載されています。

必要な時だけ使ってってめっちゃ念押してくる。
やっぱり基本はネイティブのHTML機能を使用するべきなんですね。

脳死でdivでつくって後からaria属性つけたしまくるなよってことです。

Micromodal.jsのHTML構造を紐解くと
`dialog`要素の機能に寄せられるようになんとか`div`に`aria`属性をいっぱいつけて頑張っている感じがするので
これからの実装は上記記載の点も含めて`dialog`要素でのマークアップが主流になってくるのではないかと考えます。

# 2. アコーディオン
## 必要な基本機能
さて、次はこちらもサイト内で頻出のUI、アコーディオンについて
基本機能を見ていきましょう。


- `enter`または`space`
    - 折りたたまれたパネルのアコーディオン ヘッダーにフォーカスがある場合、関連するパネルを展開します。実装が 1 つのパネルのみの展開を許可し、別のパネルが展開されている場合は、そのパネルを折りたたみます。
    - 展開されたパネルのアコーディオン ヘッダーにフォーカスがある場合、実装が折りたたみをサポートしている場合は、パネルを折りたたみます。一部の実装では、常に 1 つのパネルを展開する必要があり、展開できるパネルは 1 つだけです。そのため、折りたたみ機能はサポートされていません。
- `tab`
    - フォーカスを次のフォーカス可能な要素に移動します。アコーディオン内のすべてのフォーカス可能な要素がページTabシーケンスに含まれます。
- `shift` + `tab`
    -  フォーカスを前のフォーカス可能な要素に移動します。アコーディオン内のすべてのフォーカス可能な要素がページTabシーケンスに含まれます。
- `↓`(オプション)
    - フォーカスがアコーディオン ヘッダーにある場合、フォーカスを次のアコーディオン ヘッダーに移動します。フォーカスが最後のアコーディオン ヘッダーにある場合、何もしないか、フォーカスを最初のアコーディオン ヘッダーに移動します。
- `↑`(オプション)
    - フォーカスがアコーディオン ヘッダーにある場合、フォーカスを前のアコーディオン ヘッダーに移動します。フォーカスが最初のアコーディオン ヘッダーにある場合、何もしないか、フォーカスを最後のアコーディオン ヘッダーに移動します。
- `home`(オプション)
    - フォーカスがアコーディオン ヘッダーにある場合、フォーカスを最初のアコーディオン ヘッダーに移動します。
- `end`(オプション)
    - フォーカスがアコーディオン ヘッダーにある場合、フォーカスを最後のアコーディオン ヘッダーに移動します。

なにやらめちゃめちゃたくさんあります。
今回はオプションのところは割愛しましょう…

## いざ実装！HTML構造、どうしよう…
### 1. input要素
方法1つ目は`input`要素でマークアップする方法です。
エンジニア歴1年の私が言うのもなんですが、ちょっと古いイメージがあります。
```html
       <div class="acd-area">
          <input id="acd-check1" class="acd-check" type="checkbox">
          <label class="acd-label q" for="acd-check1">好きな食べ物はなんですか？</label>
          <div class="acd-content">
              <p>オムライスとカレーとお寿司と餃子です</p>
          </div>
          <input id="acd-check2" class="acd-check" type="checkbox">
          <label class="acd-label q" for="acd-check2">最近ハマっているお酒は何ですか？</label>
          <div class="acd-content">
              <p>ハイボールとシャリキンバイスサワーです</p>
          </div>
        </div>
```
`input`要素でつくる旨味はjsがいらないことです。
`type`属性を`checkbox`にしたり`radio`にしたりして、チェックされているかどうかをcssで判定して
アコーディオンコンテンツの表示非表示を行います。アニメーションもcssでできますね。

ただ、コードぱっと見でこれらがアコーディオンであるとちょいとわかりにくいです。
そしてキーボードフォーカスできないのでいくら`enter`や`space`を押しても受け付けてくれません。
スクリーンリーダーも開閉状態を読み上げてくれないです。

### 2. dl dt dd要素
次に`dl` `dt` `dd`要素です
```html
        <div class="acd-area">
          <dl>
            <dt class="acd-label acd-cursor q" aria-expanded="false" aria-controls="acd-panel-1">
              好きな食べ物はなんですか？
            </dt>
            <dd class="acd-target" aria-hidden="true" id="acd-panel-1">
              オムライスとカレーとお寿司と餃子です
            </dd>
          </dl>
          <dl>
            <dt class="acd-label acd-cursor q" aria-expanded="false">
              最近ハマっているお酒は何ですか？
            </dt>
            <dd class="acd-target" aria-hidden="true">
              ハイボールとシャリキンバイスサワーです
            </dd>
          </dl>
        </div>
```
こちらは見慣れていることもあり、コードをみただけでアコーディオンだとわかりやすいです。（個人の感想です）
そして、スクリーンリーダー対応のためにたくさん`aria`属性が必要になります。

`aria-expanded`でアコーディオンが開いているか、閉じているかを理解らせます。
ラベルに記載した`aria-controls`と、アコーディオンコンテンツの`id`を揃えることで両者が紐づきます。
アコーディオンコンテンツは最初は閉じているので`aria-hidden`は`true`にして、開いたら`false`に書き換えましょう。

`aria`属性の書き換えはjsで行うため、htmlもめんどくさければjsもめんどくさいですね。
（ただ開閉アニメーションはjQueryのslideToggleつかえばあっという間！）

そしてdt要素は普通`tab`キーではフォーカスされないので、キーボード操作可能にするには
`tabindex="0"`をつけてフォーカスが当たった時に`enter`か`space`が押されたら開く、みたいなjsを書くか
`div`と`button`要素の組み合わせとかでマークアップするのがいいかもしれないです。

### 3. details要素とsummary要素
最後に`details`要素と`summary`要素のご紹介です
```html
          <div class="acd-area">
            <details>
              <summary>好きな食べ物は何ですか？</summary>
              オムライスとカレーとお寿司と餃子です
            </details>
            <details>
              <summary>最近ハマっているお酒は何ですか？</summary>
              ハイボールとシャリキンバイスです
            </details>
          </div>
```
この要素で実装するメリットはコードがとてもシンプルになるということ

あと何も考えなくてもキー操作できるし
フォーカスも当たるし
スクリーンリーダーも開閉状態を判別できます。

なによりの旨味は
**サイト内検索時にも、非表示中のコンテンツに検索文字列があればアコーディオンが開いてハイライトしてくれます。**

各種ブラウザの実装状況も問題なさそうです。
https://caniuse.com/?search=details

ただ、こちらも`dialog`要素と同じように`open`属性で開閉するのでアニメーションが効きません。

ちなみに、このままではsafariで`list-style`が有効になってしまい
ブラウザ依存の三角アイコン*が表示されてしまうので、この記述を忘れないようにしないといけないです。

```scss
summary::-webkit-details-marker {
  display: none;
}
```
>*三角アイコン
![スクリーンショット 2022-12-13 22.36.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2341756/7e08c4ec-c6bc-59c5-dcfb-b8f7bc11c37f.png)


### 蛇足
ちなみに、`details`と`summary`を使えば
アコーディオンが閉じててもサイト内検索でひっかかって勝手に開くと言いましたが
`hidden="until-found"`属性を付与すれば
アコーディオンが閉じていても、隠された単語を見つけ、かつアコーディオンを開くことができます。

やったー！！

と言いたいところですが、今のところChromeとEdgeにしか対応してなかったです涙
https://caniuse.com/?search=until-found

なにやら、この属性を付与された要素には`beforematch`というイベントが使えるようになり、
このイベント内で、サイト内検索でひっかかった時にアコーディオンを開いてごにょごにょ…
的なjsを書けば`details`と`summary`要素のような実装ができるらしいです。

興味のある方はこちら！
https://developer.mozilla.org/en-US/docs/Web/API/Element/beforematch_event

私も引き続き勉強します。


# まとめ、 というか感想
ここまで、アコーディオンをつくるのに必要な機能と実装方法を紹介しました。
`details`と`summary`要素使ったことある方いらっしゃいますか？
できればネイティブなHTMLの機能を使って実装したいけど、なんだか痒いところに手が届かないなというのが個人の感想です。

結局のところ、アコーディオンに関してはどんな実装方法をとっても
アクセシブルにしようとすればどこかで苦しい思いをしなければならないので
どの要素を選択するのかは、**デザインによる**のかもしれないです。

アコーディオンがサイト上で登場する理由第1位は、**コンテンツが長くなるから**。
その意味合いを考えると`details`と`summary`は英単語の意味からとってもあまり適してない気がします。
逆にQ & Aのセクションだったり、大きなセクションの詳細部分に使うのであれば使っても良いかもしれないです。

モーダルを実装する場合にも言えることですが
マークアップする際は使用する要素が、デザイン上の意味合いと合致しているかを意識しながら実装していけるよう
日々勉強が必要だなと思いました。