Instantly share code, notes, and snippets.
Last active
September 11, 2017 07:25
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| .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; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // custom selectors to be made | |
| const MegaMenu = new MegaMenu(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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