JavaScriptでアクセシビリティに対応したアコーデオンメニューを作ってみた

  • ブックマーク
  • -
    コピー

はじめに

素のJavaScript、生のJavaScript、Vanilla JSと色々な表現がありますが、Jqueryを使わないアコーディオンメニューの実装を考えてみました。
また、Webアクセシビリティにも対応できるようにWAI-ARIAをいくつか設定しています。

概要

  • 素のJavaScriptを使った実装。
  • ES6(ES2015)で実装されたconst・letを使ったモダンな書き方と、ES5以前からあるvarを使ったレガシーな書き方の2種類を紹介します。
  • WAI-ARIAはaria-expanded、aria-controls、aria-hiddenを使います。
  • aria-expandedとaria-hiddunはsetAtributeで設定を書き換えたいのでtoggleは使わずif文でaddとremoveを使ってオンオフをしています。

HTMLとCSS

index.html

ボタンのオンオフはbuttunタグで実装します。
このbuttonタグの中に質問を書き、WAI-ARIAの属性値、aria-expadedとaria-contlolsを答えが閉じている状態を表すように設定しておきます。
答えはpタグでaria-hiddenを閉じている状態を表すようにし、質問のaria-controlsと対になるようにidを設定します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>アコーディオン</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <section id="qa">
        <h2>Q&A</h2>
        <div class="achodion">
            <div class="qa-visible">
                <h3><button type="button" class="qa-question" aria-expanded="false" aria-controls="panel-01">ここに質問が入ります。<span></span></button></h3>
                <p class="qa-answer" aria-hidden="true" id="panel-01">ここに答えが入ります。</p>
            </div><!-- /qa-visible -->
            <div class="qa-visible">
                <h3><button type="button" class="qa-question" aria-expanded="false" aria-controls="panel-02">ここに質問が入ります。<span></span></button></h3>
                <p class="qa-answer" aria-hidden="true" id="panel-02">ここに答えが入ります。</p>
            </div><!-- /qa-visible -->
            <div class="qa-visible">
                <h3><button type="button" class="qa-question" aria-expanded="false" aria-controls="panel-03">ここに質問が入ります。<span></span></button></h3>
                <p class="qa-answer" aria-hidden="true" id="panel-03">ここに答えが入ります。</p>
            </div><!-- /qa-visible -->
        </div><!-- /achodion -->
    </section><!-- /qa -->
    <script src="script.js"></script>
</body>
</html>

style.css

class qa-visibleに class activeがついていたら class qa-answerを表示、ついていなかったら非表示になるようにCSSを準備しておきます。

/* section - Q & A - */
#qa h2 {
    font-size: 1.25rem;
    text-align: center;
}
.achodion {
    max-width: 760px;
    background-color: #fff;
    margin: 0 auto;
    padding: 6px 20px 20px 20px;
}
.qa-visible {
    position: relative;
    padding-top: 15px;
    padding-bottom: 17px;
    border-bottom: 1px solid rgba(51, 51, 91, 0.6);
}
.qa-visible span {
    position: absolute;
    top: 50%;
    right: 35px;
    width: 12px;
    height: 1px;
    background-color: #33335B;
}
.qa-visible span::before {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    width: 12px;
    height: 1px;
    background-color: #33335B;
    transform: rotate(90deg);
}
.qa-visible span::after {
    content: "";
    position: absolute;
    top: 0;
    left: 0;
    transform: translate(-26%, -48%);
    width: 25px;
    height: 25px;
    border: 1px solid #33335B;
    border-radius: 50%;
}
.qa-visible h3 {
    position: relative;
    cursor: pointer;
}
.qa-visible h3 button {
    padding-left: 43px;
    padding-right: 60px;
    background-color: inherit;
    border: inherit;
    font-family: inherit;
    font-size: 1rem;
    font-weight: 400;
    text-align: left;
    color: #333;
    cursor: pointer;
}
.qa-visible p {
    padding-top: 15px;
    padding-left: 43px;
    padding-right: 60px;
}
.qa-answer {
    display: none;
}
.active.qa-visible .qa-answer {
    display: block;
}
.active.qa-visible span::before {
    display: none;
}
@media screen and (max-width:767px) {
    /* section -Q & A - */
    .achodion {
        padding: 20px 10px;
    }
    .qa-visible h3::before {
        left: 10px;
    }
    .qa-visible h3 button {
        padding-left: 30px;
        padding-right: 35px;
    }
    .qa-visible span {
        right: 10px;
    }
    .qa-visible p {
        padding-left: 30px;
        padding-right: 35px;
    }
}

JavaScript

JavaScriptではボタンを押したときにactiveというclassをつけたり外したりすることと、ariaの属性を書き換えることをしています。
日本語で表現すると、ボタンを押したとき、もしqa-visibleというclassがついたdivタグにactiveというclassがついてなかったら、そのdivタグにactiveというclassをつける。ついでにqa-questionというclassがついてるbuttonタグのaria-expandedの値をtrueにかえて、qa-answerのついているpタグのaria-hiddenの値をfalseにかえる。そうじゃなかったら、activeというclassをはずして、aria-expandedの値をfalseに、aria-hiddenの値をtrueにする。ということをします。

