WAI-ARIAに配慮したアコーディオンパネルをFLOCSSで書きたい

2022/06/29に公開約7,400字4件のコメント

テーマ

WAI-ARIAに配慮したアコーディオンパネルをFLOCSSで書きたい!

使用技術

  • HTML
  • JavaScript(vanilla)
  • SCSS

挙動

ソース

HTML

      <div class="o-accordion js-accordion">
        <dl class="c-accordion-list js-accordionList" role="tablist">
          <dt class="p-accordion-list__trigger js-accordionTrigger" aria-label="このタイトルに関連するタブを開く" role="tab" aria-expanded="false" aria-controls="panel-1" id="tab-1" tabindex="0">question<span class="c-bar--vertical"></span><span class="c-bar--horizonal"></span></dt>
          <dd class="p-accordion-list__content js-accordionContent" role="tabpanel" aria-controls="panel-1" aria-hidden="true" aria-labelledby="tab-1">
            <div>
              <!-- paddingはdivに指定 -->
              <p>
                answer
              </p>
            </div>
          </dd>
        </dl>
        <dl class="c-accordion-list js-accordionList" role="tablist">
          <dt class="p-accordion-list__trigger js-accordionTrigger" aria-label="このタイトルに関連するタブを開く" role="tab" aria-expanded="false" aria-controls="panel-2" id="tab-2" tabindex="0">question<span class="c-bar--vertical"></span><span class="c-bar--horizonal"></span></dt>
          <dd class="p-accordion-list__content js-accordionContent" role="tabpanel" aria-controls="panel-2" aria-hidden="true" aria-labelledby="tab-2">
            <div>
              <!-- paddingはdivに指定 -->
              <p>
                answer
              </p>
            </div>
          </dd>
        </dl>
        <dl class="c-accordion-list js-accordionList" role="tablist">
          <dt class="p-accordion-list__trigger js-accordionTrigger" aria-label="このタイトルに関連するタブを開く" role="tab" aria-expanded="false" aria-controls="panel-3" id="tab-3" tabindex="0">question<span class="c-bar--vertical"></span><span class="c-bar--horizonal"></span></dt>
          <dd class="p-accordion-list__content js-accordionContent" role="tabpanel" aria-controls="panel-3" aria-hidden="true" aria-labelledby="tab-3">
            <div>
              <!-- paddingはdivに指定 -->
              <p>
                answer
              </p>
            </div>
          </dd>
        </dl>
      </div>

SCSS

.o-accordion {
  position: relative;
  width: 100%; //希望のアコーディオンパネル横幅
  .c-accordion-list {
    width: 100%;
    overflow: hidden;
    .p-accordion-list__trigger {
      position: relative;
      cursor: pointer;
      &:focus-visible {
        border: 1px solid #333; // tabキーで移動してきたときのスタイル
      }
      .c-bar--vertical {
        position: absolute;
        top: 50%;
        transform: translateY(-50%);
        right: 7%;
        display: block;
        height: 0px;
        width: 0px;
        border-right: 1px solid #333;
        transition: 0.4s;
        opacity: 0;
      }
      .c-bar--horizonal {
        position: absolute;
        top: 50%;
        right: 7%;
        display: block;
        height: 20px;
        width: 1px;
        border-right: 1px solid #333;
        transform: translateY(-50%) rotate(90deg);
      }
    }
    .p-accordion-list__content {
      transition: 0.4s;
    }
    &.is-hide {
      .p-accordion-list__trigger {
        .c-bar--vertical {
          height: 20px;
          width: 1px;
          transition: 0.4s;
          opacity: 1;
        }
      }
      .p-accordion-list__content {
        transition: 0.4s;
      }
    }
  }
}

JavaScript

if (document.querySelector(".js-accordion") != null) {
  addEventListener("load", function () {
    const accordionList = document.querySelectorAll(".js-accordionList");
    const accordionListLength = accordionList.length;

    accordionList.forEach(function (elem, index) {
      const accordionTrigger = elem.querySelector(".js-accordionTrigger");
      const accordionContent = elem.querySelector(".js-accordionContent");
      const accordionContentH = accordionContent.offsetHeight; // それぞれのパネルの本来の高さを格納

      elem.classList.add("is-hide"); // 全トリガーの開閉アイコンを閉状態に設定
      elem.querySelector(".js-accordionContent").style.height = "0px"; // それぞれのパネルの高さを0にして閉状態に

      accordionTrigger.addEventListener("click", function (event) {
        // クリックされたトリガーが閉状態であれば
        if (elem.classList.contains("is-hide") === true) {
          for (let i = 0; i < accordionListLength; i++) {
            // 一旦全てのトリガーの選択状態をfalseに
            accordionList[i].querySelector(".js-accordionTrigger").setAttribute("aria-expanded", "false");
            // 一旦全てのパネルの非表示状態をfalseに
            accordionList[i].querySelector(".js-accordionContent").setAttribute("aria-hidden", "true");
            // 一旦全てのトリガーを閉状態に
            accordionList[i].classList.add("is-hide");
            // 一旦パネルの高さをautoから本来の高さに戻す(auto → 0 はtransitionしないため)
            accordionList[i].querySelector(".js-accordionContent").style.height = accordionContentH + "px";
            // リフローを発生させてパネルのheightがautoから本来の高さに変わったことをブラウザに描画させる
            document.defaultView.getComputedStyle(accordionList[i].querySelector(".js-accordionContent"), null).height;
            accordionList[i].querySelector(".js-accordionContent").style.height = 0 + "px";
          }
          // クリックされたトリガーの選択状態をtrueに
          accordionTrigger.setAttribute("aria-expanded", "true");

          //クリックされたトリガーに対応するパネルの非表示状態をtureに
          accordionContent.setAttribute("aria-hidden", "false");

          // クリックされたトリガーの閉状態を解除
          elem.classList.remove("is-hide");
          // クリックされたトリガーの直下パネルのみ一旦本来の高さに設定
          accordionContent.style.height = accordionContentH + "px";
          // クリックされたトリガーの直下パネルを取得
          const nextAccordionContent = event.target.closest(".c-accordion-list").querySelector(".p-accordion-list__content");
          // パネルが閉状態から開状態に変化し終わったら ↓ を実行
          nextAccordionContent.addEventListener("transitionend", function () {
            // 高さが0ではない(開状態の)パネルのみ、heightをautoにしてresizeに対応
            if (document.defaultView.getComputedStyle(nextAccordionContent, null).height !== "0px") {
              nextAccordionContent.style.height = "auto";
            }
          });
        } else {
          // クリックされたトリガーの選択状態をfalseに
          accordionTrigger.setAttribute("aria-expanded", "false");
          //クリックされたトリガーに対応するパネルの非表示状態をtrueに
          accordionContent.setAttribute("aria-hidden", "true");
          // クリックされたトリガーが開状態であれば
          for (let i = 0; i < accordionListLength; i++) {
            // 一旦パネルの高さをautoから本来の高さに戻す(auto → 0 はtransitionしないため)
            accordionList[i].querySelector(".js-accordionContent").style.height = accordionContentH + "px";
            // リフローを発生させてパネルのheightがautoから本来の高さに変わったことをブラウザに描画させる
            document.defaultView.getComputedStyle(accordionList[i].querySelector(".js-accordionContent"), null).height;
            // パネルを本来の高さから0までアニメーション
            accordionList[i].querySelector(".js-accordionContent").style.height = 0 + "px";
            // 全てのトリガーのアイコンを閉状態に更新
            accordionList[i].classList.add("is-hide");
          }
        }
      });
    });
  });
  // tabキーでfocusされたとき、enterでパネルを開けるように
  function handleKeyDown(event) {
    // 押されたキーの種類を判別
    if (event.code === "Enter") {
      document.activeElement.click();
    }
  }

  addEventListener("keydown", handleKeyDown);
}

備考

codepenに埋め込んだら開く動作がカクつくようになってしまいました;。;v ←reset cssしてないことが原因でしたが、reset css書くと挙動の本質とは異なるコードが増えて見通し悪くなりそうだったのでスルー
WAI-ARIAにわかなのでARIA属性の使い方が正しいか常に再考中...

Discussion

恐縮ながら質問です!
dlタグ全体をbuttonタグで囲むと、tabindexの指定が不要でフォーカスを受けられるようになる且つ
aria-role的にもクリック可能な要素であると示せて良いのではと思ったんですが、どうでしょうか。。
それと、dd,dtタグそれぞれに同じidが指定されているのが気になってしまい。。
ddのaria-controls属性の指定と、dtのidが一致すれば要素の制御対象を明示できるのかなと思いました!

あ〜id消し忘れてますね
MDNの使用例からコピってきてそのまま見落としてた...ddのidは不要です!
buttonは子要素にdlを持てないので全体をbuttonで囲むことはできないんですよね...
かといってdlの子要素にはdiv > dl+dd の形もしくはdt + dd の形しか持つことができないのでrole="tablist"で対応してます!(dtの中にbuttonならアリ)

なるほど。。
buttonタグの内包可能要素の把握不足でした!
そしたらこびとさんのコードが最適解ですね!🔥
ご返信ありがとうございます!🙏✨✨

すいません、role="tablist"を見過ごしてました!
tablist指定すれば、role的には問題ないのでbuttonタグで囲む必要はないですね!!

ログインするとコメントできます