🐧

素のJavaScriptでWAI-ARIAフルパワータブメニュー作ってみた

2022/08/11に公開

この記事を書くに至った経緯

  • jQueryで記載していたタブメニューをJavaScriptで書くことで、vanillaJSへの理解を深めたい
  • 同じようにvanillaJS導入を考えている人のググる手間を省きたい
  • WAI-ARIAにわかなので、知見・理解を深めたい
  • アクセシビリティに配慮したタブメニューを作りたい。。。ここ未達です。。

完成図

https://codepen.io/con_ns_pgm/pen/jOzZjvz

早速全体のコードを共有します

<div class="tabs" role="tablist">
      <button
        class="tabs__ttl tabs__ttl--news js-tabTrigger is_active"
        id="tab01"
        role="tab"
        aria-controls="tabpanel01"
        tabindex="-1"
        type="button"
      >
        NEWS
      </button>
      <ul
        class="tabs__panel js-tabPanel is_show"
        id="tabpanel01"
        role="tabpanel"
        aria-labelledby="tab01"
        aria-hidden="false"
        aria-selected="true"
      >
        <li class="tabs__item">
          ニュース記事ですニュース記事ですニュース記事です
        </li>
        <li class="tabs__item">
          ニュース記事ですニュース記事ですニュース記事です
        </li>
        <li class="tabs__item">
          ニュース記事ですニュース記事ですニュース記事です
        </li>
        <li class="tabs__item">
          ニュース記事ですニュース記事ですニュース記事です
        </li>
        <li class="tabs__item">
          ニュース記事ですニュース記事ですニュース記事です
        </li>
        <li class="tabs__item">
          ニュース記事ですニュース記事ですニュース記事です
        </li>
        <li class="tabs__item">
          ニュース記事ですニュース記事ですニュース記事です
        </li>
        <li class="tabs__item">
          ニュース記事ですニュース記事ですニュース記事です
        </li>
      </ul>
      <button
        class="tabs__ttl tabs__ttl--blog js-tabTrigger"
        id="tab02"
        role="tab"
        aria-controls="tabpanel02"
        tabindex="0"
        type="button"
      >
        BLOG
      </button>
      <ul
        class="tabs__panel js-tabPanel"
        id="tabpanel02"
        role="tabpanel"
        aria-labelledby="tab02"
        aria-hidden="true"
        aria-selected="false"
      >
        <li class="tabs__item">ブログ記事ですブログ記事ですブログ記事です</li>
        <li class="tabs__item">ブログ記事ですブログ記事ですブログ記事です</li>
        <li class="tabs__item">ブログ記事ですブログ記事ですブログ記事です</li>
        <li class="tabs__item">ブログ記事ですブログ記事ですブログ記事です</li>
        <li class="tabs__item">ブログ記事ですブログ記事ですブログ記事です</li>
        <li class="tabs__item">ブログ記事ですブログ記事ですブログ記事です</li>
        <li class="tabs__item">ブログ記事ですブログ記事ですブログ記事です</li>
        <li class="tabs__item">ブログ記事ですブログ記事ですブログ記事です</li>
        <li class="tabs__item">ブログ記事ですブログ記事ですブログ記事です</li>
      </ul>
      <button
        class="tabs__ttl tabs__ttl--column js-tabTrigger"
        id="tab03"
        role="tab"
        aria-controls="tabpanel03"
        tabindex="0"
        type="button"
      >
        COLUMN
      </button>
      <ul
        class="tabs__panel js-tabPanel"
        id="tabpanel03"
        role="tabpanel"
        aria-labelledby="tab03"
        aria-hidden="true"
        aria-selected="false"
      >
        <li class="tabs__item">コラム記事ですコラム記事ですコラム記事です</li>
        <li class="tabs__item">コラム記事ですコラム記事ですコラム記事です</li>
        <li class="tabs__item">コラム記事ですコラム記事ですコラム記事です</li>
        <li class="tabs__item">コラム記事ですコラム記事ですコラム記事です</li>
        <li class="tabs__item">コラム記事ですコラム記事ですコラム記事です</li>
        <li class="tabs__item">コラム記事ですコラム記事ですコラム記事です</li>
        <li class="tabs__item">コラム記事ですコラム記事ですコラム記事です</li>
      </ul>
    </div>
