accessible-menu
Version:
A JavaScript library to help you generate WCAG accessible menus in the DOM.
1,578 lines • 84.4 kB
JavaScript
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