Skip to content

Instantly share code, notes, and snippets.

@SigurdMW
Last active September 11, 2017 07:25
Show Gist options
  • Select an option

  • Save SigurdMW/6d82ac3b35f608c9f838203398d4b1d1 to your computer and use it in GitHub Desktop.

Select an option

Save SigurdMW/6d82ac3b35f608c9f838203398d4b1d1 to your computer and use it in GitHub Desktop.
Accessible and super light weight Mega Menu. Features: one sublevel mega menu (for now), close on esc, automatic overlay with close on click, close submenu when last link loses focus.
.mega-menu {
position: relative;
overflow-y: auto;
background-color: $color-white;
@include mq($breakpoint-medium) {
overflow: visible;
}
&--overflow-hidden {
@extend %overflow-hidden;
}
// Consider moving to atoms/menu-link ?
.menu-link--has-children {
&::after {
transition: transform 0.2s ease-in-out;
transform: rotate(90deg);
}
}
// Consider moving to atoms/menu-link ?
.menu-link--submenu-open {
color: $color-profile-darker;
font-weight: 700;
@include mq($breakpoint-medium) {
border-color: $color-profile-darker;
}
&::after {
transform: rotate(270deg);
}
}
&-sublinks {
display: none;
width: 100%;
background-color: $color-light-pink;
flex-wrap: wrap;
padding: 2em 0;
opacity: 0;
animation: megaMenuAnimation 0.2s ease-in;
animation-fill-mode: forwards;
@include mq($breakpoint-medium) {
position: absolute;
left: 0;
top: 100%;
}
&--open {
display: flex;
}
& .arrow-link {
display: block;
font-weight: 400;
text-transform: none;
border-color: $color-white;
padding-left: 2em;
&:focus {
background-color: $color-white;
}
}
}
&-overlay {
display: none;
animation: megaMenuAnimation 0.2s;
animation-fill-mode: forwards;
@include mq($breakpoint-medium) {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 0;
background-color: rgba(255,255,255,0.5);
opacity: 0;
transition: opacity 0.2s ease-in 0.2s;
&--open {
display: block;
opacity: 1;
}
}
}
}
@keyframes megaMenuAnimation {
from {
opacity: 0;
transform: translateY(-5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes overlayAnimation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
// custom selectors to be made
const MegaMenu = new MegaMenu();
class MegaMenu {
constructor() {
this.options = {
selector: "mega-menu",
trigger: "menu-link--has-children"
}
this.menuOpen = false;
this.documentBody = document.body;
this.megaMenu = document.querySelector("." + this.options.selector);
this.triggers = this.megaMenu.querySelectorAll("." + this.options.trigger);
this.state = [];
this.overlay = "mega-menu-overlay";
this.overlayElement = null;
this.overlayOpen = "mega-menu-overlay--open";
this.bodyOverflow = "mega-menu--overflow-hidden";
this.menuLinkSubmenuOpen = "menu-link--submenu-open";
this.menuLinkActive = "menu-link--active";
this.submenu = "mega-menu-sublinks";
this.submenuOpen = "mega-menu-sublinks--open";
// initialize mega menu
this.init();
}
init() {
this.createOverlayAndRegisterClick();
this.keyboardEventHandeler();
if (this.triggers.length > 0) {
for (var i = 0; i < this.triggers.length; i++) {
const el = this.triggers[i];
const submenu = el.parentNode.querySelector("." + this.submenu);
this.initAttributes(el, submenu, i);
this.state.push({active: false, trigger: el, submenu: submenu});
el.addEventListener("click", (e) => {
e.preventDefault();
this.state = this.state.map((item, key) => {
if (item.trigger === el) {
return Object.assign({}, item, {active: !item.active});
} else {
return Object.assign({}, item, { active: false });
}
});
this.handle();
});
}
}
}
// Create overlay element in body and make available as global variable
createOverlayAndRegisterClick() {
const background = document.createElement("div");
background.classList.add(this.overlay);
background.setAttribute("aria-hidden", "true");
this.overlayElement = background;
document.body.appendChild(background);
background.addEventListener("click", () => {
this.closeMenu(true);
});
}
// Initialize menu-link and submenu with aria attributes
initAttributes(el, submenu, i) {
const ariaId = "menucontrol-" + i;
el.setAttribute("aria-expanded", "false");
el.setAttribute("aria-controls", ariaId);
submenu.setAttribute("aria-hidden", "true");
submenu.setAttribute("id", ariaId);
submenu.setAttribute("aria-labelledby", ariaId);
}
closeMenu(placeFocus = false) {
const activeItem = this.state.filter(item => item.active === true);
if (activeItem.length > 0 && placeFocus) {
activeItem[0].trigger.focus();
}
this.state = this.state.map(item => Object.assign({}, item, { active: false }));
this.handle();
}
keyboardEventHandeler() {
document.addEventListener('keyup', (event) => {
// On esc
if (event.keyCode === 27 && this.menuOpen) {
this.closeMenu(true);
}
// On tab
if (event.keyCode === 9) {
const activeItem = this.state.filter(item => item.active === true);
if (activeItem.length > 0) {
const submenuLinks = activeItem[0].submenu.querySelectorAll("a");
if (submenuLinks.length > 0) {
const activeItemsNode = document.activeElement.parentNode.parentNode.parentNode.parentNode;
if (!activeItemsNode.classList.contains("mega-menu-sublinks--open")) {
this.closeMenu();
}
}
}
}
});
}
// Handle state
handle() {
this.state.map((item) => {
if (item.active) {
item.trigger.setAttribute("aria-expanded", "true");
item.trigger.classList.add(this.menuLinkSubmenuOpen);
item.submenu.setAttribute("aria-hidden", "false");
item.submenu.classList.add(this.submenuOpen);
} else {
item.trigger.setAttribute("aria-expanded", "false");
item.trigger.classList.remove(this.menuLinkSubmenuOpen);
item.submenu.setAttribute("aria-hidden", "true");
item.submenu.classList.remove(this.submenuOpen);
}
});
const findTrueIndex = this.state.findIndex(item => item.active === true);
// cannot find an active item
if (findTrueIndex < 0) {
this.overlayElement.classList.remove(this.overlayOpen);
this.overlayElement.setAttribute("aria-hidden", "true");
this.documentBody.classList.remove(this.bodyOverflow);
this.menuOpen = false;
} else {
// set focus to first item in sub menu
this.state[findTrueIndex].submenu.querySelectorAll("a")[0].focus();
this.overlayElement.classList.add(this.overlayOpen);
this.overlayElement.setAttribute("aria-hidden", "false");
this.documentBody.classList.add(this.bodyOverflow);
this.menuOpen = true;
}
}
}
export default MegaMenu
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment