【アクセシビリティ】アクセシビリティを意識したメニュー・メニューバーの作り方

## はじめに
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/390720/472331a3-d3c8-336b-7a9d-8b8a11a19b01.png)

みなさんアクセシビリティを意識して開発できていますか？

必要なところに`role属性`を記述したり、`tabキー`でフォーカスができるようにしたりなど、意識しないといけないことも多いです。
そのため、アクセシビリティを完璧にやろうとするのは一苦労です。

ただ、コンポーネントごとに区切って、アクセシビリティを理解しておけば、実装するタイミングに思い出しやすく、アクセシビリティも意識しやすいと思います。

そのため、この記事では「**メニュー・メニューバー**」に焦点を当てて、アクセシビリティを意識したメニュー・メニューバーの実装方法とメニュー・メニューバーで意識した方がいいアクセシビリティを解説しようと思います。

## アクセシビリティを意識したメニュー・メニューバーの仕様
### ⚪︎ メニュー・メニューバーとは？
メニューは、ユーザーに対して一連の機能・アクションを提供するコンポーネントです。
一般的に、Header等にあるメニューバーからプルダウンするような見た目です。

また、メニューボタンをアクティブにしたり、`サブメニューを開く` 項目を選択したり、`Shift + F10`(windows)のようなコマンドを呼び出すことで、メニューが開かれ、表示されます。

### ⚪︎ キーボードインタラクション
#### 前提
メニュー・メニューバーのキーボードインタラクションは、以下の前提としています。

+ 水平のメニューバーは、複数の `menuitem`・`menuitemradio`・`menuitemcheckbox` を含む
+ メニューバーの `menuitem` には、垂直に並べられたサブメニューを持つことがある
+ サブメニューの `menuitem` には、垂直に並べられた子サブメニューを持つことがある
+ フォーカスできる `role="menuitem"`・`role="menuitemradio"`・`role="menuitemcheckbox"` の要素をitemとしている
+ サブメニューは、`role="menu"` の要素である
+ 特定の場合以外は、メニューボタンで開いたメニューとメニューバーで開いたメニューは同じ振る舞いをする

#### キーボードインタラクション
メニューが開かれる時と、メニューバーがフォーカスを受ける時のキーボードフォーカスは、最初のアイテムに置かれます。また、メニュー要素とメニューバーは複合ウィジェットのため、`tab`・`Shift + Tab` でフォーカスを移動させません。そのため、以下のキーボードコマンドを使用することで、フォーカスを移動させます。

+ **`Tabキー`・`Shift + Tabキー`**
  + メニューバーにフォーカスを移動する
    + メニューバーに初めてフォーカスが当たった場合、最初の `menuitem` にフォーカスする
    + メニューバーが以前フォーカスが当たっていた場合、以前の最後にフォーカスが当たっていた `menuitem` にフォーカスする（任意）
  + メニュー・メニューバーの `menuitem` にフォーカスがある場合、メニュー・メニューバーからフォーカスを移動させ、すべてのメニュー・サブメニューを閉じる
  + `Tabキー`・`Shift + Tabキー`で、メニューにフォーカスが移動しないことを注意する
    +　メニューは、メニューバーとは異なり、ユーザーがメニューを開いた時にフォーカスが、メニュー内のアイテムに移動させる必要があります。
+ **`Enterキー`**
  + サブメニューを持つmenuitemにフォーカスがある場合、サブメニューを開き、その最初のアイテムにフォーカスを移動する
  + それ以外の場合は、アイテムをアクティブにし、メニューを閉じる
+ **`Spaceキー`**
  + `menuitemcheckbox` にフォーカスがある場合、メニューは閉じずに状態を変更します。（任意）
  + チェックされていない `menuitemradio` にフォーカスがある場合、フォーカスがある `menuitemradio` をチェックし、他のチェックされていた `menuitemradio` のチェックを外す（任意）
  + サブメニューを持つ `menuitem` にフォーカスがある場合、サブメニューを開き、その最初のアイテムにフォーカスを移動する（任意）
  + サブメニューを持たない `menuitem` にフォーカスがある場合、`menuitem` をアクティブにし、メニューを閉じる（任意）