.tabs {
  position: relative;
  border: 5px solid #403d3a;
  width: 80%;
  max-width: 800px;
  margin: 100px auto 0;

  &__ttl {
    position: absolute;
    width: 32%;
    top: 0;
    transform: translateY(-100%);
    border: 5px solid #403d3a;
    border-bottom: none;
    text-align: center;
    padding: 10px;
    border-radius: 15px 15px 0 0;
    background-color: #f8dacd;
    font-size: 16px;
    color: #888888;
    transform-origin: bottom;
    transition: all 0.4s;
    cursor: pointer;

    &:after {
      content: "";
      position: absolute;
      left: 0;
      bottom: 0;
      width: 100%;
      height: 5px;
      background-color: #403d3a;
    }

    &.is_active {
      background-color: white;
      color: #535353;
      pointer-events: none;

      &:after {
        background-color: white;
      }
    }

    &.tabs__ttl--news {
      left: -5px;

      &:hover,
      &:focus {
        transform: translateY(-100%) scaleY(1.1);
      }
    }

    &.tabs__ttl--blog {
      left: 50%;
      transform: translate(-50%, -100%);

      &:hover,
      &:focus {
        transform: translate(-50%, -100%) scaleY(1.1);
      }
    }

    &.tabs__ttl--column {
      right: -5px;

      &:hover,
      &:focus {
        transform: translateY(-100%) scaleY(1.1);
      }
    }
  }

  &__panel {
    opacity: 0;
    width: 100%;
    transition: opacity 0.3s ease-out;
    height: 0;
    overflow: hidden;

    &.is_show {
      padding: 60px 80px;
      opacity: 1;
      position: static;
      height: auto;
    }
  }

  &__item {
    margin-bottom: 20px;
    list-style: none;
  }
}
window.addEventListener("DOMContentLoaded", function () {
  tabMenu();
});

function tabMenu() {
  const tabTrigger = document.querySelectorAll(".js-tabTrigger");

  //フォーカスを受け取った要素を取得
  const activeTabTrigger = document.activeElement;

  for (let i = 0; i < tabTrigger.length; i++) {
    tabTrigger[i].addEventListener("click", function () {
      const currentTab = document.querySelector(".is_active");
      const currentTabPanel = document.querySelector(".is_show");
      const nextTabPanel = this.nextElementSibling;
      currentTab.classList.remove("is_active");
      currentTab.setAttribute("tabindex", "0");
      currentTabPanel.classList.remove("is_show");
      currentTabPanel.setAttribute("aria-selected", "false");
      currentTabPanel.setAttribute("aria-hidden", "true");
      this.classList.add("is_active");
      this.setAttribute("tabindex", "-1");
      nextTabPanel.setAttribute("aria-selected", "true");
      nextTabPanel.setAttribute("aria-hidden", "false");
      nextTabPanel.classList.add("is_show");
    });
  }
}

意識した点

  • タブを構成する要素にはそれぞれ、見合ったaria-role属性を付与
  • タブメニューの部分には[aria-controls]属性を指定して、どのパネルと関連があるか明示する
  • タブパネルには、[aria-labelledby]属性を指定して、どのタブメニューにラベリングされているか明示
  • 表示されていないタブには[visibility:hidden]を指定し、アクセシビリティツリーから削除。(スクリーンリーダーに読ませない)
  • CSS疑似要素の{:hover}と{:focus}へのスタイルは共通に指定。
    (どちらもボタンを押す準備段階と言う同じ状態を表すため)

以下の属性をJSにて動的に付与・変更を行った。

  • aria-selected=:タブパネルに付与。表示されていれば[true]。
  • tabindex:タブメニューに付与。選択されていれば[-1] として、Tabキー押下でフォーカスを受け取らないようにする。選択されていない要素には[0]を指定し、フォーカス受取可能にする。
  • aria-hidden:タブパネルに付与。表示されていれば[false]

解説??

今回は上記の点を意識して行いました。コードの内容的には、複雑な操作はしておらず
JSでは、ひたすらにDOMを取得して属性値やクラスなどをこねくり回しています。
もし、「このコード意味わからんのだが」「誰だこのクソコード書いたやつ」という方がおられましたら
ご遠慮なくコメントお願いします!
何せ、WAI-ARIAにわか、vanillaJSにわかの僕が書いたものなので間違いや冗長な箇所がある可能性無限大です。
ネットの海に誤情報を流さないためにも、教えて頂けますと大変喜びます。

最後に

今回の実装で、なんとかTabキーの押下でタブメニューがフォーカスを受け取れるところまでは来ました。
しかし、目指していた「Tabキー、矢印キー、Enterキーで操作できるタブメニュー」の完成には至りませんでした。
今後、キーボード操作対応版のタブメニューについても学習をして記事を上げる予定ですので
ぜひ興味のある方はそちらも見てほしいです。
とりあえず思ったのは、「WAI-ARIA属性自動付与システムほすぃぃぃぃいいぃぃいいい!!!!!!!」でした。

Discussion