Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

accessible-menu

Package Overview
Dependencies
Maintainers
1
Versions
47
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

accessible-menu - npm Package Compare versions

Comparing version 1.0.0-beta.3 to 1.0.0-beta.4

11

CHANGELOG.md

@@ -5,2 +5,13 @@ # Changelog

## [1.0.0-beta.4](https://github.com/NickDJM/accessible-menu/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2019-11-22)
### ⚠ BREAKING CHANGES
* **menu:** Constructor params converted to object
### Features
* **menu:** add support for root dropdown menus ([1f85f4e](https://github.com/NickDJM/accessible-menu/commit/1f85f4e729fee4b1a05b7847d77c73209502a08b)), closes [#15](https://github.com/NickDJM/accessible-menu/issues/15)
## [1.0.0-beta.3](https://github.com/NickDJM/accessible-menu/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2019-11-21)

@@ -7,0 +18,0 @@

201

dist/accessibleMenu.js

@@ -33,6 +33,10 @@ "use strict";

*
* @param {object} menuItemElement - The menu item in the DOM.
* @param {Menu} parentMenu - The parent menu.
* @param {object} param0 - The menu item object.
* @param {object} param0.menuItemElement - The menu item in the DOM.
* @param {Menu} param0.parentMenu - The parent menu.
*/
function MenuItem(menuItemElement, parentMenu) {
function MenuItem(_ref) {
var menuItemElement = _ref.menuItemElement,
parentMenu = _ref.parentMenu;
_classCallCheck(this, MenuItem);

@@ -116,2 +120,8 @@

},
parentElement: function parentElement(value) {
// Ensure value is an HTML element.
if (!(value instanceof HTMLElement)) {
throw new TypeError("parentElement must be an HTML Element.");
}
},
menu: function menu(value) {

@@ -144,10 +154,2 @@ // Ensure value is an Menu element.

},
parentMenuItem: function parentMenuItem(value) {
// Value is allowed to be null.
if (value === null) return;
if (!(value instanceof MenuItem)) {
throw new TypeError("parentMenuItem must be a MenuItem.");
}
},
rootMenu: function rootMenu(value) {

@@ -169,14 +171,20 @@ // Value is allowed to be null.

*
* @param {object} menuToggleElement - The toggle element in the DOM.
* @param {Menu} menu - The menu controlled by the this toggle.
* @param {string} openClass - The class to use when a submenu is open.
* @param {Menu} parentMenu - The menu containing the toggle.
* @param {MenuItem} parentMenuItem - The menu item containing the toggle.
* @param {Menu} rootMenu - The root menu containing the toggle.
* @param {object} param0 - The menu toggle object.
* @param {HTMLElement} param0.menuToggleElement - The toggle element in the DOM.
* @param {HTMLElement} param0.parentElement - The element containing the menu.
* @param {Menu} param0.menu - The menu controlled by the this toggle.
* @param {string} param0.openClass - The class to use when a submenu is open.
* @param {Menu} param0.parentMenu - The menu containing the toggle.
* @param {Menu} param0.rootMenu - The root menu containing the toggle.
*/
function MenuToggle(menuToggleElement, menu) {
var openClass = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : "show";
var parentMenu = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
var parentMenuItem = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : null;
var rootMenu = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : null;
function MenuToggle(_ref2) {
var menuToggleElement = _ref2.menuToggleElement,
parentElement = _ref2.parentElement,
menu = _ref2.menu,
_ref2$openClass = _ref2.openClass,
openClass = _ref2$openClass === void 0 ? "show" : _ref2$openClass,
_ref2$parentMenu = _ref2.parentMenu,
parentMenu = _ref2$parentMenu === void 0 ? null : _ref2$parentMenu,
_ref2$rootMenu = _ref2.rootMenu,
rootMenu = _ref2$rootMenu === void 0 ? null : _ref2$rootMenu;

@@ -187,12 +195,12 @@ _classCallCheck(this, MenuToggle);

validate$1.menuToggleElement(menuToggleElement);
validate$1.parentElement(parentElement);
validate$1.menu(menu);
validate$1.openClass(openClass);
validate$1.parentMenu(parentMenu);
validate$1.parentMenuItem(parentMenuItem);
validate$1.rootMenu(rootMenu);
this.domElements = {
toggle: menuToggleElement
toggle: menuToggleElement,
menuItem: parentElement
};
this.elements = {
menuItem: parentMenuItem,
menu: menu,

@@ -258,3 +266,3 @@ parentMenu: parentMenu,

this.element.setAttribute("aria-expanded", "true");
this.menuItem.element.classList.add(this.openClass);
this.menuItemElement.classList.add(this.openClass);
this.menu.element.classList.add(this.openClass); // Close all sibling menus.

@@ -264,3 +272,3 @@

this.parentMenu.currentFocus = "child";
if (this.parentMenu) this.parentMenu.currentFocus = "child";
this.menu.currentFocus = "self"; // Set the new focus.

@@ -283,3 +291,3 @@

this.element.setAttribute("aria-expanded", "false");
this.menuItem.element.classList.remove(this.openClass);
this.menuItemElement.classList.remove(this.openClass);
this.menu.element.classList.remove(this.openClass); // Close all child menus.

@@ -290,5 +298,5 @@

this.menu.currentFocus = "none";
this.parentMenu.currentFocus = "self"; // Set the new focus.
if (this.parentMenu) this.parentMenu.currentFocus = "self"; // Set the new focus.
this.parentMenu.focusCurrentChild();
if (this.parentMenu) this.parentMenu.focusCurrentChild();
}

@@ -363,19 +371,21 @@ }

_this3.close();
} else if (_this3.parentMenu.isTopLevel && key === "ArrowRight") {
// The Right Arrow key should focus the next menu item in the parent menu.
preventDefault(event);
} else if (_this3.parentMenu && _this3.parentMenu.isTopLevel) {
if (key === "ArrowRight") {
// The Right Arrow key should focus the next menu item in the parent menu.
preventDefault(event);
_this3.close();
_this3.close();
_this3.parentMenu.focusNextChild();
} else if (_this3.parentMenu.isTopLevel && key === "ArrowLeft") {
// The Left Arrow key should focus the next menu item in the parent menu.
preventDefault(event);
_this3.parentMenu.focusNextChild();
} else if (key === "ArrowLeft") {
// The Left Arrow key should focus the next menu item in the parent menu.
preventDefault(event);
_this3.close();
_this3.close();
_this3.parentMenu.focusPreviousChild();
_this3.parentMenu.focusPreviousChild();
}
}
});
this.menuItem.element.addEventListener("keydown", function (event) {
this.menuItemElement.addEventListener("keydown", function (event) {
var key = event.key;

@@ -412,5 +422,5 @@

}, {
key: "menuItem",
key: "menuItemElement",
get: function get() {
return this.elements.menuItem;
return this.domElements.menuItem;
}

@@ -528,2 +538,14 @@ /**

}
},
isDropdown: function isDropdown(controller, container) {
// Values are allowed to be null if both are null.
if (controller === null && container === null) return; // Ensure value is an HTML element.
if (!(controller instanceof HTMLElement)) {
throw new TypeError("controllerElement must be an HTML Element if containerElement is provided.");
}
if (!(container instanceof HTMLElement)) {
throw new TypeError("containerElement must be an HTML Element if controllerElement is provided.");
}
}

@@ -538,13 +560,27 @@ };

*
* @param {object} menuElement - The menu element in the DOM.
* @param {string} menuItemSelector - The selector string for menu items.
* @param {string} submenuItemSelector - The selector string for submenu items.
* @param {string} submenuToggleSelector - The selector string for submenu toggle triggers.
* @param {string} submenuSelector - The selector string for the submenu itself.
* @param {string} submenuOpenClass - The class to use when a submenu is open.
* @param {boolean} isTopLevel - Flags the menu as a top-level menu.
* @param {object} param0 - The menu object.
* @param {HTMLElement} param0.menuElement - The menu element in the DOM.
* @param {string} param0.menuItemSelector - The selector string for menu items.
* @param {string} param0.submenuItemSelector - The selector string for submenu items.
* @param {string} param0.submenuToggleSelector - The selector string for submenu toggle triggers.
* @param {string} param0.submenuSelector - The selector string for the submenu itself.
* @param {string} param0.submenuOpenClass - The class to use when a submenu is open.
* @param {boolean} param0.isTopLevel - Flags the menu as a top-level menu.
* @param {HTMLElement} param0.controllerElement - The element controlling the menu in the DOM.
* @param {HTMLElement} param0.containerElement - The element containing the menu in the DOM.
*/
function Menu(menuElement, menuItemSelector, submenuItemSelector, submenuToggleSelector, submenuSelector) {
var submenuOpenClass = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : "show";
var isTopLevel = arguments.length > 6 && arguments[6] !== undefined ? arguments[6] : true;
function Menu(_ref3) {
var menuElement = _ref3.menuElement,
menuItemSelector = _ref3.menuItemSelector,
submenuItemSelector = _ref3.submenuItemSelector,
submenuToggleSelector = _ref3.submenuToggleSelector,
submenuSelector = _ref3.submenuSelector,
_ref3$submenuOpenClas = _ref3.submenuOpenClass,
submenuOpenClass = _ref3$submenuOpenClas === void 0 ? "show" : _ref3$submenuOpenClas,
_ref3$isTopLevel = _ref3.isTopLevel,
isTopLevel = _ref3$isTopLevel === void 0 ? true : _ref3$isTopLevel,
_ref3$controllerEleme = _ref3.controllerElement,
controllerElement = _ref3$controllerEleme === void 0 ? null : _ref3$controllerEleme,
_ref3$containerElemen = _ref3.containerElement,
containerElement = _ref3$containerElemen === void 0 ? null : _ref3$containerElemen;

@@ -561,4 +597,7 @@ _classCallCheck(this, Menu);

validate$2.isTopLevel(isTopLevel);
validate$2.isDropdown(controllerElement, containerElement);
this.domElements = {
menu: menuElement,
controller: controllerElement,
container: containerElement,
menuItems: Array.from(menuElement.querySelectorAll(menuItemSelector)).filter(function (item) {

@@ -603,2 +642,13 @@ return item.parentElement === menuElement;

this.handleClick();
if (this.controllerElement && this.containerElement) {
// Create a new MenuToggle to control the menu.
var toggle = new MenuToggle({
menuToggleElement: this.controllerElement,
parentElement: this.containerElement,
menu: this,
openClass: this.openClass
});
toggle.initialize();
}
}

@@ -622,3 +672,6 @@ /**

// Create a new MenuItem.
var menuItem = new MenuItem(element, _this4); // Add the item to the list of menu items.
var menuItem = new MenuItem({
menuItemElement: element,
parentMenu: _this4
}); // Add the item to the list of menu items.

@@ -637,6 +690,20 @@ _this4.elements.menuItems.push(menuItem); // Initialize the menu item.

var menu = new Menu(submenu, _this4.selector["menu-items"], _this4.selector["submenu-items"], _this4.selector["submenu-toggle"], _this4.selector.submenu, _this4.openClass, false);
var menu = new Menu({
menuElement: submenu,
menuItemSelector: _this4.selector["menu-items"],
submenuItemSelector: _this4.selector["submenu-items"],
submenuToggleSelector: _this4.selector["submenu-toggle"],
submenuSelector: _this4.selector.submenu,
submenuOpenClass: _this4.openClass,
isTopLevel: false
});
menu.initialize(); // Create the new MenuToggle.
var toggle = new MenuToggle(toggler, menu, _this4.openClass, _this4, menuItem);
var toggle = new MenuToggle({
menuToggleElement: toggler,
parentElement: element,
menu: menu,
openClass: _this4.openClass,
parentMenu: _this4
});
toggle.initialize(); // Add it to the list of submenu items.

@@ -882,2 +949,24 @@

/**
* The menu's controller element in the DOM.
*
* @returns {HTMLElement} - The controller element.
*/
}, {
key: "controllerElement",
get: function get() {
return this.domElements.controller;
}
/**
* The menu's container element in the DOM.
*
* @returns {HTMLElement} - The container element.
*/
}, {
key: "containerElement",
get: function get() {
return this.domElements.container;
}
/**
* The menu item DOM elements contained in the menu.

@@ -884,0 +973,0 @@ *

@@ -1,1 +0,1 @@

"use strict";function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _defineProperties(e,t){for(var n=0;n<t.length;n++){var s=t[n];s.enumerable=s.enumerable||!1,s.configurable=!0,"value"in s&&(s.writable=!0),Object.defineProperty(e,s.key,s)}}function _createClass(e,t,n){return t&&_defineProperties(e.prototype,t),n&&_defineProperties(e,n),e}var AccessibleMenu=function(){var e={menuItemElement:function(e){if(!(e instanceof HTMLElement))throw new TypeError("menuItemElement must be an HTML Element.")},parentMenu:function(e){if(!(e instanceof i))throw new TypeError("parentMenu must be a Menu.")}},t=function(){function t(n,s){_classCallCheck(this,t),e.menuItemElement(n),e.parentMenu(s),this.domElements={menuItem:n,link:n.querySelector("a")},this.elements={parent:s}}return _createClass(t,[{key:"initialize",value:function(){this.element.setAttribute("role","menuitem"),this.link.tabIndex=-1}},{key:"focus",value:function(){this.element.querySelector("a").focus()}},{key:"element",get:function(){return this.domElements.menuItem}},{key:"link",get:function(){return this.domElements.link}},{key:"parentMenu",get:function(){return this.elements.parent}}]),t}(),n={menuToggleElement:function(e){if(!(e instanceof HTMLElement))throw new TypeError("menuToggleElement must be an HTML Element.")},menu:function(e){if(!(e instanceof i))throw new TypeError("menu must be a Menu.")},openClass:function(e){if("string"!=typeof e)throw TypeError("openClass must be a string.");if(e.replace(/[_a-zA-Z0-9-]/g,"").length>0)throw Error("openClass must be a valid CSS class.")},parentMenu:function(e){if(null!==e&&!(e instanceof i))throw new TypeError("parentMenu must be a Menu.")},parentMenuItem:function(e){if(null!==e&&!(e instanceof t))throw new TypeError("parentMenuItem must be a MenuItem.")},rootMenu:function(e){if(null!==e&&!(e instanceof i))throw new TypeError("rootMenu must be a Menu.")}},s=function(){function e(t,s){var u=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"show",i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null,r=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null;_classCallCheck(this,e),n.menuToggleElement(t),n.menu(s),n.openClass(u),n.parentMenu(i),n.parentMenuItem(o),n.rootMenu(r),this.domElements={toggle:t},this.elements={menuItem:o,menu:s,parentMenu:i,rootMenu:r||i},this.openClass=u}return _createClass(e,[{key:"initialize",value:function(){var e=this;if(this.element.setAttribute("aria-haspopup","true"),this.element.setAttribute("aria-expanded","false"),this.element.setAttribute("role","button"),""===this.element.id||""===this.menu.element.id){var t=Math.random().toString(36).replace(/[^a-z]+/g,"").substr(0,10),n="".concat(this.element.innerText.toLowerCase().replace(/[^a-zA-Z0-9\s]/g,"").replace(/\s/g,"-"),"-").concat(t);this.element.id=this.element.id||"".concat(n,"-menu-button"),this.menu.element.id=this.menu.element.id||"".concat(n,"-menu")}this.menu.element.setAttribute("aria-labelledby",this.element.id),this.element.setAttribute("aria-controls",this.menu.element.id),this.element.addEventListener("click",(function(t){t.preventDefault(),t.stopPropagation(),e.toggle()})),this.handleKeydown()}},{key:"open",value:function(){this.isOpen||(this.isOpen=!0,this.element.setAttribute("aria-expanded","true"),this.menuItem.element.classList.add(this.openClass),this.menu.element.classList.add(this.openClass),this.closeSiblings(),this.parentMenu.currentFocus="child",this.menu.currentFocus="self",this.menu.focusFirstChild())}},{key:"close",value:function(){this.isOpen&&(this.isOpen=!1,this.element.setAttribute("aria-expanded","false"),this.menuItem.element.classList.remove(this.openClass),this.menu.element.classList.remove(this.openClass),this.closeChildren(),this.menu.currentFocus="none",this.parentMenu.currentFocus="self",this.parentMenu.focusCurrentChild())}},{key:"toggle",value:function(){this.isOpen?this.close():this.open()}},{key:"closeSiblings",value:function(){var e=this;try{this.parentMenu.menuToggles.forEach((function(t){t!==e&&t.close()}))}catch(e){}}},{key:"closeChildren",value:function(){this.menu.menuToggles.forEach((function(e){return e.close()}))}},{key:"handleKeydown",value:function(){var e=this;function t(e){e.preventDefault(),e.stopPropagation()}this.menu.element.addEventListener("keydown",(function(n){var s=n.key;"Escape"===s?(t(n),e.close()):e.parentMenu.isTopLevel&&"ArrowRight"===s?(t(n),e.close(),e.parentMenu.focusNextChild()):e.parentMenu.isTopLevel&&"ArrowLeft"===s&&(t(n),e.close(),e.parentMenu.focusPreviousChild())})),this.menuItem.element.addEventListener("keydown",(function(n){var s=n.key;"none"===e.menu.currentFocus&&e.parentMenu.isTopLevel&&("ArrowUp"===s?(t(n),e.open(),e.menu.focusLastChild()):"ArrowDown"===s&&(t(n),e.open()))}))}},{key:"element",get:function(){return this.domElements.toggle}},{key:"menuItem",get:function(){return this.elements.menuItem}},{key:"menu",get:function(){return this.elements.menu}},{key:"parentMenu",get:function(){return this.elements.parentMenu}},{key:"rootMenu",get:function(){return this.elements.rootMenu}},{key:"isOpen",get:function(){return this.show},set:function(e){if("boolean"!=typeof e)throw new TypeError("Open state must be true or false.");this.show=e}}]),e}(),u={menuElement:function(e){if(!(e instanceof HTMLElement))throw new TypeError("menuElement must be an HTML Element.")},menuItemSelector:function(e){if("string"!=typeof e)throw new TypeError("menuItemSelector must be a CSS selector string.")},submenuItemSelector:function(e){if("string"!=typeof e)throw new TypeError("submenuItemSelector must be a CSS selector string.")},submenuToggleSelector:function(e){if("string"!=typeof e)throw new TypeError("submenuToggleSelector must be a CSS selector string.")},submenuSelector:function(e){if("string"!=typeof e)throw new TypeError("submenuSelector must be a CSS selector string.")},submenuOpenClass:function(e){if("string"!=typeof e)throw TypeError("submenuOpenClass must be a string.");if(e.replace(/[_a-zA-Z0-9-]/g,"").length>0)throw Error("submenuOpenClass must be a valid CSS class.")},isTopLevel:function(e){if("boolean"!=typeof e)throw new TypeError("isTopLevel must be true or false")}},i=function(){function e(t,n,s,i,o){var r=arguments.length>5&&void 0!==arguments[5]?arguments[5]:"show",l=!(arguments.length>6&&void 0!==arguments[6])||arguments[6];_classCallCheck(this,e),u.menuElement(t),u.menuItemSelector(n),u.submenuItemSelector(s),u.submenuToggleSelector(i),u.submenuSelector(o),u.submenuOpenClass(r),u.isTopLevel(l),this.domElements={menu:t,menuItems:Array.from(t.querySelectorAll(n)).filter((function(e){return e.parentElement===t})),submenuItems:Array.from(t.querySelectorAll(s)).filter((function(e){return e.parentElement===t}))},this.domSelectors={"menu-items":n,"submenu-items":s,"submenu-toggle":i,submenu:o},this.elements={menuItems:[],menuToggles:[]},this.focussedChild=-1,this.focusState="none",this.openClass=r,this.root=l}return _createClass(e,[{key:"initialize",value:function(){this.element.setAttribute("role","menu"),this.element.tabIndex=0,this.createMenuItems(),this.handleKeydown(),this.handleClick()}},{key:"createMenuItems",value:function(){var n=this;this.menuItemElements.forEach((function(u){var i=new t(u,n);if(n.elements.menuItems.push(i),i.initialize(),n.submenuItemElements.includes(u)){var o=u.querySelector(n.selector["submenu-toggle"]),r=new e(u.querySelector(n.selector.submenu),n.selector["menu-items"],n.selector["submenu-items"],n.selector["submenu-toggle"],n.selector.submenu,n.openClass,!1);r.initialize();var l=new s(o,r,n.openClass,n,i);l.initialize(),n.elements.menuToggles.push(l)}}))}},{key:"handleKeydown",value:function(){var e=this;function t(e){e.preventDefault(),e.stopPropagation()}this.element.addEventListener("keydown",(function(n){var s=n.key,u=n.code,i=n.altKey,o=n.crtlKey,r=n.metaKey,l=i||o||r;"none"===e.currentFocus?("Enter"===s||" "===s&&"Space"===u)&&(t(n),e.currentFocus="self",e.focusFirstChild()):"self"===e.currentFocus&&("Escape"===s?(t(n),e.focus(),e.currentFocus="none"):e.isTopLevel||"ArrowUp"!==s?e.isTopLevel&&"ArrowRight"===s?(t(n),e.focusNextChild()):e.isTopLevel||"ArrowDown"!==s?e.isTopLevel&&"ArrowLeft"===s?(t(n),e.focusPreviousChild()):"Home"===s?(t(n),e.focusFirstChild()):"End"===s?(t(n),e.focusLastChild()):s.match(/^[a-zA-Z]{1}$/)&&!l&&(t(n),e.focusNextChildWithCharacter(s)):(t(n),e.focusNextChild()):(t(n),e.focusPreviousChild())),"none"!==e.currentFocus&&"Tab"===s&&(e.blur(),e.closeChildren())}))}},{key:"handleClick",value:function(){var e=this;document.addEventListener("click",(function(t){e.element.contains(t.target)||e.element===t.target||(e.blur(),e.closeChildren())}))}},{key:"focus",value:function(){this.focussedChild=0,this.currentFocus="self",this.element.focus()}},{key:"blur",value:function(){this.focussedChild=-1,this.currentFocus="none",this.element.blur()}},{key:"focusFirstChild",value:function(){this.focussedChild=0,this.focusCurrentChild()}},{key:"focusLastChild",value:function(){this.focussedChild=this.menuItems.length-1,this.focusCurrentChild()}},{key:"focusNextChild",value:function(){this.focussedChild===this.menuItems.length-1?this.focusFirstChild():(this.focussedChild=this.focussedChild+1,this.focusCurrentChild())}},{key:"focusPreviousChild",value:function(){0===this.focussedChild?this.focusLastChild():(this.focussedChild=this.focussedChild-1,this.focusCurrentChild())}},{key:"focusCurrentChild",value:function(){-1!==this.focussedChild&&this.menuItems[this.focussedChild].focus()}},{key:"focusNextChildWithCharacter",value:function(e){for(var t=e.toLowerCase(),n=this.focussedChild+1,s=!1;!s&&n<this.menuItems.length;){this.menuItems[n].element.innerText.toLowerCase().startsWith(t)&&(s=!0,this.focussedChild=n,this.focusCurrentChild()),n++}}},{key:"closeChildren",value:function(){this.menuToggles.forEach((function(e){return e.close()}))}},{key:"element",get:function(){return this.domElements.menu}},{key:"menuItemElements",get:function(){return this.domElements.menuItems}},{key:"submenuItemElements",get:function(){return this.domElements.submenuItems}},{key:"menuItems",get:function(){return this.elements.menuItems}},{key:"menuToggles",get:function(){return this.elements.menuToggles}},{key:"selector",get:function(){return this.domSelectors}},{key:"currentFocus",get:function(){return this.focusState},set:function(e){if(!["self","child","none"].includes(e))throw new Error("Focus state must be 'self', 'child', or 'none'.");this.focusState=e}},{key:"openClass",get:function(){return this.submenuOpenClass},set:function(e){if("string"!=typeof e)throw new TypeError("Class must be a string.");this.submenuOpenClass=e}},{key:"isTopLevel",get:function(){return this.root},set:function(e){if("boolean"!=typeof e)throw new TypeError("Top-level flag must be true or false.");this.root=e}}]),e}();return i}();
"use strict";function _classCallCheck(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function _defineProperties(e,t){for(var n=0;n<t.length;n++){var s=t[n];s.enumerable=s.enumerable||!1,s.configurable=!0,"value"in s&&(s.writable=!0),Object.defineProperty(e,s.key,s)}}function _createClass(e,t,n){return t&&_defineProperties(e.prototype,t),n&&_defineProperties(e,n),e}var AccessibleMenu=function(){var e={menuItemElement:function(e){if(!(e instanceof HTMLElement))throw new TypeError("menuItemElement must be an HTML Element.")},parentMenu:function(e){if(!(e instanceof u))throw new TypeError("parentMenu must be a Menu.")}},t=function(){function t(n){var s=n.menuItemElement,o=n.parentMenu;_classCallCheck(this,t),e.menuItemElement(s),e.parentMenu(o),this.domElements={menuItem:s,link:s.querySelector("a")},this.elements={parent:o}}return _createClass(t,[{key:"initialize",value:function(){this.element.setAttribute("role","menuitem"),this.link.tabIndex=-1}},{key:"focus",value:function(){this.element.querySelector("a").focus()}},{key:"element",get:function(){return this.domElements.menuItem}},{key:"link",get:function(){return this.domElements.link}},{key:"parentMenu",get:function(){return this.elements.parent}}]),t}(),n={menuToggleElement:function(e){if(!(e instanceof HTMLElement))throw new TypeError("menuToggleElement must be an HTML Element.")},parentElement:function(e){if(!(e instanceof HTMLElement))throw new TypeError("parentElement must be an HTML Element.")},menu:function(e){if(!(e instanceof u))throw new TypeError("menu must be a Menu.")},openClass:function(e){if("string"!=typeof e)throw TypeError("openClass must be a string.");if(e.replace(/[_a-zA-Z0-9-]/g,"").length>0)throw Error("openClass must be a valid CSS class.")},parentMenu:function(e){if(null!==e&&!(e instanceof u))throw new TypeError("parentMenu must be a Menu.")},rootMenu:function(e){if(null!==e&&!(e instanceof u))throw new TypeError("rootMenu must be a Menu.")}},s=function(){function e(t){var s=t.menuToggleElement,o=t.parentElement,u=t.menu,i=t.openClass,r=void 0===i?"show":i,l=t.parentMenu,m=void 0===l?null:l,c=t.rootMenu,a=void 0===c?null:c;_classCallCheck(this,e),n.menuToggleElement(s),n.parentElement(o),n.menu(u),n.openClass(r),n.parentMenu(m),n.rootMenu(a),this.domElements={toggle:s,menuItem:o},this.elements={menu:u,parentMenu:m,rootMenu:a||m},this.openClass=r}return _createClass(e,[{key:"initialize",value:function(){var e=this;if(this.element.setAttribute("aria-haspopup","true"),this.element.setAttribute("aria-expanded","false"),this.element.setAttribute("role","button"),""===this.element.id||""===this.menu.element.id){var t=Math.random().toString(36).replace(/[^a-z]+/g,"").substr(0,10),n="".concat(this.element.innerText.toLowerCase().replace(/[^a-zA-Z0-9\s]/g,"").replace(/\s/g,"-"),"-").concat(t);this.element.id=this.element.id||"".concat(n,"-menu-button"),this.menu.element.id=this.menu.element.id||"".concat(n,"-menu")}this.menu.element.setAttribute("aria-labelledby",this.element.id),this.element.setAttribute("aria-controls",this.menu.element.id),this.element.addEventListener("click",(function(t){t.preventDefault(),t.stopPropagation(),e.toggle()})),this.handleKeydown()}},{key:"open",value:function(){this.isOpen||(this.isOpen=!0,this.element.setAttribute("aria-expanded","true"),this.menuItemElement.classList.add(this.openClass),this.menu.element.classList.add(this.openClass),this.closeSiblings(),this.parentMenu&&(this.parentMenu.currentFocus="child"),this.menu.currentFocus="self",this.menu.focusFirstChild())}},{key:"close",value:function(){this.isOpen&&(this.isOpen=!1,this.element.setAttribute("aria-expanded","false"),this.menuItemElement.classList.remove(this.openClass),this.menu.element.classList.remove(this.openClass),this.closeChildren(),this.menu.currentFocus="none",this.parentMenu&&(this.parentMenu.currentFocus="self"),this.parentMenu&&this.parentMenu.focusCurrentChild())}},{key:"toggle",value:function(){this.isOpen?this.close():this.open()}},{key:"closeSiblings",value:function(){var e=this;try{this.parentMenu.menuToggles.forEach((function(t){t!==e&&t.close()}))}catch(e){}}},{key:"closeChildren",value:function(){this.menu.menuToggles.forEach((function(e){return e.close()}))}},{key:"handleKeydown",value:function(){var e=this;function t(e){e.preventDefault(),e.stopPropagation()}this.menu.element.addEventListener("keydown",(function(n){var s=n.key;"Escape"===s?(t(n),e.close()):e.parentMenu&&e.parentMenu.isTopLevel&&("ArrowRight"===s?(t(n),e.close(),e.parentMenu.focusNextChild()):"ArrowLeft"===s&&(t(n),e.close(),e.parentMenu.focusPreviousChild()))})),this.menuItemElement.addEventListener("keydown",(function(n){var s=n.key;"none"===e.menu.currentFocus&&e.parentMenu.isTopLevel&&("ArrowUp"===s?(t(n),e.open(),e.menu.focusLastChild()):"ArrowDown"===s&&(t(n),e.open()))}))}},{key:"element",get:function(){return this.domElements.toggle}},{key:"menuItemElement",get:function(){return this.domElements.menuItem}},{key:"menu",get:function(){return this.elements.menu}},{key:"parentMenu",get:function(){return this.elements.parentMenu}},{key:"rootMenu",get:function(){return this.elements.rootMenu}},{key:"isOpen",get:function(){return this.show},set:function(e){if("boolean"!=typeof e)throw new TypeError("Open state must be true or false.");this.show=e}}]),e}(),o={menuElement:function(e){if(!(e instanceof HTMLElement))throw new TypeError("menuElement must be an HTML Element.")},menuItemSelector:function(e){if("string"!=typeof e)throw new TypeError("menuItemSelector must be a CSS selector string.")},submenuItemSelector:function(e){if("string"!=typeof e)throw new TypeError("submenuItemSelector must be a CSS selector string.")},submenuToggleSelector:function(e){if("string"!=typeof e)throw new TypeError("submenuToggleSelector must be a CSS selector string.")},submenuSelector:function(e){if("string"!=typeof e)throw new TypeError("submenuSelector must be a CSS selector string.")},submenuOpenClass:function(e){if("string"!=typeof e)throw TypeError("submenuOpenClass must be a string.");if(e.replace(/[_a-zA-Z0-9-]/g,"").length>0)throw Error("submenuOpenClass must be a valid CSS class.")},isTopLevel:function(e){if("boolean"!=typeof e)throw new TypeError("isTopLevel must be true or false")},isDropdown:function(e,t){if(null!==e||null!==t){if(!(e instanceof HTMLElement))throw new TypeError("controllerElement must be an HTML Element if containerElement is provided.");if(!(t instanceof HTMLElement))throw new TypeError("containerElement must be an HTML Element if controllerElement is provided.")}}},u=function(){function e(t){var n=t.menuElement,s=t.menuItemSelector,u=t.submenuItemSelector,i=t.submenuToggleSelector,r=t.submenuSelector,l=t.submenuOpenClass,m=void 0===l?"show":l,c=t.isTopLevel,a=void 0===c||c,h=t.controllerElement,f=void 0===h?null:h,p=t.containerElement,d=void 0===p?null:p;_classCallCheck(this,e),o.menuElement(n),o.menuItemSelector(s),o.submenuItemSelector(u),o.submenuToggleSelector(i),o.submenuSelector(r),o.submenuOpenClass(m),o.isTopLevel(a),o.isDropdown(f,d),this.domElements={menu:n,controller:f,container:d,menuItems:Array.from(n.querySelectorAll(s)).filter((function(e){return e.parentElement===n})),submenuItems:Array.from(n.querySelectorAll(u)).filter((function(e){return e.parentElement===n}))},this.domSelectors={"menu-items":s,"submenu-items":u,"submenu-toggle":i,submenu:r},this.elements={menuItems:[],menuToggles:[]},this.focussedChild=-1,this.focusState="none",this.openClass=m,this.root=a}return _createClass(e,[{key:"initialize",value:function(){(this.element.setAttribute("role","menu"),this.element.tabIndex=0,this.createMenuItems(),this.handleKeydown(),this.handleClick(),this.controllerElement&&this.containerElement)&&new s({menuToggleElement:this.controllerElement,parentElement:this.containerElement,menu:this,openClass:this.openClass}).initialize()}},{key:"createMenuItems",value:function(){var n=this;this.menuItemElements.forEach((function(o){var u=new t({menuItemElement:o,parentMenu:n});if(n.elements.menuItems.push(u),u.initialize(),n.submenuItemElements.includes(o)){var i=o.querySelector(n.selector["submenu-toggle"]),r=new e({menuElement:o.querySelector(n.selector.submenu),menuItemSelector:n.selector["menu-items"],submenuItemSelector:n.selector["submenu-items"],submenuToggleSelector:n.selector["submenu-toggle"],submenuSelector:n.selector.submenu,submenuOpenClass:n.openClass,isTopLevel:!1});r.initialize();var l=new s({menuToggleElement:i,parentElement:o,menu:r,openClass:n.openClass,parentMenu:n});l.initialize(),n.elements.menuToggles.push(l)}}))}},{key:"handleKeydown",value:function(){var e=this;function t(e){e.preventDefault(),e.stopPropagation()}this.element.addEventListener("keydown",(function(n){var s=n.key,o=n.code,u=n.altKey,i=n.crtlKey,r=n.metaKey,l=u||i||r;"none"===e.currentFocus?("Enter"===s||" "===s&&"Space"===o)&&(t(n),e.currentFocus="self",e.focusFirstChild()):"self"===e.currentFocus&&("Escape"===s?(t(n),e.focus(),e.currentFocus="none"):e.isTopLevel||"ArrowUp"!==s?e.isTopLevel&&"ArrowRight"===s?(t(n),e.focusNextChild()):e.isTopLevel||"ArrowDown"!==s?e.isTopLevel&&"ArrowLeft"===s?(t(n),e.focusPreviousChild()):"Home"===s?(t(n),e.focusFirstChild()):"End"===s?(t(n),e.focusLastChild()):s.match(/^[a-zA-Z]{1}$/)&&!l&&(t(n),e.focusNextChildWithCharacter(s)):(t(n),e.focusNextChild()):(t(n),e.focusPreviousChild())),"none"!==e.currentFocus&&"Tab"===s&&(e.blur(),e.closeChildren())}))}},{key:"handleClick",value:function(){var e=this;document.addEventListener("click",(function(t){e.element.contains(t.target)||e.element===t.target||(e.blur(),e.closeChildren())}))}},{key:"focus",value:function(){this.focussedChild=0,this.currentFocus="self",this.element.focus()}},{key:"blur",value:function(){this.focussedChild=-1,this.currentFocus="none",this.element.blur()}},{key:"focusFirstChild",value:function(){this.focussedChild=0,this.focusCurrentChild()}},{key:"focusLastChild",value:function(){this.focussedChild=this.menuItems.length-1,this.focusCurrentChild()}},{key:"focusNextChild",value:function(){this.focussedChild===this.menuItems.length-1?this.focusFirstChild():(this.focussedChild=this.focussedChild+1,this.focusCurrentChild())}},{key:"focusPreviousChild",value:function(){0===this.focussedChild?this.focusLastChild():(this.focussedChild=this.focussedChild-1,this.focusCurrentChild())}},{key:"focusCurrentChild",value:function(){-1!==this.focussedChild&&this.menuItems[this.focussedChild].focus()}},{key:"focusNextChildWithCharacter",value:function(e){for(var t=e.toLowerCase(),n=this.focussedChild+1,s=!1;!s&&n<this.menuItems.length;){this.menuItems[n].element.innerText.toLowerCase().startsWith(t)&&(s=!0,this.focussedChild=n,this.focusCurrentChild()),n++}}},{key:"closeChildren",value:function(){this.menuToggles.forEach((function(e){return e.close()}))}},{key:"element",get:function(){return this.domElements.menu}},{key:"controllerElement",get:function(){return this.domElements.controller}},{key:"containerElement",get:function(){return this.domElements.container}},{key:"menuItemElements",get:function(){return this.domElements.menuItems}},{key:"submenuItemElements",get:function(){return this.domElements.submenuItems}},{key:"menuItems",get:function(){return this.elements.menuItems}},{key:"menuToggles",get:function(){return this.elements.menuToggles}},{key:"selector",get:function(){return this.domSelectors}},{key:"currentFocus",get:function(){return this.focusState},set:function(e){if(!["self","child","none"].includes(e))throw new Error("Focus state must be 'self', 'child', or 'none'.");this.focusState=e}},{key:"openClass",get:function(){return this.submenuOpenClass},set:function(e){if("string"!=typeof e)throw new TypeError("Class must be a string.");this.submenuOpenClass=e}},{key:"isTopLevel",get:function(){return this.root},set:function(e){if("boolean"!=typeof e)throw new TypeError("Top-level flag must be true or false.");this.root=e}}]),e}();return u}();
{
"name": "accessible-menu",
"version": "1.0.0-beta.3",
"version": "1.0.0-beta.4",
"description": "A JavaScript library to help you generate WAI-ARIA accessible menus in the DOM.",

@@ -5,0 +5,0 @@ "main": "src/menu.js",

@@ -67,10 +67,10 @@ # accessible-menu

```jsx
const menu = new AccessibleMenu(
menuDOMObject,
"menu-item-css-selector",
"menu-item-with-dropdown-css-selector",
"dropdown-toggle-css-selector",
"dropdown-menu-css-selector",
"class-to-open-menus"
);
const menu = new AccessibleMenu({
menuElement: menuDOMObject,
menuItemSelector: "menu-item-css-selector",
submenuItemSelector: "menu-item-with-dropdown-css-selector",
submenuToggleSelector: "dropdown-toggle-css-selector",
submenuSelector: "dropdown-menu-css-selector",
submenuOpenClass: "class-to-open-menus"
});

@@ -77,0 +77,0 @@ menu.initialize();

@@ -54,2 +54,19 @@ import MenuItem from "./menuItem";

}
},
isDropdown: (controller, container) => {
// Values are allowed to be null if both are null.
if (controller === null && container === null) return;
// Ensure value is an HTML element.
if (!(controller instanceof HTMLElement)) {
throw new TypeError(
"controllerElement must be an HTML Element if containerElement is provided."
);
}
if (!(container instanceof HTMLElement)) {
throw new TypeError(
"containerElement must be an HTML Element if controllerElement is provided."
);
}
}

@@ -62,11 +79,14 @@ };

*
* @param {object} menuElement - The menu element in the DOM.
* @param {string} menuItemSelector - The selector string for menu items.
* @param {string} submenuItemSelector - The selector string for submenu items.
* @param {string} submenuToggleSelector - The selector string for submenu toggle triggers.
* @param {string} submenuSelector - The selector string for the submenu itself.
* @param {string} submenuOpenClass - The class to use when a submenu is open.
* @param {boolean} isTopLevel - Flags the menu as a top-level menu.
* @param {object} param0 - The menu object.
* @param {HTMLElement} param0.menuElement - The menu element in the DOM.
* @param {string} param0.menuItemSelector - The selector string for menu items.
* @param {string} param0.submenuItemSelector - The selector string for submenu items.
* @param {string} param0.submenuToggleSelector - The selector string for submenu toggle triggers.
* @param {string} param0.submenuSelector - The selector string for the submenu itself.
* @param {string} param0.submenuOpenClass - The class to use when a submenu is open.
* @param {boolean} param0.isTopLevel - Flags the menu as a top-level menu.
* @param {HTMLElement} param0.controllerElement - The element controlling the menu in the DOM.
* @param {HTMLElement} param0.containerElement - The element containing the menu in the DOM.
*/
constructor(
constructor({
menuElement,

@@ -78,4 +98,6 @@ menuItemSelector,

submenuOpenClass = "show",
isTopLevel = true
) {
isTopLevel = true,
controllerElement = null,
containerElement = null
}) {
// Run validations.

@@ -89,5 +111,8 @@ validate.menuElement(menuElement);

validate.isTopLevel(isTopLevel);
validate.isDropdown(controllerElement, containerElement);
this.domElements = {
menu: menuElement,
controller: controllerElement,
container: containerElement,
menuItems: Array.from(

@@ -130,2 +155,13 @@ menuElement.querySelectorAll(menuItemSelector)

this.handleClick();
if (this.controllerElement && this.containerElement) {
// Create a new MenuToggle to control the menu.
const toggle = new MenuToggle({
menuToggleElement: this.controllerElement,
parentElement: this.containerElement,
menu: this,
openClass: this.openClass
});
toggle.initialize();
}
}

@@ -143,2 +179,20 @@

/**
* The menu's controller element in the DOM.
*
* @returns {HTMLElement} - The controller element.
*/
get controllerElement() {
return this.domElements.controller;
}
/**
* The menu's container element in the DOM.
*
* @returns {HTMLElement} - The container element.
*/
get containerElement() {
return this.domElements.container;
}
/**
* The menu item DOM elements contained in the menu.

@@ -257,3 +311,6 @@ *

// Create a new MenuItem.
const menuItem = new MenuItem(element, this);
const menuItem = new MenuItem({
menuItemElement: element,
parentMenu: this
});

@@ -275,21 +332,21 @@ // Add the item to the list of menu items.

// Create the new Menu and initialize it.
const menu = new Menu(
submenu,
this.selector["menu-items"],
this.selector["submenu-items"],
this.selector["submenu-toggle"],
this.selector.submenu,
this.openClass,
false
);
const menu = new Menu({
menuElement: submenu,
menuItemSelector: this.selector["menu-items"],
submenuItemSelector: this.selector["submenu-items"],
submenuToggleSelector: this.selector["submenu-toggle"],
submenuSelector: this.selector.submenu,
submenuOpenClass: this.openClass,
isTopLevel: false
});
menu.initialize();
// Create the new MenuToggle.
const toggle = new MenuToggle(
toggler,
menu,
this.openClass,
this,
menuItem
);
const toggle = new MenuToggle({
menuToggleElement: toggler,
parentElement: element,
menu: menu,
openClass: this.openClass,
parentMenu: this
});
toggle.initialize();

@@ -296,0 +353,0 @@

@@ -22,6 +22,7 @@ import Menu from "./menu";

*
* @param {object} menuItemElement - The menu item in the DOM.
* @param {Menu} parentMenu - The parent menu.
* @param {object} param0 - The menu item object.
* @param {object} param0.menuItemElement - The menu item in the DOM.
* @param {Menu} param0.parentMenu - The parent menu.
*/
constructor(menuItemElement, parentMenu) {
constructor({ menuItemElement, parentMenu }) {
// Run validations.

@@ -28,0 +29,0 @@ validate.menuItemElement(menuItemElement);

@@ -11,2 +11,8 @@ import Menu from "./menu";

},
parentElement: value => {
// Ensure value is an HTML element.
if (!(value instanceof HTMLElement)) {
throw new TypeError("parentElement must be an HTML Element.");
}
},
menu: value => {

@@ -39,10 +45,2 @@ // Ensure value is an Menu element.

},
parentMenuItem: value => {
// Value is allowed to be null.
if (value === null) return;
if (!(value instanceof MenuItem)) {
throw new TypeError("parentMenuItem must be a MenuItem.");
}
},
rootMenu: value => {

@@ -63,30 +61,31 @@ // Value is allowed to be null.

*
* @param {object} menuToggleElement - The toggle element in the DOM.
* @param {Menu} menu - The menu controlled by the this toggle.
* @param {string} openClass - The class to use when a submenu is open.
* @param {Menu} parentMenu - The menu containing the toggle.
* @param {MenuItem} parentMenuItem - The menu item containing the toggle.
* @param {Menu} rootMenu - The root menu containing the toggle.
* @param {object} param0 - The menu toggle object.
* @param {HTMLElement} param0.menuToggleElement - The toggle element in the DOM.
* @param {HTMLElement} param0.parentElement - The element containing the menu.
* @param {Menu} param0.menu - The menu controlled by the this toggle.
* @param {string} param0.openClass - The class to use when a submenu is open.
* @param {Menu} param0.parentMenu - The menu containing the toggle.
* @param {Menu} param0.rootMenu - The root menu containing the toggle.
*/
constructor(
constructor({
menuToggleElement,
parentElement,
menu,
openClass = "show",
parentMenu = null,
parentMenuItem = null,
rootMenu = null
) {
}) {
// Run validations.
validate.menuToggleElement(menuToggleElement);
validate.parentElement(parentElement);
validate.menu(menu);
validate.openClass(openClass);
validate.parentMenu(parentMenu);
validate.parentMenuItem(parentMenuItem);
validate.rootMenu(rootMenu);
this.domElements = {
toggle: menuToggleElement
toggle: menuToggleElement,
menuItem: parentElement
};
this.elements = {
menuItem: parentMenuItem,
menu: menu,

@@ -155,4 +154,4 @@ parentMenu: parentMenu,

*/
get menuItem() {
return this.elements.menuItem;
get menuItemElement() {
return this.domElements.menuItem;
}

@@ -219,3 +218,3 @@

this.element.setAttribute("aria-expanded", "true");
this.menuItem.element.classList.add(this.openClass);
this.menuItemElement.classList.add(this.openClass);
this.menu.element.classList.add(this.openClass);

@@ -227,3 +226,3 @@

// Set proper focus states to parent & child.
this.parentMenu.currentFocus = "child";
if (this.parentMenu) this.parentMenu.currentFocus = "child";
this.menu.currentFocus = "self";

@@ -246,3 +245,3 @@

this.element.setAttribute("aria-expanded", "false");
this.menuItem.element.classList.remove(this.openClass);
this.menuItemElement.classList.remove(this.openClass);
this.menu.element.classList.remove(this.openClass);

@@ -255,6 +254,6 @@

this.menu.currentFocus = "none";
this.parentMenu.currentFocus = "self";
if (this.parentMenu) this.parentMenu.currentFocus = "self";
// Set the new focus.
this.parentMenu.focusCurrentChild();
if (this.parentMenu) this.parentMenu.focusCurrentChild();
}

@@ -314,15 +313,17 @@ }

this.close();
} else if (this.parentMenu.isTopLevel && key === "ArrowRight") {
// The Right Arrow key should focus the next menu item in the parent menu.
preventDefault(event);
this.close();
this.parentMenu.focusNextChild();
} else if (this.parentMenu.isTopLevel && key === "ArrowLeft") {
// The Left Arrow key should focus the next menu item in the parent menu.
preventDefault(event);
this.close();
this.parentMenu.focusPreviousChild();
} else if (this.parentMenu && this.parentMenu.isTopLevel) {
if (key === "ArrowRight") {
// The Right Arrow key should focus the next menu item in the parent menu.
preventDefault(event);
this.close();
this.parentMenu.focusNextChild();
} else if (key === "ArrowLeft") {
// The Left Arrow key should focus the next menu item in the parent menu.
preventDefault(event);
this.close();
this.parentMenu.focusPreviousChild();
}
}
});
this.menuItem.element.addEventListener("keydown", event => {
this.menuItemElement.addEventListener("keydown", event => {
const { key } = event;

@@ -329,0 +330,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc