はじめに
みなさんアクセシビリティを意識して開発できていますか?
必要なところに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
にフォーカスがある場合、サブメニューを開き、その最初のアイテムにフォーカスを移動する - メニュー内にフォーカスがあり、サブメニューを持たないアイテムにフォーカスがある場合、以下のアクションを実行する
- サブメニューとその親メニューを閉じる
- メニューバー内の次のアイテムにフォーカスを移動する
- フォーカスがサブメニューを持つ
menuitem
にある場合は、次のいずれかを実行する
- その
menuitem
のサブメニューを開いてもサブメニュー内にフォーカスを移動させない - その
menuitem
のサブメニューを開いて、サブメニュー内の最初のアイテムにフォーカスを移動する
- メニューバー内にフォーカスがある場合、次のアイテムにフォーカスを移動する
- メニューバーが存在しない場合(メニューがメニューボタンから開かれた場合)、サブメニューを持たないアイテムにフォーカスがあるときは、右矢印キーを使っても何も起こらない
-
←キー
-
メニューバー内にフォーカスがある場合、前のアイテムにフォーカスを移動する
- 最初のアイテムの場合は、最後のアイテムにフォーカスを移動する(任意)
-
メニュー内のアイテムのサブメニュー内にフォーカスがある場合、サブメニューを閉じて、親のmenuitemにフォーカスを移動する
-
メニューバー内のアイテムのサブメニュー内にフォーカスがある場合、以下のアクションを実行する
- サブメニューを閉じる
- メニューバー内の前のアイテムにフォーカスを移動する
- フォーカスがサブメニューを持つ
menuitem
にある場合は、次のいずれかを実行する
- その
menuitem
のサブメニューを開いても、サブメニュー内にフォーカスを移動させない - その
menuitem
のサブメニューを開いて、サブメニュー内の最初のアイテムにフォーカスを移動する
-
-
Homeキー
-
矢印キー
のフォーカスのラップがサポートしていない場合、メニュー・メニューバーの最初のアイテムにフォーカスを移動する
-
-
Endキー
-
矢印キー
のフォーカスのラップがサポートしていない場合、メニュー・メニューバーの最後のアイテムにフォーカスを移動する
-
-
Escapeキー
- フォーカスを含むメニューを閉じ、メニューが開かれた要素(メニューボタンや親のmenuitem)にフォーカスを移動する
-
印刷可能な文字に対応する任意のキー
(オプション)- その印刷可能な文字で始まるラベルを持つメニューの次のアイテムにフォーカスを移動する
⚪︎ WAI-ARIA の役割、状態、プロパティ
- メニューとして選択肢を表すアイテムのコンテナーに
role="menu"
orrole="menubar"
を設定する - メニューに含まれるアイテムで、そのメニューやメニューバーの子要素なら、
role="menuitem"
・role="menuitemcheckbox"
・role="menuitemradio"
のいずれかのroleを設定する -
menuitem
をアクティブにし、サブメニューが開く場合、そのmenuitem
は親menuitem
と呼ばれ、サブメニューのmenu要素は、以下のようにする-
親menuitem
と同じmenu要素の内部に含める -
親menuitem
と兄弟要素になる
-
-
親menuitem
は、aria-haspopup
にtrue
ormenu
を設定する -
親menuitem
は、子メニューが見える時はaria-expanded="true"
を設定し、子メニューが見えない時はaria-expanded="false"
を設定する - スクリプトを使ってメニュー内のアイテム間でフォーカスを移動するために、以下のいずれかのアプローチを使用する
- メニューコンテナに
tabindex="-1"
ortabindex="0"
を設定し、aria-activedescendant
にフォーカスされたアイテムのIDを設定する - メニュー内の各アイテムに
tabindex="-1"
を設定し、メニューバーの最初のアイテムには、tabindex="0"
を設定する
- メニューコンテナに
-
menuitemcheckbox
ormenuitemradio
がチェックされている場合、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
- メニューの
アクセシビリティを意識したメニュー・メニューバーの完成形
See the Pen Memu menubar Accessibility by でぐぅー | Qiita (@sp_degu) on CodePen.
アクセシビリティを意識したメニュー・メニューバーの作り方
1. 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>
2. 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を実装する
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では、他のコンポーネントにも焦点を当てて、アクセシビリティについても解説しているので、ぜひ購読していてください。
最後まで読んでくださってありがとうございます!
普段はデザインやフロントエンドを中心にQiitaに記事を投稿しているので、ぜひQiitaのフォローとX(Twitter)のフォローをお願いします。