+ **`↓キー`**
  + メニューバーの `menuitem` にフォーカスがあり、その `menuitem` にサブメニューがある場合は、サブメニューを開いて、サブメニューの最初のアイテムにフォーカスを移動する
  + メニュー内にフォーカスがある場合、次のアイテムにフォーカスを移動する
    + 最後のアイテムの場合は、最初のアイテムにフォーカスを移動する（任意）
+ **`↑キー`**
  + メニュー内にフォーカスがある場合、前のアイテムにフォーカスを移動する
    + 最初のアイテムの場合は、最後のアイテムにフォーカスを移動する（任意）
  + メニューバーの `menuitem` にフォーカスがあり、その `menuitem` にサブメニューがある場合、サブメニューを開いて、サブメニューの最後のアイテムにフォーカスを移動する（任意）
+ **`→キー`**
  + メニューバー内にフォーカスがある場合、次のアイテムにフォーカスを移動する
    + 最後のアイテムの場合は、最初のアイテムにフォーカスを移動する（任意）
  + メニュー内にフォーカスがあり、サブメニューを持つ `menuitem` にフォーカスがある場合、サブメニューを開き、その最初のアイテムにフォーカスを移動する
  + メニュー内にフォーカスがあり、サブメニューを持たないアイテムにフォーカスがある場合、以下のアクションを実行する
    1. サブメニューとその親メニューを閉じる
    2. メニューバー内の次のアイテムにフォーカスを移動する
    3. フォーカスがサブメニューを持つ `menuitem` にある場合は、次のいずれかを実行する
      + その `menuitem` のサブメニューを開いてもサブメニュー内にフォーカスを移動させない
      + その `menuitem` のサブメニューを開いて、サブメニュー内の最初のアイテムにフォーカスを移動する
 + メニューバーが存在しない場合（メニューがメニューボタンから開かれた場合）、サブメニューを持たないアイテムにフォーカスがあるときは、右矢印キーを使っても何も起こらない
+ **`←キー`**
  + メニューバー内にフォーカスがある場合、前のアイテムにフォーカスを移動する
    + 最初のアイテムの場合は、最後のアイテムにフォーカスを移動する（任意）
  + メニュー内のアイテムのサブメニュー内にフォーカスがある場合、サブメニューを閉じて、親のmenuitemにフォーカスを移動する

  + メニューバー内のアイテムのサブメニュー内にフォーカスがある場合、以下のアクションを実行する
    1. サブメニューを閉じる
    2. メニューバー内の前のアイテムにフォーカスを移動する
    3. フォーカスがサブメニューを持つ `menuitem` にある場合は、次のいずれかを実行する
      + その `menuitem` のサブメニューを開いても、サブメニュー内にフォーカスを移動させない
      + その `menuitem` のサブメニューを開いて、サブメニュー内の最初のアイテムにフォーカスを移動する
+ **`Homeキー`**
  + `矢印キー` のフォーカスのラップがサポートしていない場合、メニュー・メニューバーの最初のアイテムにフォーカスを移動する
+ **`Endキー`**
  + `矢印キー` のフォーカスのラップがサポートしていない場合、メニュー・メニューバーの最後のアイテムにフォーカスを移動する
+ **`Escapeキー`**
  + フォーカスを含むメニューを閉じ、メニューが開かれた要素（メニューボタンや親のmenuitem）にフォーカスを移動する
+ **`印刷可能な文字に対応する任意のキー`**（オプション）
  + その印刷可能な文字で始まるラベルを持つメニューの次のアイテムにフォーカスを移動する

### ⚪︎ WAI-ARIA の役割、状態、プロパティ

+ メニューとして選択肢を表すアイテムのコンテナーに　`role="menu"` or `role="menubar"` を設定する
+ メニューに含まれるアイテムで、そのメニューやメニューバーの子要素なら、`role="menuitem"`・`role="menuitemcheckbox"`・`role="menuitemradio"` のいずれかのroleを設定する
+ `menuitem` をアクティブにし、サブメニューが開く場合、その `menuitem` は `親menuitem` と呼ばれ、サブメニューのmenu要素は、以下のようにする
  + `親menuitem` と同じmenu要素の内部に含める
  + `親menuitem` と兄弟要素になる
