0
0

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.

【初心者】アコーディオンメニューをVanilla JSで制作【備忘録】

Last updated at Posted at 2023-04-08

はじめに

投稿時点で、筆者は知識ゼロの状態から勉強を初めて2ヶ月程度の実力です。
そのため、理解不足や説明不足、誤った内容や呼び方をしている可能性があります。
万が一参考にする場合は上記の点を考慮した上でご一読ください。

それと今回はNotionに貯めたメモをそのまま貼り付けただけです。
あと結論から言うと、納得のいったものには仕上がりませんでした。
それと試してないけど、アンサー部分の中身が入れ子になってると閉じてくれないかも…。

制作過程

アコーディオンメニューをJavascript(外部ライブラリ無し)で設計。
まずは、display:none;を付与して開閉動作を試みた。

index.html
<div class="accordion">

  <p class="menu pointer">メニュー1</p>
  <div class="contents display-none">コンテンツ1</div>

  <p class="menu pointer">メニュー2</p>
  <div class="contents display-none">コンテンツ2</div>

  <p class="menu pointer">メニュー3</p>
  <div class="contents display-none">コンテンツ3</div>

</div>
style.css
.accordion {
  position: relative;
  top: 5rem;
  left: 5rem;
  width: 20rem;
  text-align: center;
}

.menu {
  height: 3rem;
  font-weight: 600;
  background-color: aqua;
  border-bottom: solid 1px #000;
}

.contents {
  height: 3rem;
  background-color: aquamarine;
  border-bottom: solid 1px #000;
  transition: all 0.5s;
}

.display-none {
  display: none;
}

.pointer {
  cursor: pointer;
}

HTMLとCSSはこれとして、次はJavascript部分。
まずは自分だけで考えた最適解から。(後ほどドットインストールもみてここから変更しました)

main.js
const menus = document.querySelectorAll('.menu');
menus.forEach((menu) => {
  menu.addEventListener('click', function() {
    const contents = this.nextElementSibling;
    contents.classList.toggle('display-none');
  });
});

次は、試行錯誤した経過をお見せします。
まず最初に、要素を取得します。

main.js
// 要素を取得
// querySelectorAll('.menu')で全てのmenuを指定
const menus = document.querySelectorAll('.menu');

querySelectorだけでは一番最初の要素しか取得できないのでquerySelectorAllを使う。
querySelectorAllでは全ての要素を取得してくれるが、取得した要素の何番目かを判別させる必要がある。
結論からいうと、何番目に当たるかをmenus[何番目]みたいになる。
これをfor文を使って行うとこうなる。

main.js
for (let index = 0; index < menus.length; index++) {
  const menu = menus[index];
}

const menu = menus[index];の部分が処理部分になる。
次はfor文ではなくforEachメソッドで行ってみる。
forEach()メソッドで反復処理をするほうが簡潔にコードを書くことができるため。

