Skip to content

Instantly share code, notes, and snippets.

@reecelucas
Last active April 11, 2019 16:47
Show Gist options
  • Save reecelucas/42bde6270108de6298ee5ec1db026926 to your computer and use it in GitHub Desktop.
Save reecelucas/42bde6270108de6298ee5ec1db026926 to your computer and use it in GitHub Desktop.
Accessible dropdown menu using the `<details>` element. https://codepen.io/reecelucas/pen/QozpGw
<details class="c-menu" data-js-menu>
<summary id="menu-control" class="c-menu__control" data-js-menu-control>
Actions
</summary>
<ul id="menu" class="c-menu__list" data-js-menu-list>
<li>
<a href="#dowload">Download</a>
</li>
<li>
<a href="#copy">Copy to Clipboard</a>
</li>
<li>
<a href="#edit">Edit</a>
</li>
<li>
<a href="#delete">Delete</a>
</li>
<li>
<a href="#share">Share</a>
</li>
</ul>
</details>
/**
* For accessibility requirements see:
* https://www.w3.org/TR/wai-aria-practices/examples/menu-button/menu-button-links.html
*/
const menu = document.querySelector("[data-js-menu]");
const menuControl = document.querySelector("[data-js-menu-control]");
const menuList = document.querySelector("[data-js-menu-list]");
const FOCUSABLE_ELEMENTS = "a, input, button, textarea, select, summary";
const isFocusable = el =>
!el.disabled &&
!el.hidden &&
(!el.type || el.type !== "hidden") &&
!el.closest("[hidden]");
const isActiveElement = el => el === document.activeElement;
const getFocusableChildren = el =>
[...el.querySelectorAll(FOCUSABLE_ELEMENTS)].filter(isFocusable);
const getNextItem = (items, currIdx) => {
const nextIdx = currIdx + 1;
return nextIdx >= items.length ? items[0] : items[nextIdx];
};
const getPrevItem = (items, currIdx) => {
const prevIdx = currIdx - 1;
return prevIdx < 0 ? items[items.length - 1] : items[prevIdx];
};
const setAttributes = () => {
menuControl.setAttribute("aria-haspopup", "true");
menuControl.setAttribute("aria-controls", menuList.id);
menuList.setAttribute("aria-role", "menu");
menuList.setAttribute("aria-labelledby", menuControl.id);
};
const selectMenuItem = key => {
const menuItems = getFocusableChildren(menuList);
const activeItem = menuItems.find(isActiveElement);
const activeItemIdx = menuItems.indexOf(activeItem);
const nextItem =
key === "ArrowDown" || key === "Down"
? getNextItem(menuItems, activeItemIdx)
: getPrevItem(menuItems, activeItemIdx);
nextItem.focus();
};
const onToggle = () => {
if (menu.open) {
const firstMenuItem = getFocusableChildren(menuList)[0];
if (firstMenuItem) {
firstMenuItem.focus();
}
} else {
menuControl.focus();
}
};
const onKeydown = event => {
if (!menu.open) {
if (isActiveElement(menuControl) && event.key === "ArrowDown") {
menu.open = true;
}
return;
}
switch (event.key) {
case "Tab":
event.preventDefault();
break;
case "Escape":
case "Esc": // IE & Edge
menu.open = false;
break;
case "ArrowUp":
case "Up": // IE & Edge
case "ArrowDown":
case "Down": // IE & Edge
selectMenuItem(event.key);
}
};
const init = () => {
if (!menu || !menuControl || !menuList) {
return;
}
setAttributes();
menu.addEventListener("toggle", onToggle);
window.addEventListener("keydown", onKeydown);
};
init();
.c-menu {
position: relative;
&[open] {
.c-menu__control {
&:before {
display: block;
}
}
}
}
.c-menu__control {
cursor: pointer;
&:before {
content: "";
cursor: default;
display: none;
height: 100%;
left: 0;
position: fixed;
top: 0;
width: 100%;
z-index: 1;
}
}
.c-menu__list {
display: block;
position: absolute;
right: 0;
z-index: 2;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment