【JavaScript】アコーディオンメニューの実装その2(開いたら前のやつが閉じるパターン)

どうも、くまだです。

Web制作の案件でよくアコーディオンメニューを実装するので、それのメモ書きみたいなもの。

今回はアコーディオンメニューで、初期状態で1つ開いた状態で、次のアコーディオンメニューを開くと前に開いていたアコーディオンメニューは閉じる仕様。

その1はこちら。

アコーディオンメニュー

動き的には下記動画で。

コードはこちら。HTMLとCSSは、アコーディオンメニューその1といっしょです。

〇 HTML


  <div class="p-qa">

    <ul class="p-qa__block" itemscope="" itemtype="https://schema.org/FAQPage">
      <li class="c-qa" itemscope="" itemprop="mainEntity" itemtype="https://schema.org/Question">
        <button itemprop="name" class="c-qa__head js-ac is-open">テキストテキストテキストテキスト</button>
        <div itemscope="" itemprop="acceptedAnswer" itemtype="https://schema.org/Answer" class="c-qa__body is-open">
          <p itemprop="text">
            テキストテキストテキストテキスト
          </p>
        </div>
      </li>
      <li class="c-qa" itemscope="" itemprop="mainEntity" itemtype="https://schema.org/Question">
        <button itemprop="name"
          class="c-qa__head js-ac">テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</button>
        <div itemscope="" itemprop="acceptedAnswer" itemtype="https://schema.org/Answer" class="c-qa__body">
          <p itemprop="text">
            テキストテキストテキストテキストテキストテキストテキストテキスト
          </p>
        </div>
      </li>
      <li class="c-qa" itemscope="" itemprop="mainEntity" itemtype="https://schema.org/Question">
        <button itemprop="name" class="c-qa__head js-ac">テキストテキストテキストテキスト</button>
        <div itemscope="" itemprop="acceptedAnswer" itemtype="https://schema.org/Answer" class="c-qa__body">
          <p itemprop="text">
            テキストテキストテキストテキスト
          </p>
        </div>
      </li>

    </ul>
  </div>
ul {
  padding: 0;
  margin: 0;
}

p {
  margin: 0;
}

button {
  outline: 0;
  border: none;
}


.p-qa__block {
  display: flex;
  flex-direction: column;
  min-width: 200px;
}


.c-qa {
  display: inline-block;

}

.c-qa__head {

  position: relative;
  text-align: left;
  padding: 20px 30px 20px 20px;
  border-radius: 8px 8px 0 0;
  background: blue;
  color: #ffffff;
  cursor: pointer;
  width: 100%;
}

.c-qa__head:after {
  content: "";
  border-top: 1px solid #fff;
  border-left: 1px solid #fff;
  display: inline-block;
  width: 10px;
  height: 10px;
  transform: rotate(-135deg) translateY(9px);
  position: absolute;
  right: 20px;
  top: 50%;
  transition: transform .4s;

}

.c-qa__body {
  position: relative;
  padding: 0 20px;
  overflow: hidden;
  border-radius: 0 0 8px 8px;
  background: #fff;
  color: #1A236C;
  opacity: 0;
  transition: line-height 0.4s, padding 0.4s, opacity 0.4s;
  border: 1px solid;
  line-height: 0;
  margin: 0;
}

.c-qa__body.is-open {
  padding: 20px;
  line-height: 1.5;
  opacity: 1;
  border-color: blue;
}

.c-qa__head.is-open::after {
  transform: rotate(45deg) translateY(-10px);

}


.c-qa:not(:first-child) {
  margin-top: 16px;
}
const acmenu = document.querySelectorAll('.js-ac');

function toggleAc() {
  const currentTitle = this;
  const currentContent = currentTitle.nextElementSibling;

  acmenu.forEach((title) => {
    if (title !== currentTitle) {
      title.classList.remove('is-open');
      title.nextElementSibling.classList.remove('is-open');
    }
  });

  currentTitle.classList.toggle('is-open');
  currentContent.classList.toggle('is-open');
}