main.js
menus.forEach((menu) => {
  console.log(menu.textContent);
}

ちなみにmenu両枠の括弧を外してもOKらしい

main.js
menus.forEach(menu => {
  console.log(menu.textContent);
}

上記はアロー関数で行っていたが、通常の関数だとこうなる。

main.js
menus.forEach(function(menu) {
  console.log(menu.textContent);
}

では、要素の取得と取得した要素の反復処理をまとめてみる。
今回はforEachメソッドで行ってみる。

main.js
// 要素を取得
// querySelectorAll('.menu')で全てのmenuを指定
const menus = document.querySelectorAll('.menu');
// 取得した要素を反復処理
menus.forEach((menu) => {
  // コンソールに要素のテキストを出力
  console.log(menu.textContent);
});

次に、クリックしたときの動作を考えていく。

main.js
menu.addEventListener('click', () => {
  // console.log('click');
});

これがクリックしたときの記述となる
クリックしたあとの処理を次に考えていく。

main.js
const contents = menu.nextElementSibling;
contents.classList.toggle('display-none');

nextElementSiblingは指定された要素の次の要素を取得しにいく。
menuはクリックした部分の.menuを指定しているので、その次となると.contents部分となる。
そしてclassList.toggleでクラス名をつけ外しして表示、非表示を行っている。
今までのことをまとめると、下記になる。

main.js
const menus = document.querySelectorAll('.menu');
menus.forEach((menu) => {
  menu.addEventListener('click', () => {
    const contents = menu.nextElementSibling;
    contents.classList.toggle('display-none');
  });
});

このままでもいいが、個人的には
menu.nextElementSibling部分をthis.nextElementSibling;を使って要素の取得をしたかった。
thisを使うことで、クリックした要素の次の要素であることを指定している。
しかしそのままthis.nextElementSibling;に書き換えただけでは機能してくれなかった。
調べたところ、thisは通常の関数「function()」でないと束縛してくれないため、アロー関数は使えないとのこと。
それを踏まえてコードを変更した結果、下記となった。

main.js
const menus = document.querySelectorAll('.menu');
menus.forEach((menu) => {
  menu.addEventListener('click', function() {
    const contents = this.nextElementSibling;
    contents.classList.toggle('display-none');
  });
});

これでクリックした際に表示、非表示を行えるようになった。

しかしここで問題が発生。
表示、非表示は行えたが、transitionが効かないのである。
結論から言うと、transitiondisplay:none;には効果を発揮しないためである。
以下引用文

transitionは変化前と変化後を数値で変化させます。逆に言うと変化前と変化後の両方が数値でなければ変化できません。
つまりnoneautoなどを指定しているときは効きません。

ということで、違うプランが必要となる。
一応、z-indexで無理やり隠すやり方もできたが好きではないので今回は使わない。
参考程度にやり方は残しておく。赤字が追加項目となる。
でもよく考えたらheightを指定しないと機能しないので、文章量により高さが変動するときはできないと思う。
height:autoではtransitionは機能しない。

style.css
.contents {
  height: 3rem;
  background-color: aquamarine;
  border-bottom: solid 1px #000;
  transition: all 0.5s;
  position: relative;
  z-index: 1;
}

.display-none {
  overflow: hidden;
  height: 0;
}

こうして自分なりに解答だしたけど、その後にドットインストールのやり方とか試して、シンプルな範囲で一番いいと思ったのがこれになった。

index.html
<dl class="accordion">

  <div class="item">
    <dt class="menu pointer">メニュー1</dt>
    <dd class="contents">コンテンツ1<br>2行目</dd>
  </div>

  <div class="item">
    <dt class="menu pointer">メニュー2</dt>
    <dd class="contents">コンテンツ2<br>2行目</dd>
  </div>

  <div class="item">
    <dt class="menu pointer">メニュー3</dt>
    <dd class="contents">コンテンツ3<br>2行目</dd>
  </div>

</dl>
style.css
.accordion {
  position: relative;
  top: 5rem;
  left: 5rem;
  width: 20rem;
  text-align: center;
}

.menu {
  position: relative;
  height: 3rem;
  font-weight: 600;
  background-color: aqua;
  border-bottom: solid 1px #000;
}

.menu::after {
  content: "+";
  position: absolute;
  top: 50%;
  right: 1rem;
  transform: translateY(-50%) rotate(90deg);
  transition: transform 0.3s; /* 開閉バッジに関係する */
}

/* 親要素のmenuにopenが付与されたときの動作 */
.open .menu::after {
  content: "-";
  transform: translateY(-50%) rotate(180deg);
}

.contents {
  background-color: aquamarine;
  border-bottom: solid 1px #000;
  line-height: 0;
  overflow: hidden;
  transition: 0.3s; /* アコーディオン開閉に関係する */
}

/* 親要素のmenuにopenが付与されたときの動作 */
.open .contents {
  line-height: 1.5;
  padding: 1rem; /* paddingはopen側で付与する */
}

.pointer {
  cursor: pointer;
  user-select: none; /* ダブルクリックした時にテキストが選択されるのが気になるのでそれを消すため */
}
main.js
// 要素を取得
const menus = document.querySelectorAll('.menu');

menus.forEach((menu) => {
  menu.addEventListener('click', () => {
	// menuの親要素(item)にopenを付与
    menu.parentElement.classList.toggle('open');

	// 開いたら他のメニューは閉じる動作
    menus.forEach((menuNow) => {
      if (menu !== menuNow) {
        menuNow.parentElement.classList.remove('open');
      }
    });

  });
});

buttonタグを使ったほうがいいのかなと思ったけど疲れてもういいやってなった。
あとはここまでやってもjQueryより微妙な形になるなら素直にjQuery使えばいいんじゃないかと思った。
だけど今回の一番の目的はJavascriptの学習だったので良かったと思う。

ドットインストールは高さ指定してCSSでアニメーションつけてました。
最後に忘れないためにドットインストール風にしたCSSパターン載せときます。

style.css
.accordion {
  position: relative;
  top: 5rem;
  left: 5rem;
  width: 20rem;
  text-align: center;
}

.menu {
  position: relative;
  height: 3rem;
  font-weight: 600;
  background-color: aqua;
  border-bottom: solid 1px #000;
}

.menu::after {
  content: "+";
  position: absolute;
  top: 50%;
  right: 1rem;
  transform: translateY(-50%) rotate(90deg);
  transition: transform 0.3s; /* 開閉バッジに関係する */
}

/* 親要素のmenuにopenが付与されたときの動作 */
.open .menu::after {
  content: "-";
  transform: translateY(-50%) rotate(180deg);
}

.contents {
  height: 3rem;
  background-color: aquamarine;
  border-bottom: solid 1px #000;
  display: none;
}

/* 親要素のmenuにopenが付与されたときの動作 */
.open .contents {
  display: block;
  animation: 0.3s fadeIn; /* 下記のkeyflamesを付与 */
}

.pointer {
  cursor: pointer;
  user-select: none; /* ダブルクリックした時にテキストが選択されるのが気になるのでそれを消すため */
}

/* transitionはdisplayには効果ないのでkeyflamesで対応 */
@keyframes fadeIn {
  0% {
    opacity: 0;
    transform: translateY(-10px);
  }
  100% {
    opacity: 1;
    transform: none;
  }
}

参考サイト

アコーディオンメニューはJavaScriptで作れる!【初心者もOK】
【JS】アコーディオンをVanilla JS でやってみる
素のJavaScriptでアニメーション付きアコーディオンを実装する方法【3通り】
detailsとsummaryタグで作るアコーディオンUI - アニメーションのより良い実装方法
JavaScriptでアコーディオンUIを作ろう

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?