const・letでの実装

ES6(ES2015)以降で使える方法から紹介します。完成形はこちらです。

const qa = document.querySelectorAll('.qa-visible');
    for (let i = 0; i < qa.length; i++) {
        qa[i].addEventListener('click', () => {
            if (!qa[i].classList.contains('active')) {
                qa[i].classList.add('active');
                qa[i].querySelector('.qa-question').setAttribute('aria-expanded', 'true');
                qa[i].querySelector('.qa-answer').setAttribute('aria-hidden', 'false');
            }else {
                qa[i].classList.remove('active');
                qa[i].querySelector('.qa-question').setAttribute('aria-expanded', 'false');
                qa[i].querySelector('.qa-answer').setAttribute('aria-hidden', 'true');
            }
        });
    }

まずはじめに、定数constにqaという名前をつけて、その定数の中に、ducument.querySelectorAllでqa-visibleというclassを格納します。qa-visibleは複数あるのでquerySelectorではなく、querySelectorAllを使います。console.log(qa);で確認すると、この定数qaは配列になります。

const qa = document.querySelectorAll('.qa-visible');
console.log(qa);


次にfor文を使って繰り返し処理を作ります。繰り返す回数は定数qaの配列の個数3回(qa.length)です。そのあとqa[i]にクリックイベントを作ります。クリックイベントの中でconsole.log(qa[i])を確認すると、クリックした場所のqa-visibleのついたdivタグが取得できます。

for (let i = 0; i < qa.length; i++) {
    qa[i].addEventListener('click', () => {
        console.log(qa[i]);
    });
}


最後にif文でactiveというクラスつけはずし、setAttributeでaria-expandedとaria-hiddenの属性値を変更します。

if (!qa[i].classList.contains('active')) {
    qa[i].classList.add('active');
    qa[i].querySelector('.qa-question').setAttribute('aria-expanded', 'true');
    qa[i].querySelector('.qa-answer').setAttribute('aria-hidden', 'false');
}else {
    qa[i].classList.remove('active');
    qa[i].querySelector('.qa-question').setAttribute('aria-expanded', 'false');
    qa[i].querySelector('.qa-answer').setAttribute('aria-hidden', 'true');
}

これで完成です!

varでの実装

2023年時点で新規にES5での書き方を使うことはあまりないかもしれません。なにかのタイミングでES5で書かれたものを書き換える必要もあるかもしれません。また、ES5でしか動かない現場もあるかもしれません。そんなときのためにES5での実装も紹介しておきます。完成形はこちらです。

var qa = document.querySelectorAll('.qa-visible');
    for (var i = 0; i < qa.length; i++) {
        qa[i].addEventListener('click', function(e) {
            var el = e.currentTarget;
            if (!el.classList.contains('active')) {
                el.classList.add('active');
                el.querySelector('.qa-question').setAttribute('aria-expanded', 'true');
                el.querySelector('.qa-answer').setAttribute('aria-hidden', 'false');
            }else {
                el.classList.remove('active');
                el.querySelector('.qa-question').setAttribute('aria-expanded', 'false');
                el.querySelector('.qa-answer').setAttribute('aria-hidden', 'true');
            }
        });
    }

基本的にはconst・letを使った実装と変わりありません。ただconst・letをvarに書き換えただけでは動きませんし、アロー関数もきちんとfunctionと書かなければなりません。
一番の違いはクリックイベントの中でqa[i]が使えないことです。

qa[i].addEventListener('click', function() {
    console.log(qa[i]);
}

console.logで確認すると未定義(undefined)になります。


一応、thisを使うことで目的の要素を取得することはできますが、もうちょっと掘ってみたいと思います。
まず、functionに仮引数eを設定して、これをconsole.logで見てみます。

qa[i].addEventListener('click', function(e) {
    console.log(e);
}


たくさんの要素がでてきますが、targetにbutton.qa-questionというのが見つかります。でも欲しいのはこれではなくてdiv.qa-visibleです。上の方を見ていくとcurrentTarget:nullという怪しげな要素があります。これをconsole.logで見てみることにします。

qa[i].addEventListener('click', function(e) {
    console.log(e.currentTarget);
}


const・letを使った時のqa[i]と同じ要素が取得できました。これを変数elに入れてqa[i]のかわりにelを使うことで問題なく動くようになります。
これで完成です!

おわりに

今回、素のJavaScriptを使ってアコーディオンメニューを作ってみて、const、let、varの違いを体験することができました。どちらが簡単かというと、const・letですが、どちらが面白いかといったらvarが面白かったです。これらの変数の違いについてもっと勉強してみると面白いかなぁと思いました。
プログラムを書くことは難しさもありますが、面白さや楽しさもあるので、また機会があればこういった記事を書いていきたいと思います。

ご意見ご感想はこちらのお問い合わせフォームからお願いします。

この記事を書いた人

みかげうら

こーだー(仮)してるよ。
元作業療法士、臨床経験17年。

うつ / HSP / 精神障害者福祉手帳3級。