Date de création 2023-02-20Date d’expiration 2023-05-24/** * Install an A11Y burger menu for every element having the "burger" class. * * The system is meant to degrade gracefully, has A11Y in mind and other * features: * * - If JavaScript is disabled, the menu is left as-is. * - If CSS is missing, the menu is left as-is with an ineffective button * - The menu can be used with keyboard. * - The script has no dependency. * - The script tries to optimize CSS selectors, JavaScript resources etc. * * The styling for the menu is outside the scope of this script. * * Before running or without JavaScript, the following structure is expected: * * <nav class="burger> * <h2 class="visually-hidden>...</h2> * <ul>...</ul> * </nav> * * At start or when the menu is closed, the following structure is expected: * * <nav class="burger burger-menu burger-closed"> * <button class="burger-button burger-closed"> * <span class="visually-hidden">Ouvrir </span> * Menu * </button> * <h2 class="visually-hidden burger-closed">...</h2> * <ul class="burger-list burger-closed">...</ul> * </nav> * * When the menu is open, the following structure is expected: * * <nav class="burger burger-menu"> * <button class="burger-button"> * <span class="visually-hidden">Ouvrir </span> * Menu * </button> * <h2 class="visually-hidden">...</h2> * <ul class="burger-list">...</ul> * </nav> * * @author Frédéric BISSON <fbisson@rouen.fr> */ (function() { /** * Default title which will be displayed on the button. * @constant {string} */ const TITLE_MENU = "Menu" /** * If the menu contains a tag of this particular name, its text content * will be displayed on the button. * @constant {string} */ const TITLE_TAGNAME = "h2" /** * This text will be inserted at the beginning of the button title if the * menu is closed. It is meant to be visually hidden. * @constant {string} */ const NOTE_OPEN = "Ouvrir " /** * This text will be inserted at the beginning of the button title if the * menu is open. It is meant to be visually hidden. * @constant {string} */ const NOTE_CLOSE = "Fermer " /** * This script will only work on blocks having this class. This class * should not be used to style elements. * @constant {string} */ const CLASS_INSTALL = "burger" /** * This class will be added by the script to every block having the * CLASS_INSTALL class. It is meant for styling. * @constant {string} */ const CLASS_MENU = "burger-menu" /** * This class will be added by the script to every ul inside a block having * the CLASS_INSTALL class. It is meant for styling. * @constant {string} */ const CLASS_LIST = "burger-list" /** * This class will be added to the button of the menu. It is meant for * styling. * @constant {string} */ const CLASS_BUTTON = "burger-button" /** * If the menu is closed (not displayed), the block containing the menu and * the button will have this class. The class is removed when the menu is * open. It is meant for styling. * @constant {string} */ const CLASS_CLOSED = "burger-closed" /** * This is the class used to signal content that should be visually hidden * but still available for A11Y tools. * @constant {string} */ const CLASS_VISUALLY_HIDDEN = "visually-hidden" /** * Get title from a menu. * * It looks for the first tag with the specifid tag name and returns its * text content. If no such tag exists, it returns the default title. * * @param {HTMLElement} menu - The HTMLElement to search the title tag in. * @param {string} tagName - The title tag name ("h2", "h3"…). * @param {string} defaultTitle - The default title ("Menu"…) * @returns {string} */ function getTitle(menu, tagName, defaultTitle) { const tagNames = menu.getElementsByTagName(tagName) return (tagNames.length > 0 ? tagNames[0].textContent : defaultTitle) } /** * Create and install the burger menu for an HTMLElement. * * A button with the CLASS_BUTTON class is created and inserted before the * element. * * The menu element receives the CLASS_MENU class. * * The menu and its button receives the CLASS_CLOSED class. * * @param {HTMLElement} menu - The HTMLElement to work on (usually a nav) */ function installOn(menu) { // The menu variable needs to point to a nav tag. If it does not, try // to work on the first child. if (menu.tagName !== "NAV") menu = menu.children[0] // Create the menu button. const button = document.createElement("button") button.classList.add(CLASS_BUTTON) button.setAttribute("type", "button") // Set button title. const note = document.createElement("span") note.classList.add(CLASS_VISUALLY_HIDDEN) note.append(NOTE_OPEN) button.append(note, getTitle(menu, TITLE_TAGNAME, TITLE_MENU)) // Insert the button before the menu. menu.insertBefore(button, menu.firstChild) // Toggle menu state on button click. button.addEventListener("click", () => toggleMenu(button, note, menu)) menu.classList.add(CLASS_MENU); Array.from(menu.children).forEach( (element) => { if (element.tagName === "UL" || element.tagName === "OL") { element.classList.add(CLASS_LIST) } } ) Array.from(menu.getElementsByClassName(CLASS_INSTALL)) .forEach(installOn) toggleMenu(button, note, menu) } /** * Toggle menu and button state. * * It toggles the CLASS_CLOSED class on menu and button and updates the * A11Y note accordingly. * * @param {HTMLElement} button - The menu button * @param {HTMLElement} note - The A11Y text before the button title * @param {HTMLElement} menu - The menu element */ function toggleMenu(button, note, menu) { Array.from(menu.children).forEach( (element) => element.classList.toggle(CLASS_CLOSED) ) note.innertText = button.classList.contains(CLASS_CLOSED) ? NOTE_OPEN : NOTE_CLOSE } window.addEventListener( "DOMContentLoaded", () => { Array.from(document.getElementsByClassName(CLASS_INSTALL)) .forEach(installOn) }, false ) })()