39
22

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.

un-T factory! XAAdvent Calendar 2022

Day 13

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

Last updated at Posted at 2022-12-13

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

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

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

1. モーダル

必要な基本機能

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

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

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

気をつけよう、キーボード操作

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

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

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

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

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

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

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

1. ライブラリに頼る

私も、私の周りもよくMicromodal.jsを利用しています。
採用される理由は主にこれです。

  • JQuery非依存
  • WAI-ARIAのガイドラインに準拠していてアクセシブル
  • 軽量

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

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

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

さてさて、どんな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"みたいにしとくってことですね。

 <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を付けてあげるだけで背景色が指定できます。

.dialog {
  &::backdrop {
    background-color: rgba(43, 62, 80, 0.7);
  }
}

3種類のメソッドも備わっていて、show()showModal()がopen属性を付与しモーダルを表示させます。
両者の違いは、show()だとモーダル意外も操作可能なモーダル
showModal()だとモーダルの裏側のコンテンツは操作できなくなります。
close()はそのままで、このメソッドを実行するとopen属性が取り除かれモーダルが閉じます。

      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();
      });

各ブラウザの実装状況はこちら

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要素の機能に寄せられるようになんとかdivaria属性をいっぱいつけて頑張っている感じがするので
これからの実装は上記記載の点も含めてdialog要素でのマークアップが主流になってくるのではないかと考えます。

2. アコーディオン

必要な基本機能

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

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

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

いざ実装!HTML構造、どうしよう…

1. input要素

方法1つ目はinput要素でマークアップする方法です。
エンジニア歴1年の私が言うのもなんですが、ちょっと古いイメージがあります。

       <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でできますね。

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

2. dl dt dd要素

次にdl dt dd要素です

        <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-hiddentrueにして、開いたらfalseに書き換えましょう。

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

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

3. details要素とsummary要素

最後にdetails要素とsummary要素のご紹介です

          <div class="acd-area">
            <details>
              <summary>好きな食べ物は何ですか?</summary>
              オムライスとカレーとお寿司と餃子です
            </details>
            <details>
              <summary>最近ハマっているお酒は何ですか?</summary>
              ハイボールとシャリキンバイスです
            </details>
          </div>

この要素で実装するメリットはコードがとてもシンプルになるということ

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

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

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

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

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

summary::-webkit-details-marker {
  display: none;
}

*三角アイコン
スクリーンショット 2022-12-13 22.36.49.png

蛇足

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

やったー!!

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

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

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

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

まとめ、 というか感想

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

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

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

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

39
22
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
39
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?