+ `親menuitem` は、`aria-haspopup` に `true` or `menu` を設定する
+ `親menuitem` は、子メニューが見える時は `aria-expanded="true"` を設定し、子メニューが見えない時は `aria-expanded="false"` を設定する
+ スクリプトを使ってメニュー内のアイテム間でフォーカスを移動するために、以下のいずれかのアプローチを使用する
  + メニューコンテナに `tabindex="-1"` or `tabindex="0"` を設定し、`aria-activedescendant`にフォーカスされたアイテムのIDを設定する
  + メニュー内の各アイテムに `tabindex="-1"` を設定し、メニューバーの最初のアイテムには、 `tabindex="0"` を設定する
+ `menuitemcheckbox` or `menuitemradio` がチェックされている場合、`aria-checked="true"` を設定する
+ メニューアイテムが無効の場合、`aria-disabled="true"` を設定する
+ メニュー内のアイテムは、グループ間に `role="separator"` を持つ要素を置くことでグループに分けられる場合がある
  + `menuitemradio` のアイテムのセットを含むメニューでは、この技術を使用するべきである
+ すべてのセパレーターは、セパレーターの向きと一致する `aria-orientation` を指定するべきである
+ メニューバーに可視ラベルがある場合、`role="menubar"` を持つ要素に、ラベル付け要素のIDを `aria-labelledby` に設定する
  + そうでない場合、メニューバー要素に `aria-label` を指定してラベルが設定する
+ メニューバーが垂直方向に配置されている場合、`aria-orientation="vertical"` を設定する
  + メニューバーの `aria-orientation` のデフォルトは `horizontal`
+ `role="menu"` を持つ要素は、以下のいずれかを設定する
  + 表示を制御する `menuitem` か、ボタンのIDを `aria-labelledby` に設定する
  + `aria-label` を指定してラベルを設定する
+ メニューが水平方向に配置されている場合、`aria-orientation="horizontal"` を設定する
  + メニューの `aria-orientation` のデフォルトは `vertical` 


