UNPKG

accessible-menu

Version:

A JavaScript library to help you generate WCAG accessible menus in the DOM.

1,578 lines 84.4 kB
var j = Object.defineProperty; var V = (n, e, t) => e in n ? j(n, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : n[e] = t; var r = (n, e, t) => V(n, typeof e != "symbol" ? e + "" : e, t); function _(n, e) { n === "" || n.length === 0 || (typeof n == "string" ? e.classList.add(n) : e.classList.add(...n)); } function y(n, e) { n === "" || n.length === 0 || (typeof n == "string" ? e.classList.remove(n) : e.classList.remove(...n)); } function C(n, e) { try { if (typeof e != "object") { const t = typeof e; throw new TypeError( `Elements given to isValidInstance() must be inside of an object. "${t}" given.` ); } for (const t in e) if (!(e[t] instanceof n)) { const s = typeof e[t]; throw new TypeError( `${t} must be an instance of ${n.name}. "${s}" given.` ); } return { status: !0, error: null }; } catch (t) { return { status: !1, error: t }; } } function u(n, e) { try { if (typeof e != "object") { const t = typeof e; throw new TypeError( `Values given to isValidType() must be inside of an object. "${t}" given.` ); } for (const t in e) { const s = typeof e[t]; if (s !== n) throw new TypeError(`${t} must be a ${n}. "${s}" given.`); } return { status: !0, error: null }; } catch (t) { return { status: !1, error: t }; } } function A(n) { try { if (typeof n != "object") { const e = typeof n; throw new TypeError( `Values given to isQuerySelector() must be inside of an object. "${e}" given.` ); } for (const e in n) try { if (n[e] === null) throw new Error(); document.querySelector(n[e]); } catch { throw new TypeError( `${e} must be a valid query selector. "${n[e]}" given.` ); } return { status: !0, error: null }; } catch (e) { return { status: !1, error: e }; } } function b(n) { try { if (typeof n != "object" || Array.isArray(n)) { const e = typeof n; throw new TypeError( `Values given to isValidClassList() must be inside of an object. "${e}" given.` ); } for (const e in n) { const t = typeof n[e]; if (t !== "string") if (Array.isArray(n[e])) n[e].forEach((s) => { if (typeof s != "string") throw new TypeError( `${e} must be a string or an array of strings. An array containing non-strings given.` ); }); else throw new TypeError( `${e} must be a string or an array of strings. "${t}" given.` ); else { const s = {}; s[e] = n[e], A(s); } } return { status: !0, error: null }; } catch (e) { return { status: !1, error: e }; } } function z(n) { try { if (typeof n != "object") { const t = typeof n; throw new TypeError( `Values given to isValidState() must be inside of an object. "${t}" given.` ); } const e = ["none", "self", "child"]; for (const t in n) if (!e.includes(n[t])) throw new TypeError( `${t} must be one of the following values: ${e.join( ", " )}. "${n[t]}" given.` ); return { status: !0, error: null }; } catch (e) { return { status: !1, error: e }; } } function q(n) { try { if (typeof n != "object") { const t = typeof n; throw new TypeError( `Values given to isValidEvent() must be inside of an object. "${t}" given.` ); } const e = ["none", "mouse", "keyboard", "character"]; for (const t in n) if (!e.includes(n[t])) throw new TypeError( `${t} must be one of the following values: ${e.join( ", " )}. "${n[t]}" given.` ); return { status: !0, error: null }; } catch (e) { return { status: !1, error: e }; } } function x(n) { try { if (typeof n != "object") { const t = typeof n; throw new TypeError( `Values given to isValidHoverType() must be inside of an object. "${t}" given.` ); } const e = ["off", "on", "dynamic"]; for (const t in n) if (!e.includes(n[t])) throw new TypeError( `${t} must be one of the following values: ${e.join( ", " )}. "${n[t]}" given.` ); return { status: !0, error: null }; } catch (e) { return { status: !1, error: e }; } } function H(n, e) { if (u("string", { tagName: n }).status && C(HTMLElement, e).status) { const t = n.toLowerCase(); let s = !0; for (const i in e) e[i].tagName.toLowerCase() !== t && (s = !1); return s; } else return !1; } class O { /** * Constructs a new `BaseMenuToggle`. * * @param {object} options - The options for generating the menu toggle. * @param {HTMLElement} options.menuToggleElement - The toggle element in the DOM. * @param {HTMLElement} options.parentElement - The element containing the controlled menu. * @param {BaseMenu} options.controlledMenu - The menu controlled by this toggle. * @param {?BaseMenu} [options.parentMenu = null] - The menu containing this toggle. */ constructor({ menuToggleElement: e, parentElement: t, controlledMenu: s, parentMenu: i = null }) { /** * The DOM elements within the menu toggle. * * @protected * * @type {Object<HTMLElement>} * * @property {HTMLElement} toggle - The menu toggle. * @property {HTMLElement} parent - The menu containing this toggle. */ r(this, "_dom", { toggle: null, parent: null }); /** * The declared accessible-menu elements within the menu toggle. * * @protected * * @type {Object<BaseMenu>} * * @property {BaseMenu} controlledMenu - The menu controlled by this toggle. * @property {BaseMenu} parentMenu - The menu containing this toggle. */ r(this, "_elements", { controlledMenu: null, parentMenu: null }); /** * The open state of the menu toggle. * * @protected * * @type {boolean} */ r(this, "_open", !1); /** * The event that is triggered when the menu toggle expands. * * @protected * * @event accessibleMenuExpand * * @type {CustomEvent} * * @property {boolean} bubbles - A flag to bubble the event. * @property {Object<BaseMenuToggle>} details - The details object containing the BaseMenuToggle itself. */ r(this, "_expandEvent", new CustomEvent("accessibleMenuExpand", { bubbles: !0, detail: { toggle: this } })); /** * The event that is triggered when the menu toggle collapses. * * @protected * * @event accessibleMenuCollapse * * @type {CustomEvent} * * @property {boolean} bubbles - A flag to bubble the event. * @property {Object<BaseMenuToggle>} details - The details object containing the BaseMenuToggle itself. */ r(this, "_collapseEvent", new CustomEvent("accessibleMenuCollapse", { bubbles: !0, detail: { toggle: this } })); this._dom.toggle = e, this._dom.parent = t, this._elements.controlledMenu = s, this._elements.parentMenu = i; } /** * Initializes the menu toggle. * * The first steps are to ensure that the toggle and controlled menu have IDs * using the setIds method, and to set the ARIA attributes on the toggle * and controlled menu using the setAriaAttributes method. * * Then the collapse method is called to make sure the submenu is closed. */ initialize() { this._setIds(), this._setAriaAttributes(), this._collapse(!1); } /** * The DOM elements within the toggle. * * @readonly * * @type {Object<HTMLElement>} * * @see _dom */ get dom() { return this._dom; } /** * The declared accessible-menu elements within the toggle. * * @readonly * * @type {Object<BaseMenu>} * * @see _elements */ get elements() { return this._elements; } /** * The open state on the toggle. * * @type {boolean} * * @see _open */ get isOpen() { return this._open; } set isOpen(e) { u("boolean", { value: e }), this._open = e; } /** * Sets unique IDs for the toggle and controlled menu. * * If the toggle and controlled menu do not have IDs, the following steps take place: * - Generate a random string 1-10 characters long, * - Get the innerText of the toggle, * - Set the toggle's ID to: `menu-button-${toggle-inner-text}-${the-random-string}` * - Set the menu's ID to: `menu-${toggle-inner-text}-${the-random-string}` * * @protected */ _setIds() { var e; if (this.dom.toggle.id === "" || this.elements.controlledMenu.dom.menu.id === "") { const t = Math.random().toString(36).replace(/[^a-z]+/g, "").substring(0, 10); let s = ((e = this.dom.toggle.innerText) == null ? void 0 : e.replace(/[^a-zA-Z0-9\s]/g, "")) || "", i = t; !s.replace(/\s/g, "").length && this.dom.toggle.getAttribute("aria-label") && (s = this.dom.toggle.getAttribute("aria-label").replace(/[^a-zA-Z0-9\s]/g, "")), s.replace(/\s/g, "").length > 0 && (s = s.toLowerCase().replace(/\s+/g, "-"), s.startsWith("-") && (s = s.substring(1)), s.endsWith("-") && (s = s.slice(0, -1)), i = `${s}-${i}`), this.dom.toggle.id = this.dom.toggle.id || `menu-button-${i}`, this.elements.controlledMenu.dom.menu.id = this.elements.controlledMenu.dom.menu.id || `menu-${i}`; } } /** * Sets the ARIA attributes on the toggle and controlled menu. * * The first steps are to ensure that the toggle has `aria-expanded` * is initially set to "false". * * Then using the toggle and menu's IDs, the menu's `aria-labelledby` is set to * the toggle's ID. * * @protected */ _setAriaAttributes() { this.dom.toggle.setAttribute("aria-expanded", "false"), this.elements.controlledMenu.dom.menu.setAttribute( "aria-labelledby", this.dom.toggle.id ); } /** * Expands the controlled menu. * * Sets the toggle's `aria-expanded` to "true", adds the * open class to the toggle's parent menu item * and controlled menu, and removes the closed class * from the toggle's parent menu item and controlled menu. * * If `emit` is set to `true`, this will also emit a custom event * called accessibleMenuExpand * * @protected * * @fires accessibleMenuExpand * * @param {boolean} [emit = true] - A toggle to emit the expand event once expanded. */ _expand(e = !0) { const { closeClass: t, openClass: s, transitionClass: i, openDuration: o } = this.elements.controlledMenu; this.dom.toggle.setAttribute("aria-expanded", "true"), this.elements.controlledMenu.elements.rootMenu.hasOpened = !0, i !== "" ? (_(i, this.elements.controlledMenu.dom.menu), requestAnimationFrame(() => { y(t, this.elements.controlledMenu.dom.menu), requestAnimationFrame(() => { _(s, this.elements.controlledMenu.dom.menu), requestAnimationFrame(() => { setTimeout(() => { y( i, this.elements.controlledMenu.dom.menu ); }, o); }); }); })) : (_(s, this.elements.controlledMenu.dom.menu), y(t, this.elements.controlledMenu.dom.menu)), e && this.dom.toggle.dispatchEvent(this._expandEvent); } /** * Collapses the controlled menu. * * Sets the toggle's `aria-expanded` to "false", adds the * closed class to the toggle's parent menu item * and controlled menu, and removes the open class * from the toggle's parent menu item and controlled menu. * * If `emit` is set to `true`, this will also emit a custom event * called accessibleMenuCollapse * * @protected * * @fires accessibleMenuCollapse * * @param {boolean} [emit = true] - A toggle to emit the collapse event once collapsed. */ _collapse(e = !0) { const { closeClass: t, openClass: s, transitionClass: i, closeDuration: o } = this.elements.controlledMenu; this.dom.toggle.setAttribute("aria-expanded", "false"), i !== "" ? (_(i, this.elements.controlledMenu.dom.menu), requestAnimationFrame(() => { y(s, this.elements.controlledMenu.dom.menu), requestAnimationFrame(() => { _(t, this.elements.controlledMenu.dom.menu), requestAnimationFrame(() => { setTimeout(() => { y( i, this.elements.controlledMenu.dom.menu ); }, o); }); }); })) : (_(t, this.elements.controlledMenu.dom.menu), y(s, this.elements.controlledMenu.dom.menu)), e && this.dom.toggle.dispatchEvent(this._collapseEvent); } /** * Opens the controlled menu. * * Sets the controlled menu's focus state to "self" * and the parent menu's focus state to "child", calls expand, * and sets the isOpen value to `true`. * * @public */ open() { this.elements.controlledMenu.focusState = "self", this.isOpen || (this._expand(), this.isOpen = !0); } /** * Opens the controlled menu without the current focus entering it. * * Sets the controlled menu's focus state to "self" * and the parent menu's focus state to "child", * and calls expand. * * @public */ preview() { this.elements.parentMenu && (this.elements.parentMenu.focusState = "self"), this.isOpen || (this._expand(), this.isOpen = !0); } /** * Closes the controlled menu. * * Sets the controlled menu's focus state to "none" * and the parent menu's focus state to "self", blurs the controlled menu * and sets it's current child index to 0, * calls collapse, and sets * the isOpen value to `false`. * * @public */ close() { this.isOpen && (this.elements.controlledMenu.blur(), this.elements.parentMenu && (this.elements.parentMenu.focusState = "self"), this._collapse(), this.isOpen = !1); } /** * Toggles the open state of the controlled menu between `true` and `false`. * * @public */ toggle() { this.isOpen ? this.close() : this.open(); } /** * Closes all sibling menus. * * @public */ closeSiblings() { this.elements.parentMenu && this.elements.parentMenu.elements.submenuToggles.forEach((e) => { e !== this && e.close(); }); } /** * Closes all child menus. * * @public */ closeChildren() { this.elements.controlledMenu.elements.submenuToggles.forEach( (e) => e.close() ); } } class $ { /** * Constructs a new `BaseMenuItem`. * * @param {object} options - The options for generating the menu item. * @param {HTMLElement} options.menuItemElement - The menu item in the DOM. * @param {HTMLElement} options.menuLinkElement - The menu item's link in the DOM. * @param {BaseMenu} options.parentMenu - The parent menu. * @param {boolean} [options.isSubmenuItem = false] - A flag to mark if the menu item is controlling a submenu. * @param {?BaseMenu} [options.childMenu = null] - The child menu. * @param {?BaseMenuToggle} [options.toggle = null] - The controller for the child menu. */ constructor({ menuItemElement: e, menuLinkElement: t, parentMenu: s, isSubmenuItem: i = !1, childMenu: o = null, toggle: l = null }) { /** * The DOM elements within the menu item. * * @protected * * @type {Object<HTMLElement>} * * @property {HTMLElement} item - The menu item. * @property {HTMLElement} link - The menu item's link. */ r(this, "_dom", { item: null, link: null }); /** * The declared accessible-menu elements within the menu item. * * @protected * * @type {Object<BaseMenu, BaseMenuToggle>} * * @property {BaseMenu} parentMenu - The menu containing this menu item. * @property {?BaseMenu} childMenu - The menu contained within this menu item. * @property {?BaseMenuToggle} toggle - The menu toggle within this menu item that controls the `childMenu`. */ r(this, "_elements", { parentMenu: null, childMenu: null, toggle: null }); /** * A flag marking a submenu item. * * @protected * * @type {boolean} */ r(this, "_submenu", !1); this._dom.item = e, this._dom.link = t, this._elements.parentMenu = s, this._elements.childMenu = o, this._elements.toggle = l, this._submenu = i; } /** * Initialize the menu item. */ initialize() { } /** * The DOM elements within the menu item. * * @readonly * * @type {Object<HTMLElement>} * * @see _dom */ get dom() { return this._dom; } /** * The declared accessible-menu elements within the menu item. * * @readonly * * @type {Object<BaseMenu, BaseMenuToggle>} * * @see _elements */ get elements() { return this._elements; } /** * A flag marking a submenu item. * * @readonly * * @type {boolean} * * @see _submenu */ get isSubmenuItem() { return this._submenu; } /** * Focuses the menu item's link if the parent menu's * shouldFocus value is `true`. * * @public */ focus() { this.elements.parentMenu.shouldFocus && requestAnimationFrame(() => { this.dom.link.focus(); }); } /** * Blurs the menu item's link if the parent menu's * shouldFocus value is `true`. * * @public */ blur() { this.elements.parentMenu.shouldFocus && requestAnimationFrame(() => { this.dom.link.blur(); }); } } function T(n) { try { const e = n.key || n.keyCode, t = { Enter: e === "Enter" || e === 13, Space: e === " " || e === "Spacebar" || e === 32, Escape: e === "Escape" || e === "Esc" || e === 27, ArrowUp: e === "ArrowUp" || e === "Up" || e === 38, ArrowRight: e === "ArrowRight" || e === "Right" || e === 39, ArrowDown: e === "ArrowDown" || e === "Down" || e === 40, ArrowLeft: e === "ArrowLeft" || e === "Left" || e === 37, Home: e === "Home" || e === 36, End: e === "End" || e === 35, Character: isNaN(e) && !!e.match(/^[a-zA-Z]{1}$/), Tab: e === "Tab" || e === 9, Asterisk: e === "*" || e === 56 }; return Object.keys(t).find((s) => t[s] === !0) || ""; } catch { return ""; } } function c(n) { n.preventDefault(), n.stopPropagation(); } class E { /** * Constructs a new `BaseMenu`. * * @param {object} options - The options for generating the menu. * @param {HTMLElement} options.menuElement - The menu element in the DOM. * @param {string} [options.menuItemSelector = li] - The query selector string for menu items. * @param {string} [options.menuLinkSelector = a] - The query selector string for menu links. * @param {string} [options.submenuItemSelector = li:has(ul)] - The query selector string for menu items containing submenus. * @param {string} [options.submenuToggleSelector = a] - The query selector string for submenu toggle buttons/links. * @param {string} [options.submenuSelector = ul] - The query selector string for submenus. * @param {?HTMLElement} [options.controllerElement = null] - The element controlling the menu in the DOM. * @param {?HTMLElement} [options.containerElement = null] - The element containing the menu in the DOM. * @param {?(string|string[])} [options.openClass = show] - The class to apply when a menu is "open". * @param {?(string|string[])} [options.closeClass = hide] - The class to apply when a menu is "closed". * @param {?(string|string[])} [options.transitionClass = transitioning] - The class to apply when a menu is transitioning between "open" and "closed" states. * @param {number} [options.transitionDuration = 250] - The duration of the transition between "open" and "closed" states (in milliseconds). * @param {boolean} [options.openDuration = -1] - The duration of the transition from "closed" to "open" states (in milliseconds). * @param {boolean} [options.closeDuration = -1] - The duration of the transition from "open" to "closed" states (in milliseconds). * @param {boolean} [options.isTopLevel = false] - A flag to mark the root menu. * @param {?BaseMenu} [options.parentMenu = null] - The parent menu to this menu. * @param {string} [options.hoverType = off] - The type of hoverability a menu has. * @param {number} [options.hoverDelay = 250] - The delay for opening and closing menus if the menu is hoverable (in milliseconds). * @param {number} [options.enterDelay = -1] - The delay for opening menus if the menu is hoverable (in milliseconds). * @param {number} [options.leaveDelay = -1] - The delay for closing menus if the menu is hoverable (in milliseconds). * @param {?string} [options.prefix = am-] - The prefix to use for CSS custom properties. */ constructor({ menuElement: e, menuItemSelector: t = "li", menuLinkSelector: s = "a", submenuItemSelector: i = "li:has(ul)", submenuToggleSelector: o = "a", submenuSelector: l = "ul", controllerElement: h = null, containerElement: a = null, openClass: g = "show", closeClass: d = "hide", transitionClass: f = "transitioning", transitionDuration: p = 250, openDuration: m = -1, closeDuration: v = -1, isTopLevel: M = !0, parentMenu: D = null, hoverType: w = "off", hoverDelay: I = 250, enterDelay: L = -1, leaveDelay: S = -1, prefix: k = "am-" }) { /** * The class to use when generating submenus. * * @protected * * @type {typeof BaseMenu} */ r(this, "_MenuType", E); /** * The class to use when generating menu items. * * @protected * * @type {typeof BaseMenuItem} */ r(this, "_MenuItemType", $); /** * The class to use when generating submenu toggles. * * @protected * * @type {typeof BaseMenuToggle} */ r(this, "_MenuToggleType", O); /** * The DOM elements within the menu. * * @protected * * @type {Object<HTMLElement, HTMLElement[]>} * * @property {HTMLElement} menu - The menu element. * @property {HTMLElement[]} menuItems - An array of menu items. * @property {HTMLElement[]} submenuItems - An array of menu items that also contain submenu elements. * @property {HTMLElement[]} submenuToggles - An array of menu links that function as submenu toggles. * @property {HTMLElement[]} submenus - An array of submenu elements. * @property {HTMLElement} controller - The toggle for this menu. * @property {HTMLElement} container - The container for this menu. */ r(this, "_dom", { menu: null, menuItems: [], submenuItems: [], submenuToggles: [], submenus: [], controller: null, container: null }); /** * The query selectors used by the menu to populate the dom. * * @protected * * @type {Object<string>} * * @property {string} menuItems - The query selector for menu items. * @property {string} menuLinks - The query selector for menu links. * @property {string} submenuItems - The query selector for menu items containing submenus. * @property {string} submenuToggles - The query selector for menu links that function as submenu toggles. * @property {string} submenus - The query selector for for submenus. */ r(this, "_selectors", { menuItems: "", menuLinks: "", submenuItems: "", submenuToggles: "", submenus: "" }); /** * The declared accessible-menu elements within the menu. * * @protected * * @type {Object<BaseMenu, BaseMenuToggle, BaseMenuItem[], BaseMenuToggle[]>} * * @property {BaseMenuItem[]} menuItems - An array of menu items. * @property {BaseMenuToggle[]} submenuToggles - An array of menu toggles. * @property {?BaseMenuToggle} controller - A menu toggle that controls this menu. * @property {?BaseMenu} parentMenu - The parent menu. * @property {?BaseMenu} rootMenu - The root menu of the menu tree. */ r(this, "_elements", { menuItems: [], submenuToggles: [], controller: null, parentMenu: null, rootMenu: null }); /** * The class(es) to apply when the menu is open. * * @protected * * @type {string|string[]} */ r(this, "_openClass", "show"); /** * The class(es) to apply when the menu is closed. * * @protected * * @type {string|string[]} */ r(this, "_closeClass", "hide"); /** * The class(es) to apply when the menu is transitioning between states. * * @protected * * @type {string|string[]} */ r(this, "_transitionClass", "transitioning"); /** * The duration time (in milliseconds) for the transition between open and closed states. * * @protected * * @type {number} */ r(this, "_transitionDuration", 250); /** * The duration time (in milliseconds) for the transition from closed to open states. * * @protected * * @type {number} */ r(this, "_openDuration", -1); /** * The duration time (in milliseconds) for the transition from open to closed states. * * @protected * * @type {number} */ r(this, "_closeDuration", -1); /** * A flag marking the root menu. * * @protected * * @type {boolean} */ r(this, "_root", !0); /** * The index of the currently selected menu item in the menu. * * @protected * * @type {number} */ r(this, "_currentChild", 0); /** * The current state of the menu's focus. * * @protected * * @type {string} */ r(this, "_focusState", "none"); /** * This last event triggered on the menu. * * @protected * * @type {string} */ r(this, "_currentEvent", "none"); /** * The type of hoverability for the menu. * * @protected * * @type {string} */ r(this, "_hoverType", "off"); /** * The delay time (in milliseconds) used for pointerenter/pointerleave events to take place. * * @protected * * @type {number} */ r(this, "_hoverDelay", 250); /** * The delay time (in milliseconds) used for pointerenter events to take place. * * @protected * * @type {number} */ r(this, "_enterDelay", -1); /** * The delay time (in milliseconds) used for pointerleave events to take place. * * @protected * * @type {number} */ r(this, "_leaveDelay", -1); /** * The prefix to use for CSS custom properties. * * @protected * * @type {string} */ r(this, "_prefix", "am-"); /** * A variable to hold the hover timeout function. * * @protected * * @type {?Function} */ r(this, "_hoverTimeout", null); /** * A flag to check if the menu can dynamically hover based on if a menu has been opened already. * * @protected * * @type {boolean} */ r(this, "_hasOpened", !1); /** * An array of error messages generated by the menu. * * @protected * * @type {string[]} */ r(this, "_errors", []); this._dom.menu = e, this._dom.controller = h, this._dom.container = a, this._selectors.menuItems = t, this._selectors.menuLinks = s, this._selectors.submenuItems = i, this._selectors.submenuToggles = o, this._selectors.submenus = l, this._elements.menuItems = [], this._elements.submenuToggles = [], this._elements.controller = null, this._elements.parentMenu = D, this._elements.rootMenu = M ? this : null, this._openClass = g || "", this._closeClass = d || "", this._transitionClass = f || "", this._transitionDuration = p, this._openDuration = m, this._closeDuration = v, this._prefix = k || "", this._root = M, this._hoverType = w, this._hoverDelay = I, this._enterDelay = L, this._leaveDelay = S; } /** * Initializes the menu. * * The following steps will be taken to initialize the menu: * - Validate that the menu can initialize. * - Find the root menu of the menu tree if it isn't already set. * - Populate all DOM elements within the dom. * - If the current menu is the root menu _and_ has a controller, initialize * the controller. * - Populate the menu elements within the elements. * - Set the transition duration custom prop for the menu. * * @public * * @throws {Error} Will throw an Error if validate returns `false`. */ initialize() { if (!this._validate()) throw new Error( `AccessibleMenu: cannot initialize menu. The following errors have been found: - ${this.errors.join( ` - ` )}` ); if (this.elements.rootMenu === null && this._findRootMenu(this), this._setDOMElements(), this.isTopLevel && this.dom.controller && this.dom.container) { const e = new this._MenuToggleType({ menuToggleElement: this.dom.controller, parentElement: this.dom.container, controlledMenu: this }); H("button", { toggle: e.dom.toggle }) || e.dom.toggle.setAttribute("role", "button"), e.dom.toggle.setAttribute("aria-controls", this.dom.menu.id), this._elements.controller = e; } this._createChildElements(), this._setTransitionDurations(), this.isTopLevel && (window.AccessibleMenu = window.AccessibleMenu || { menus: {} }, window.AccessibleMenu.menus[this.dom.menu.id] = this); } /** * The DOM elements within the menu. * * @readonly * * @type {Object<HTMLElement, HTMLElement[]>} * * @see _dom */ get dom() { return this._dom; } /** * The query selectors used by the menu to populate the dom. * * @readonly * * @type {Object<string>} * * @see _selectors */ get selectors() { return this._selectors; } /** * The declared accessible-menu elements within the menu. * * @readonly * * @type {Object<BaseMenu, BaseMenuToggle, BaseMenuItem[], BaseMenuToggle[]>} * * @see _elements */ get elements() { return this._elements; } /** * The flag marking the root menu. * * @readonly * * @type {boolean} * * @see _root */ get isTopLevel() { return this._root; } /** * The class(es) to apply when the menu is open. * * This functions differently for root vs. submenus. * Submenus will always inherit their root menu's open class(es). * * @type {string|string[]} * * @see _openClass */ get openClass() { return this.isTopLevel ? this._openClass : this.elements.rootMenu.openClass; } /** * The class(es) to apply when the menu is closed. * * This functions differently for root vs. submenus. * Submenus will always inherit their root menu's close class(es). * * @type {string|string[]} * * @see _closeClass */ get closeClass() { return this.isTopLevel ? this._closeClass : this.elements.rootMenu.closeClass; } /** * The class(es) to apply when the menu is transitioning between open and closed. * * This functions differently for root vs. submenus. * Submenus will always inherit their root menu's transition class(es). * * @type {string|string[]} * * @see _transitionClass */ get transitionClass() { return this.isTopLevel ? this._transitionClass : this.elements.rootMenu.transitionClass; } /** * The duration time (in milliseconds) for the transition between open and closed states. * * This functions differently for root vs. submenus. * Submenus will always inherit their root menu's transition duration. * * Setting this value will also set the --am-transition-duration CSS custom property on the menu. * * @type {number} * * @see _transitionDuration */ get transitionDuration() { return this.isTopLevel ? this._transitionDuration : this.elements.rootMenu.transitionDuration; } /** * The duration time (in milliseconds) for the transition from closed to open states. * * This functions differently for root vs. submenus. * Submenus will always inherit their root menu's openDuration. * * If openDuration is set to -1, the transitionDuration value will be used instead. * * Setting this value will also set the --am-open-transition-duration CSS custom property on the menu. * * @type {number} * * @see _openDuration */ get openDuration() { return this._openDuration === -1 ? this.transitionDuration : this.isTopLevel ? this._openDuration : this.elements.rootMenu.openDuration; } /** * The duration time (in milliseconds) for the transition from open to closed states. * * This functions differently for root vs. submenus. * Submenus will always inherit their root menu's closeDuration. * * If closeDuration is set to -1, the transitionDuration value will be used instead. * * Setting this value will also set the --am-close-transition-duration CSS custom property on the menu. * * @type {number} * * @see _closeDuration */ get closeDuration() { return this._closeDuration === -1 ? this.transitionDuration : this.isTopLevel ? this._closeDuration : this.elements.rootMenu.closeDuration; } /** * The index of the currently selected menu item in the menu. * * - Attempting to set a value less than -1 will set the current child to -1. * - Attempting to set a value greater than or equal to the number of menu items * will set the current child to the index of the last menu item in the menu. * * If the current menu has a parent menu _and_ the menu's * current event is "mouse", The parent menu * will have it's current child updated as well to help with transitioning * between mouse and keyboard navigation. * * @type {number} * * @see _currentChild */ get currentChild() { return this._currentChild; } /** * The current state of the menu's focus. * * - If the menu has submenus, setting the focus state to "none" or "self" will * update all child menus to have the focus state of "none". * - If the menu has a parent menu, setting the focus state to "self" or "child" * will update all parent menus to have the focus state of "child". * * @type {string} * * @see _focusState */ get focusState() { return this._focusState; } /** * The last event triggered on the menu. * * @type {string} * * @see _currentEvent */ get currentEvent() { return this._currentEvent; } /** * The currently selected menu item. * * @readonly * * @type {BaseMenuItem} */ get currentMenuItem() { return this.elements.menuItems[this.currentChild]; } /** * The type of hoverability for the menu. * * This functions differently for root vs. submenus. * Submenus will always inherit their root menu's hoverability. * * @type {string} * * @see _hoverType */ get hoverType() { return this._root ? this._hoverType : this.elements.rootMenu.hoverType; } /** * The delay time (in milliseconds) used for pointerenter/pointerleave events to take place. * * This functions differently for root vs. submenus. * Submenus will always inherit their root menu's hover delay. * * @type {number} * * @see _hoverDelay */ get hoverDelay() { return this._root ? this._hoverDelay : this.elements.rootMenu.hoverDelay; } /** * The delay time (in milliseconds) used for pointerenter events to take place. * * This functions differently for root vs. submenus. * Submenus will always inherit their root menu's enter delay. * * If enterDelay is set to -1, the hoverDelay value will be used instead. * * @type {number} * * @see _enterDelay */ get enterDelay() { return this._enterDelay === -1 ? this.hoverDelay : this._root ? this._enterDelay : this.elements.rootMenu.enterDelay; } /** * The delay time (in milliseconds) used for pointerleave events to take place. * * This functions differently for root vs. submenus. * Submenus will always inherit their root menu's leave delay. * * If leaveDelay is set to -1, the hoverDelay value will be used instead. * * @type {number} * * @see _leaveDelay */ get leaveDelay() { return this._leaveDelay === -1 ? this.hoverDelay : this._root ? this._leaveDelay : this.elements.rootMenu.leaveDelay; } /** * The prefix to use for CSS custom properties. * * This functions differently for root vs. submenus. * Submenus will always inherit their root menu's prefix. * * @type {string} * * @see _prefix */ get prefix() { return this._root ? this._prefix : this.elements.rootMenu.prefix; } /** * A flag to check if the menu's focus methods should _actually_ move the focus in the DOM. * * This will be `false` unless any of the following criteria are met: * - The menu's current event is "keyboard". * - The menu's current event is "character". * - The menu's current event is "mouse" _and_ the menu's * hover type is "dynamic". * * @readonly * * @type {boolean} */ get shouldFocus() { let e = !1; return (this.currentEvent === "keyboard" || this.currentEvent === "character") && (e = !0), this.currentEvent === "mouse" && this.hoverType === "dynamic" && (e = !0), e; } /** * A flag to check if the menu can dynamically hover. * * This functions differently for root vs. submenus. * Submenus will always inherit their root menu's hasOpened. * * @type {boolean} * * @see _hasOpened */ get hasOpened() { return this._root ? this._hasOpened : this.elements.rootMenu.hasOpened; } /** * An array of error messages generated by the menu. * * @readonly * * @type {string[]} * * @see _errors */ get errors() { return this._errors; } set openClass(e) { b({ openClass: e }), this._openClass !== e && (this._openClass = e); } set closeClass(e) { b({ closeClass: e }), this._closeClass !== e && (this._closeClass = e); } set transitionClass(e) { b({ transitionClass: e }), this._transitionClass !== e && (this._transitionClass = e); } set transitionDuration(e) { u("number", { value: e }), this._transitionDuration !== e && (this._transitionDuration = e, this._setTransitionDurations()); } set openDuration(e) { u("number", { value: e }), this._openDuration !== e && (this._openDuration = e, this._setTransitionDurations()); } set closeDuration(e) { u("number", { value: e }), this._closeDuration !== e && (this._closeDuration = e, this._setTransitionDurations()); } set currentChild(e) { u("number", { value: e }); function t(s) { if (["mouse", "character"].includes(s.currentEvent) && s.elements.parentMenu) { let o = 0, l = !1; for (; !l && o < s.elements.parentMenu.elements.menuItems.length; ) { const h = s.elements.parentMenu.elements.menuItems[o]; h.isSubmenuItem && h.elements.toggle.elements.controlledMenu === s && (l = !0, s.elements.parentMenu.currentEvent = s.currentEvent, s.elements.parentMenu.currentChild = o), o++; } } } e < -1 ? (this._currentChild = -1, t(this)) : e >= this.elements.menuItems.length ? (this._currentChild = this.elements.menuItems.length - 1, t(this)) : this.focusChild !== e && (this._currentChild = e, t(this)); } set focusState(e) { z({ value: e }), this._focusState !== e && (this._focusState = e), this.elements.submenuToggles.length > 0 && (e === "self" || e === "none") && this.elements.submenuToggles.forEach((t) => { t.elements.controlledMenu.focusState = "none"; }), this.elements.parentMenu && (e === "self" || e === "child") && (this.elements.parentMenu.focusState = "child"); } set currentEvent(e) { q({ value: e }), this._currentEvent !== e && (this._currentEvent = e, this.elements.submenuToggles.length > 0 && this.elements.submenuToggles.forEach((t) => { t.elements.controlledMenu.currentEvent = e; })); } set hoverType(e) { x({ value: e }), this._hoverType !== e && (this._hoverType = e); } set hoverDelay(e) { u("number", { value: e }), this._hoverDelay !== e && (this._hoverDelay = e); } set enterDelay(e) { u("number", { value: e }), this._enterDelay !== e && (this._enterDelay = e); } set leaveDelay(e) { u("number", { value: e }), this._leaveDelay !== e && (this._leaveDelay = e); } set prefix(e) { u("string", { value: e }), this._prefix !== e && (this._prefix = e); } set hasOpened(e) { u("boolean", { value: e }), this._hasOpened !== e && (this._hasOpened = e); } /** * Validates all aspects of the menu to ensure proper functionality. * * @protected * * @return {boolean} - The result of the validation. */ _validate() { let e = !0, t; this._dom.container !== null || this._dom.controller !== null ? t = C(HTMLElement, { menuElement: this._dom.menu, controllerElement: this._dom.controller, containerElement: this._dom.container }) : t = C(HTMLElement, { menuElement: this._dom.menu }), t.status || (this._errors.push(t.error.message), e = !1); let s; if (this._selectors.submenuItems !== "" ? s = A({ menuItemSelector: this._selectors.menuItems, menuLinkSelector: this._selectors.menuLinks, submenuItemSelector: this._selectors.submenuItems, submenuToggleSelector: this._selectors.submenuToggles, submenuSelector: this._selectors.submenus }) : s = A({ menuItemSelector: this._selectors.menuItems, menuLinkSelector: this._selectors.menuLinks }), s.status || (this._errors.push(s.error.message), e = !1), this._openClass !== "") { const m = b({ openClass: this._openClass }); m.status || (this._errors.push(m.error.message), e = !1); } if (this._closeClass !== "") { const m = b({ closeClass: this._closeClass }); m.status || (this._errors.push(m.error.message), e = !1); } if (this._transitionClass !== "") { const m = b({ transitionClass: this._transitionClass }); m.status || (this._errors.push(m.error.message), e = !1); } const i = u("number", { transitionDuration: this._transitionDuration }); i.status || (this._errors.push(i.error.message), e = !1); const o = u("number", { openDuration: this._openDuration }); o.status || (this._errors.push(o.error.message), e = !1); const l = u("number", { closeDuration: this._closeDuration }); l.status || (this._errors.push(l.error.message), e = !1); const h = u("boolean", { isTopLevel: this._root }); if (h.status || (this._errors.push(h.error.message), e = !1), this._elements.parentMenu !== null) { const m = C(E, { parentMenu: this._elements.parentMenu }); m.status || (this._errors.push(m.error.message), e = !1); } const a = x({ hoverType: this._hoverType }); a.status || (this._errors.push(a.error.message), e = !1); const g = u("number", { hoverDelay: this._hoverDelay }); g.status || (this._errors.push(g.error.message), e = !1); const d = u("number", { enterDelay: this._enterDelay }); d.status || (this._errors.push(d.error.message), e = !1); const f = u("number", { leaveDelay: this._leaveDelay }); f.status || (this._errors.push(f.error.message), e = !1); const p = u("string", { prefix: this._prefix }); return p.status || (this._errors.push(p.error.message), e = !1), e; } /** * Sets DOM elements within the menu. * * Elements that are not stored inside an array cannot be set through this method. * * @protected * * @param {string} elementType - The type of element to populate. * @param {HTMLElement} [base = this.dom.menu] - The element used as the base for the querySelect. * @param {boolean} [overwrite = true] - A flag to set if the existing elements will be overwritten. */ _setDOMElementType(e, t = this.dom.menu, s = !0) { if (typeof this.selectors[e] == "string") { if (!Array.isArray(this.dom[e])) throw new Error( `AccessibleMenu: The "${e}" element cannot be set through _setDOMElementType.` ); t !== this.dom.menu && C(HTMLElement, { base: t }); const o = Array.from( t.querySelectorAll(this.selectors[e]) ).filter( (l) => l.parentElement === t ); s ? this._dom[e] = o : this._dom[e] = [ ...this._dom[e], ...o ]; } else throw new Error( `AccessibleMenu: "${e}" is not a valid element type within the menu.` ); } /** * Resets DOM elements within the menu. * * Elements that are not stored inside an array cannot be reset through this method. * * @protected * * @param {string} elementType - The type of element to clear. */ _resetDOMElementType(e) { if (typeof this.dom[e] < "u") { if (!Array.isArray(this.dom[e])) throw new Error( `AccessibleMenu: The "${e}" element cannot be reset through _resetDOMElementType.` ); this._dom[e] = []; } else throw new Error( `AccessibleMenu: "${e}" is not a valid element type within the menu.` ); } /** * Sets all DOM elements within the menu. * * Utilizes _setDOMElementType and * _resetDOMElementType. * * @protected */ _setDOMElements() { this._setDOMElementType("menuItems"), this.selectors.submenuItems !== "" && (this._setDOMElementType("submenuItems"), this._resetDOMElementType("submenuToggles"), this._resetDOMElementType("submenus"), this.dom.submenuItems.forEach((e) => { this._setDOMElementType("submenuToggles", e, !1), this._setDOMElementType("submenus", e, !1); })); } /** * Finds the root menu element. * * @protected * * @param {BaseMenu} menu - The menu to check. */ _findRootMenu(e) { if (e.isTopLevel) this._elements.rootMenu = e; else if (e.elements.parentMenu !== null) this._findRootMenu(e.elements.parentMenu); else throw new Error("Cannot find root menu."); } /** * Creates and initializes all menu items and submenus. * * @protected */ _createChildElements() { this.dom.menuItems.forEach((e) => { let t; if (this.dom.submenuItems.includes(e)) { const s = e.querySelector(this.selectors.submenuToggles), i = e.querySelector(this.selectors.submenus), o = new this._MenuType({ menuElement: i, menuItemSelector: this.selectors.menuItems, menuLinkSelector: this.selectors.menuLinks, submenuItemSelector: this.selectors.submenuItems, submenuToggleSelector: this.selectors.submenuToggles, submenuSelector: this.selectors.submenus, openClass: this.openClass, closeClass: this.closeClass, transitionClass: this.transitionClass, transitionDuration: this.transitionDuration, openDuration: this.openDuration, closeDuration: this.closeDuration, isTopLevel: !1, parentMenu: this, hoverType: this.hoverType, hoverDelay: this.hoverDelay, enterDelay: this.enterDelay, leaveDelay: this.leaveDelay }), l = new this._MenuToggleType({ menuToggleElement: s, parentElement: e, controlledMenu: o, parentMenu: this }); this._elements.submenuToggles.push(l), t = new this._MenuItemType({ menuItemElement: e, menuLinkElement: s, parentMenu: this, isSubmenuItem: !0, childMenu: o, toggle: l }); } else { const s = e.querySelector(this.selectors.menuLinks); t = new this._MenuItemType({ menuItemElement: e, menuLinkElement: s, parentMenu: this }); } this._elements.menuItems.push(t); }); } /** * Clears the hover timeout. * * @protected */ _clearTimeout() { clearTimeout(this._hoverTimeout); } /** * Sets the hover timeout. * * @protected * * @param {Function} callback - The callback function to execute. * @param {number} delay - The delay time in milliseconds. */ _setTimeout(e, t) { u("function", { callbac