acmenu.forEach((title) => {
  title.addEventListener('click', toggleAc);
});

HTMLの、itemscope~とかitemprop=~とかは構造化マークアップ対応のためです。(クライアント様にアコーディオンのところを構造化マークアップで、といわれたときあったので)

構造化マークアップのチェックは下記のサイトで。

開閉前のコンテンツのpaddingの上下0やline-heightが0なのは、ここに数値が入ってしまうと、

.c-qa__body {
  position: relative;
  padding: 20px; /** 開閉前の余白上下に数値をいれた場合  **/
  overflow: hidden;
  border-radius: 0 0 8px 8px;
  background: #fff;
  color: #1A236C;
  opacity: 0;
  transition: line-height 0.4s, padding 0.4s, opacity 0.4s;
  border: 1px solid;
  line-height: 1;/** 開閉前数値をいれた場合  **/
  margin: 0;
}
アコーディオンメニュー

上記画像のように、アコーディオン間に開閉前のコンテンツが(見えないけど)高さがあるままなので余白が広がってしまう上下余白やline-heightは開閉前は0にして、開いたときに数字を持たせる

.c-qa__body.is-open { /** 開いたときに数字持たせる **/
  padding: 20px;
  line-height: 1.5;
  opacity: 1;
  border-color: blue;
}

なお、.c-qa__bodyの開閉前の左右paddingを0にしないのは、開いたときにテキストが左から右に出るような不自然な動きになってしまうため。

.c-qa__body {
  position: relative;
  padding: 0; /** ここを0にした場合 **/
  overflow: hidden;
  border-radius: 0 0 8px 8px;
  background: #fff;
  color: #1A236C;
  opacity: 0;
  transition: line-height 0.4s, padding 0.4s, opacity 0.4s;
  border: 1px solid;
  line-height: 0;
  margin: 0;
}

テキストの出方に違和感があるので、左右の余白は開閉前も保持したままで。上下の余白は0、左右の余白は保持したままなら開いたとき自然な感じになる。あとはtransitionでうまく調整。

JSは、

const acmenu = document.querySelectorAll('.js-ac');

でjs-acクラスがついたものすべて取得する。アコーディオンが一つしかないなら、querySelectorでよい。今回は複数あるのでquerySelectorAllで。

クリックしたときの動きを作成。

function toggleAc() {
  const currentTitle = this;
  const currentContent = currentTitle.nextElementSibling;

  acmenu.forEach((title) => {
    if (title !== currentTitle) {
      title.classList.remove('is-open');
      title.nextElementSibling.classList.remove('is-open');
    }
  });

  currentTitle.classList.toggle('is-open');
  currentContent.classList.toggle('is-open');
}

nextElementSiblingは「次の要素を取得する」プロパティです。ここの「this」は、js-acクラスがついている要素のことです。(青いコンテンツのところ)

<button itemprop="name" class="c-qa__head js-ac">テキストテキストテキストテキスト</button>

js-acクラスを持ったc-qa__headをクリックしたら、次の要素のc-qa__bodyを取得する。

currentContent.classList.toggle('is-open');

で、取得したc-qa__bodyにis-openのクラスを付与する。toggle()使ってるので、is-openのつけ外し可能。

currentTitle.classList.toggle('is-open');

ここで、js-acクラスがついたc-qa__head自身。自身にis-openクラスのつけ外しをする。(矢印用)

  acmenu.forEach((title) => {
    if (title !== currentTitle) {
      title.classList.remove('is-open');
      title.nextElementSibling.classList.remove('is-open');
    }
  });

if文で、変数titleとcurrentTitleが等しくなかったら(!==の部分)、c-qa__head要素とc-qa__body要素のis-openクラスを外す。つまりtitleが現在のタイトル要素(currentTitle)でない場合、is-openを外す。

クリックイベントは下記で登録。クリックしたらtoggleAc関数が発火する

acmenu.forEach((title) => {
  title.addEventListener('click', toggleAc);
});

ここまで読んでくださりありがとうございました。

この記事を書いた人