## アクセシビリティを意識したメニュー・メニューバーの完成形
<p class="codepen" data-height="500" data-default-tab="result" data-slug-hash="MWLXoej" data-editable="true" data-user="sp_degu" style="height: 500px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/sp_degu/pen/MWLXoej">
  Memu menubar Accessibility</a> by でぐぅー | Qiita (<a href="https://codepen.io/sp_degu">@sp_degu</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

## アクセシビリティを意識したメニュー・メニューバーの作り方
### 1. HTMLを実装する
```sample.html
<nav aria-label="Mythical University">
  <ul class="menubar-navigation" role="menubar" aria-label="Mythical University">
    <li role="none">
      <a role="menuitem" aria-haspopup="true" aria-expanded="false">
        ユーザー
        <span class="material-symbols-outlined">expand_more</span>
      </a>
      <ul role="menu" aria-label="About">
        <li role="none">
          <a role="menuitem">
            <span class="material-symbols-outlined">person</span>
            ユーザーA
          </a>
        </li>
        <li role="none">
          <a role="menuitem">
            <span class="material-symbols-outlined">person</span>
            ユーザーB
          </a>
        </li>
        <li role="none">
          <a role="menuitem">
            <span class="material-symbols-outlined">person</span>
            ユーザーC
          </a>
        </li>
      </ul>
    </li>
    <li role="none">
      <a role="menuitem" aria-haspopup="true" aria-expanded="false">
        デバイス
        <span class="material-symbols-outlined">expand_more</span>
      </a>
      <ul role="menu" aria-label="Admissions">
        <li role="none">
          <a role="menuitem">
            <span class="material-symbols-outlined">computer</span>
            パソコン
          </a>
        </li>
        <li role="none">
          <a role="menuitem">
            <span class="material-symbols-outlined">tablet_android</span>
            タブレット
          </a>
        </li>
        <li role="none">
          <a role="menuitem" aria-haspopup="true" aria-expanded="false">
            <span class="material-symbols-outlined">smartphone</span>
            モバイル
            <span class="material-symbols-outlined">chevron_right</span>
          </a>
          <ul role="menu" aria-label="Tuition">
            <li role="none">
              <a role="menuitem">iPhone</a>
            </li>
            <li role="none">
              <a role="menuitem">Google Pixel</a>
            </li>
            <li role="none">
              <a role="menuitem">Galaxy</a>
            </li>
          </ul>
        </li>
      </ul>
    </li>
  </ul>
</nav>
```

### ２. CSSを実装する
```sample.css
body {
  background-color: #212529;
  color: #fff;
  display: grid;
  height: calc(100vh - 40px);
  margin: 0;
  padding: 20px 0;
  place-items: center;
  width: 100vw;
  align-items: start;
}

nav {
  backdrop-filter: blur(50px);
  background-color: rgb(128 128 128 / .3);
  background-blend-mode: luminosity;
  border-radius: 24px;
  padding: 12px;
  position: relative;
  &::before {
    background: linear-gradient(135deg, rgb(255 255 255 / .4) 0, rgb(255 255 255 / 0) 40%, rgb(255 255 255 / 0) 60%, rgb(255 255 255 / .1) 100%);
    border: 1.4px solid transparent;
    border-radius: 24px;
    content: "";
    inset: 0;
    position: absolute;
    -webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0) border-box;
    -webkit-mask-composite: destination-out;
    mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0) border-box;
    mask-composite: exclude;
    z-index: -1;
  }
}

.menubar-navigation {
  display: flex;
  list-style: none;
  margin: 0;
  padding: 0;
}

.menubar-navigation li {
  list-style: none;
  margin: 0;
  padding: 0;
}

.menubar-navigation > li {
  position: relative;
}

li > [role="menuitem"] {
  align-items: center;
  color: #ffffff;
  display: flex;
  font-size: 17px;
  font-weight: 600;
  gap: 8px;
  padding: 8px 16px;
  text-decoration: none;
  white-space: nowrap;
  &:hover, &:focus, &.focused {
    border-radius: 8px;
    background: radial-gradient(34.12% 136.61% at 50% 100%, rgba(94, 94, 94, 0.14) 0%, rgba(94, 94, 94, 0.00) 73.85%), radial-gradient(50% 164.29% at 50% 100%, rgba(255, 255, 255, 0.07) 0%, rgba(255, 255, 255, 0.00) 60.33%), linear-gradient(0deg, rgba(94, 94, 94, 0.18) 0%, rgba(94, 94, 94, 0.18) 100%), rgba(255, 255, 255, 0.06);
    background-blend-mode: color-dodge, normal, color-dodge, lighten;
  }
}

[role="menu"] {
  background-blend-mode: luminosity;
  background-color: rgb(128 128 128 / .3);
  backdrop-filter: blur(50px);
  border-radius: 24px;
  display: none;
  position: absolute;
  padding: 12px;
  &::before {
    background: linear-gradient(135deg, rgb(255 255 255 / .4) 0, rgb(255 255 255 / 0) 40%, rgb(255 255 255 / 0) 60%, rgb(255 255 255 / .1) 100%);
    border: 1.4px solid transparent;
    border-radius: 24px;
    content: "";
    inset: 0;
    position: absolute;
    -webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0) border-box;
    -webkit-mask-composite: destination-out;
    mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0) border-box;
    mask-composite: exclude;
    z-index: -1;
  }
}

[role="menu"] > li > [role="menuitem"] {
  font-weight: 400;
}
```

### 3. JavaScriptを実装する
```sample.js
class MenubarNavigation {
  constructor(domNode) {
    var linkURL, linkTitle;

    this.domNode = domNode;

    this.menuitems = [];
    this.popups = [];
    this.menuitemGroups = {};
    this.menuOrientation = {};
    this.isPopup = {};
    this.isPopout = {};
    this.openPopups = false;

    this.firstChars = {};
    this.firstMenuitem = {};
    this.lastMenuitem = {};

    this.initMenu(domNode, 0);

    domNode.addEventListener('focusin', this.onMenubarFocusin.bind(this));
    domNode.addEventListener('focusout', this.onMenubarFocusout.bind(this));

    window.addEventListener(
      'pointerdown',
      this.onBackgroundPointerdown.bind(this),
      true
    );

    domNode.querySelector('[role=menuitem]').tabIndex = 0;
  }

  getParentMenuitem(menuitem) {
    var node = menuitem.parentNode;
    if (node) {
      node = node.parentNode;
      if (node) {
        node = node.previousElementSibling;
        if (node) {
          if (node.getAttribute('role') === 'menuitem') {
            return node;
          }
        }
      }
    }
    return false;
  }

  updateContent(linkURL, linkName, moveFocus) {
    var h1Node, paraNodes, pathNode;

    if (typeof moveFocus !== 'boolean') {
      moveFocus = true;
    }
  }

  getMenuitems(domNode, depth) {
    var nodes = [];

    var initMenu = this.initMenu.bind(this);
    var popups = this.popups;

    function findMenuitems(node) {
      var role, flag;

      while (node) {
        flag = true;
        role = node.getAttribute('role');

        if (role) {
          role = role.trim().toLowerCase();
        }

        switch (role) {
          case 'menu':
            node.tabIndex = -1;
            initMenu(node, depth + 1);
            flag = false;
            break;

          case 'menuitem':
            if (node.getAttribute('aria-haspopup') === 'true') {
              popups.push(node);
            }
            nodes.push(node);
            break;

          default:
            break;
        }

        if (
          flag &&
          node.firstElementChild &&
          node.firstElementChild.tagName !== 'svg'
        ) {
          findMenuitems(node.firstElementChild);
        }
        node = node.nextElementSibling;
      }
    }
    findMenuitems(domNode.firstElementChild);
    return nodes;
  }

  initMenu(menu, depth) {
    var menuitems, menuitem, role;

    var menuId = this.getMenuId(menu);

    menuitems = this.getMenuitems(menu, depth);
    this.menuOrientation[menuId] = this.getMenuOrientation(menu);

    this.isPopup[menuId] = menu.getAttribute('role') === 'menu' && depth === 1;
    this.isPopout[menuId] = menu.getAttribute('role') === 'menu' && depth > 1;

    this.menuitemGroups[menuId] = [];
    this.firstChars[menuId] = [];
    this.firstMenuitem[menuId] = null;
    this.lastMenuitem[menuId] = null;

    for (var i = 0; i < menuitems.length; i++) {
      menuitem = menuitems[i];
      role = menuitem.getAttribute('role');

      if (role.indexOf('menuitem') < 0) {
        continue;
      }

      menuitem.tabIndex = -1;
      this.menuitems.push(menuitem);
      this.menuitemGroups[menuId].push(menuitem);
      this.firstChars[menuId].push(
        menuitem.textContent.trim().toLowerCase()[0]
      );

      menuitem.addEventListener('keydown', this.onKeydown.bind(this));
      menuitem.addEventListener('click', this.onMenuitemClick.bind(this), {
        capture: true,
      });

      menuitem.addEventListener(
        'pointerover',
        this.onMenuitemPointerover.bind(this)
      );

      if (!this.firstMenuitem[menuId]) {
        if (this.hasPopup(menuitem)) {
          menuitem.tabIndex = 0;
        }
        this.firstMenuitem[menuId] = menuitem;
      }
      this.lastMenuitem[menuId] = menuitem;
    }
  }

  setFocusToMenuitem(menuId, newMenuitem) {
    this.closePopupAll(newMenuitem);

    if (this.menuitemGroups[menuId]) {
      this.menuitemGroups[menuId].forEach(function (item) {
        if (item === newMenuitem) {
          item.tabIndex = 0;
          newMenuitem.focus();
        } else {
          item.tabIndex = -1;
        }
      });
    }
  }

  setFocusToFirstMenuitem(menuId) {
    this.setFocusToMenuitem(menuId, this.firstMenuitem[menuId]);
  }

  setFocusToLastMenuitem(menuId) {
    this.setFocusToMenuitem(menuId, this.lastMenuitem[menuId]);
  }

  setFocusToPreviousMenuitem(menuId, currentMenuitem) {
    var newMenuitem, index;

    if (currentMenuitem === this.firstMenuitem[menuId]) {
      newMenuitem = this.lastMenuitem[menuId];
    } else {
      index = this.menuitemGroups[menuId].indexOf(currentMenuitem);
      newMenuitem = this.menuitemGroups[menuId][index - 1];
    }

    this.setFocusToMenuitem(menuId, newMenuitem);

    return newMenuitem;
  }

  setFocusToNextMenuitem(menuId, currentMenuitem) {
    var newMenuitem, index;

    if (currentMenuitem === this.lastMenuitem[menuId]) {
      newMenuitem = this.firstMenuitem[menuId];
    } else {
      index = this.menuitemGroups[menuId].indexOf(currentMenuitem);
      newMenuitem = this.menuitemGroups[menuId][index + 1];
    }
    this.setFocusToMenuitem(menuId, newMenuitem);

    return newMenuitem;
  }

  setFocusByFirstCharacter(menuId, currentMenuitem, char) {
    var start, index;

    char = char.toLowerCase();

    start = this.menuitemGroups[menuId].indexOf(currentMenuitem) + 1;
    if (start >= this.menuitemGroups[menuId].length) {
      start = 0;
    }

    index = this.getIndexFirstChars(menuId, start, char);

    if (index === -1) {
      index = this.getIndexFirstChars(menuId, 0, char);
    }

    if (index > -1) {
      this.setFocusToMenuitem(menuId, this.menuitemGroups[menuId][index]);
    }
  }

  getIndexFirstChars(menuId, startIndex, char) {
    for (var i = startIndex; i < this.firstChars[menuId].length; i++) {
      if (char === this.firstChars[menuId][i]) {
        return i;
      }
    }
    return -1;
  }

  isPrintableCharacter(str) {
    return str.length === 1 && str.match(/\S/);
  }

  getIdFromAriaLabel(node) {
    var id = node.getAttribute('aria-label');
    if (id) {
      id = id.trim().toLowerCase().replace(' ', '-').replace('/', '-');
    }
    return id;
  }

  getMenuOrientation(node) {
    var orientation = node.getAttribute('aria-orientation');

    if (!orientation) {
      var role = node.getAttribute('role');

      switch (role) {
        case 'menubar':
          orientation = 'horizontal';
          break;

        case 'menu':
          orientation = 'vertical';
          break;

        default:
          break;
      }
    }

    return orientation;
  }

  getMenuId(node) {
    var id = false;
    var role = node.getAttribute('role');

    while (node && role !== 'menu' && role !== 'menubar') {
      node = node.parentNode;
      if (node) {
        role = node.getAttribute('role');
      }
    }

    if (node) {
      id = role + '-' + this.getIdFromAriaLabel(node);
    }

    return id;
  }

  getMenu(menuitem) {
    var menu = menuitem;
    var role = menuitem.getAttribute('role');

    while (menu && role !== 'menu' && role !== 'menubar') {
      menu = menu.parentNode;
      if (menu) {
        role = menu.getAttribute('role');
      }
    }

    return menu;
  }

  isAnyPopupOpen() {
    for (var i = 0; i < this.popups.length; i++) {
      if (this.popups[i].getAttribute('aria-expanded') === 'true') {
        return true;
      }
    }
    return false;
  }

  setMenubarDataExpanded(value) {
    this.domNode.setAttribute('data-menubar-item-expanded', value);
  }

  isMenubarDataExpandedTrue() {
    return this.domNode.getAttribute('data-menubar-item-expanded') === 'true';
  }

  openPopup(menuId, menuitem) {
    var popupMenu = menuitem.nextElementSibling;

    if (popupMenu) {
      var rect = menuitem.getBoundingClientRect();

      if (this.isPopup[menuId]) {
        popupMenu.parentNode.style.position = 'relative';
        popupMenu.style.display = 'block';
        popupMenu.style.position = 'absolute';
        popupMenu.style.left = rect.width + 20 + 'px';
        popupMenu.style.top = '-8px';
        popupMenu.style.zIndex = 100;
      } else {
        popupMenu.style.display = 'block';
        popupMenu.style.position = 'absolute';
        popupMenu.style.left = '-12px';
        popupMenu.style.top = rect.height + 20 + 'px';
        popupMenu.style.zIndex = 100;
      }

      menuitem.setAttribute('aria-expanded', 'true');
      this.setMenubarDataExpanded('true');
      return this.getMenuId(popupMenu);
    }

    return false;
  }

  closePopout(menuitem) {
    var menu,
      menuId = this.getMenuId(menuitem),
      cmi = menuitem;

    while (this.isPopup[menuId] || this.isPopout[menuId]) {
      menu = this.getMenu(cmi);
      cmi = menu.previousElementSibling;
      menuId = this.getMenuId(cmi);
      menu.style.display = 'none';
    }
    cmi.focus();
    return cmi;
  }

  closePopup(menuitem) {
    var menu,
      menuId = this.getMenuId(menuitem),
      cmi = menuitem;

    if (this.isMenubar(menuId)) {
      if (this.isOpen(menuitem)) {
        menuitem.setAttribute('aria-expanded', 'false');
        menuitem.nextElementSibling.style.display = 'none';
      }
    } else {
      menu = this.getMenu(menuitem);
      cmi = menu.previousElementSibling;
      cmi.setAttribute('aria-expanded', 'false');
      cmi.focus();
      menu.style.display = 'none';
    }

    return cmi;
  }

  doesNotContain(popup, menuitem) {
    if (menuitem) {
      return !popup.nextElementSibling.contains(menuitem);
    }
    return true;
  }

  closePopupAll(menuitem) {
    if (typeof menuitem !== 'object') {
      menuitem = false;
    }
    for (var i = 0; i < this.popups.length; i++) {
      var popup = this.popups[i];
      if (this.doesNotContain(popup, menuitem) && this.isOpen(popup)) {
        var cmi = popup.nextElementSibling;
        if (cmi) {
          popup.setAttribute('aria-expanded', 'false');
          cmi.style.display = 'none';
        }
      }
    }
  }

  hasPopup(menuitem) {
    return menuitem.getAttribute('aria-haspopup') === 'true';
  }

  isOpen(menuitem) {
    return menuitem.getAttribute('aria-expanded') === 'true';
  }

  isMenubar(menuId) {
    return !this.isPopup[menuId] && !this.isPopout[menuId];
  }

  isMenuHorizontal(menuitem) {
    return this.menuOrientation[menuitem] === 'horizontal';
  }

  hasFocus() {
    return this.domNode.classList.contains('focus');
  }

  onMenubarFocusin() {
    this.domNode.classList.add('focus');
  }

  onMenubarFocusout() {
    this.domNode.classList.remove('focus');
  }

  onKeydown(event) {
    var tgt = event.currentTarget,
      key = event.key,
      flag = false,
      menuId = this.getMenuId(tgt),
      id,
      popupMenuId,
      mi;

    switch (key) {
      case ' ':
      case 'Enter':
        if (this.hasPopup(tgt)) {
          this.openPopups = true;
          popupMenuId = this.openPopup(menuId, tgt);
          this.setFocusToFirstMenuitem(popupMenuId);
        } else {
          if (tgt.href !== '#') {
            this.closePopupAll();
            this.updateContent(tgt.href, tgt.textContent.trim());
            this.setMenubarDataExpanded('false');
          }
        }
        flag = true;
        break;

      case 'Esc':
      case 'Escape':
        this.openPopups = false;
        mi = this.closePopup(tgt);
        id = this.getMenuId(mi);
        this.setMenubarDataExpanded('false');
        flag = true;
        break;

      case 'Up':
      case 'ArrowUp':
        if (this.isMenuHorizontal(menuId)) {
          if (this.hasPopup(tgt)) {
            this.openPopups = true;
            popupMenuId = this.openPopup(menuId, tgt);
            this.setFocusToLastMenuitem(popupMenuId);
          }
        } else {
          this.setFocusToPreviousMenuitem(menuId, tgt);
        }
        flag = true;
        break;

      case 'ArrowDown':
      case 'Down':
        if (this.isMenuHorizontal(menuId)) {
          if (this.hasPopup(tgt)) {
            this.openPopups = true;
            popupMenuId = this.openPopup(menuId, tgt);
            this.setFocusToFirstMenuitem(popupMenuId);
          }
        } else {
          this.setFocusToNextMenuitem(menuId, tgt);
        }
        flag = true;
        break;

      case 'Left':
      case 'ArrowLeft':
        if (this.isMenuHorizontal(menuId)) {
          mi = this.setFocusToPreviousMenuitem(menuId, tgt);
          if (this.isAnyPopupOpen() || this.isMenubarDataExpandedTrue()) {
            this.openPopup(menuId, mi);
          }
        } else {
          if (this.isPopout[menuId]) {
            mi = this.closePopup(tgt);
            id = this.getMenuId(mi);
            mi = this.setFocusToMenuitem(id, mi);
          } else {
            mi = this.closePopup(tgt);
            id = this.getMenuId(mi);
            mi = this.setFocusToPreviousMenuitem(id, mi);
            this.openPopup(id, mi);
          }
        }
        flag = true;
        break;

      case 'Right':
      case 'ArrowRight':
        if (this.isMenuHorizontal(menuId)) {
          mi = this.setFocusToNextMenuitem(menuId, tgt);
          if (this.isAnyPopupOpen() || this.isMenubarDataExpandedTrue()) {
            this.openPopup(menuId, mi);
          }
        } else {
          if (this.hasPopup(tgt)) {
            popupMenuId = this.openPopup(menuId, tgt);
            this.setFocusToFirstMenuitem(popupMenuId);
          } else {
            mi = this.closePopout(tgt);
            id = this.getMenuId(mi);
            mi = this.setFocusToNextMenuitem(id, mi);
            this.openPopup(id, mi);
          }
        }
        flag = true;
        break;

      case 'Home':
      case 'PageUp':
        this.setFocusToFirstMenuitem(menuId, tgt);
        flag = true;
        break;

      case 'End':
      case 'PageDown':
        this.setFocusToLastMenuitem(menuId, tgt);
        flag = true;
        break;

      case 'Tab':
        this.openPopups = false;
        this.setMenubarDataExpanded('false');
        this.closePopup(tgt);
        break;

      default:
        if (this.isPrintableCharacter(key)) {
          this.setFocusByFirstCharacter(menuId, tgt, key);
          flag = true;
        }
        break;
    }

    if (flag) {
      event.stopPropagation();
      event.preventDefault();
    }
  }

  onMenuitemClick(event) {
    var tgt = event.currentTarget;
    var menuId = this.getMenuId(tgt);

    if (this.hasPopup(tgt)) {
      if (this.isOpen(tgt)) {
        this.closePopup(tgt);
      } else {
        this.closePopupAll(tgt);
        this.openPopup(menuId, tgt);
      }
    } else {
      this.updateContent(tgt.href, tgt.textContent.trim());
      this.closePopupAll();
    }
    event.stopPropagation();
    event.preventDefault();
  }

  onMenuitemPointerover(event) {
    var tgt = event.currentTarget;
    var menuId = this.getMenuId(tgt);

    if (this.hasFocus()) {
      this.setFocusToMenuitem(menuId, tgt);
    }

    if (this.isAnyPopupOpen() || this.hasFocus()) {
      this.closePopupAll(tgt);
      if (this.hasPopup(tgt)) {
        this.openPopup(menuId, tgt);
      }
    }
  }

  onBackgroundPointerdown(event) {
    if (!this.domNode.contains(event.target)) {
      this.closePopupAll();
    }
  }
}

window.addEventListener('load', function () {
  var menubarNavs = document.querySelectorAll('.menubar-navigation');
  for (var i = 0; i < menubarNavs.length; i++) {
    new MenubarNavigation(menubarNavs[i]);
  }
});
```

## まとめ
この記事では、「**メニュー・メニューバー**」に焦点を当てて、アクセシビリティを意識したメニュー・メニューバーの実装方法とメニュー・メニューバーで意識した方がいいアクセシビリティを解説しました。

ぜひこの記事をストックして、メニュー・メニューバーを実装する時にアクセシビリティについて思い出してもらえると嬉しいです。

Advent Calendar 2023では、他のコンポーネントにも焦点を当てて、アクセシビリティについても解説しているので、ぜひ購読していてください。

https://qiita.com/advent-calendar/2023/degudegu2510

---

最後まで読んでくださってありがとうございます！

普段はデザインやフロントエンドを中心にQiitaに記事を投稿しているので、ぜひQiitaのフォローとX(Twitter)のフォローをお願いします。

https://twitter.com/degudegu2510