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.1 to 1.0.0-beta.2

.babelrc.js

38

.github/CONTRIBUTING.md

@@ -48,13 +48,43 @@ # Contributing to accessible-menu

- Use the standards provided by the lint file. Run `npm run lint` to be sure.
- Keep all functional code in the `src/` directory.
- Keep all functional code inside of the `src/` directory
- Use the coding standards provided.
This project follows a set of coding standards combining [StandardJS](https://standardjs.com/), [Prettier](https://prettier.io/), and [JSDoc](https://jsdoc.app/).
To check your code, you can use [ESLint](https://eslint.org/) with the provided script:
```
npm run lint
```
You can also fix some violations automatically using:
```
npm run fix
```
Code that does not follow the linting standards _will not_ be merged.
## Commit Guidelines
We use an [Angular style](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits) for commit messages. Please make sure all commits follow this.
This project uses the conventional commit standard, which means your commits should follow a basic template of:
If you're unsure of how to format your messages, running `npm run commit` will give you a hand!
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
For more detailed information about available types, scopes, breaking changes, etc. please see the [official documentation](https://www.conventionalcommits.org/en/v1.0.0/).
This project also provides a command to assist you in formatting commit messages using [commitizen](https://commitizen.github.io/cz-cli/):
```
npm run commit
```
Commits that do not follow this format _will not_ be merged.
## References
This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md)

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

## [1.0.0-beta.2](https://github.com/NickDJM/accessible-menu/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2019-11-20)
### Features
* **menu:** add aria-controls to menu toggles ([ed3cfd2](https://github.com/NickDJM/accessible-menu/commit/ed3cfd2f1737358086f3c9993a714c06f82e5e18)), closes [#9](https://github.com/NickDJM/accessible-menu/issues/9)
* **menu:** add aria-label/labelledby to submenus ([6f6f2ac](https://github.com/NickDJM/accessible-menu/commit/6f6f2ac9049329628f30f6062c2a8344e6003e86)), closes [#10](https://github.com/NickDJM/accessible-menu/issues/10)
* **menu:** add basic type error handling ([b705d47](https://github.com/NickDJM/accessible-menu/commit/b705d474776307e856ee8d6cc46d5f42fda9aff2)), closes [#26](https://github.com/NickDJM/accessible-menu/issues/26)
* **menu:** compile menu through babel ([46d4efd](https://github.com/NickDJM/accessible-menu/commit/46d4efd3e86d5edb4c50cd18bba110ebcf836972))
### Build System
* **npm:** add babel ([4910dcc](https://github.com/NickDJM/accessible-menu/commit/4910dcc2c4a3e28b17067eab3dfdd4c2ae1697cb))
### Documentation
* **contributing:** update contributing guidelines with more information ([8b7563b](https://github.com/NickDJM/accessible-menu/commit/8b7563bcd5b3f3b3216e7b9a9aff23b0f1083889))
* **development:** add specific release documentation ([84f908c](https://github.com/NickDJM/accessible-menu/commit/84f908c9efe761911912017c76a0d59a7bc6841c))
* **menu:** add usage documentation ([5552e68](https://github.com/NickDJM/accessible-menu/commit/5552e689d2e86e4f075b6fc9dde52c4beccc16e6)), closes [#25](https://github.com/NickDJM/accessible-menu/issues/25)
## [1.0.0-beta.1](https://github.com/NickDJM/accessible-menu/compare/v1.0.0-beta.0...v1.0.0-beta.1) (2019-11-18)

@@ -7,0 +29,0 @@

1313

dist/accessibleMenu.js

@@ -1,5 +0,30 @@

var AccessibleMenu = (function () {
"use strict";
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
var AccessibleMenu = function () {
'use strict';
class MenuItem {
var validate = {
menuItemElement: function menuItemElement(value) {
// Ensure value is an HTML element.
if (!(value instanceof HTMLElement)) {
throw new TypeError("menuItemElement must be an HTML Element.");
}
},
parentMenu: function parentMenu(value) {
// Ensure value is an Menu element.
if (!(value instanceof Menu)) {
throw new TypeError("parentMenu must be a Menu.");
}
}
};
var MenuItem =
/*#__PURE__*/
function () {
/**

@@ -11,3 +36,8 @@ * Construct the menu item.

*/
constructor(menuItemElement, parentMenu) {
function MenuItem(menuItemElement, parentMenu) {
_classCallCheck(this, MenuItem);
// Run validations.
validate.menuItemElement(menuItemElement);
validate.parentMenu(parentMenu);
this.domElements = {

@@ -17,3 +47,2 @@ menuItem: menuItemElement,

};
this.elements = {

@@ -23,47 +52,115 @@ parent: parentMenu

}
/**
* Initialize the menu item by setting its tab index.
*/
initialize() {
this.element.setAttribute("role", "menuitem");
this.link.tabIndex = -1;
}
/**
* The menu item element in the DOM.
*
* @returns {object} - The menu item element.
*/
get element() {
return this.domElements.menuItem;
}
/**
* The link element inside the menu item.
*
* @returns {object} - The link.
*/
get link() {
return this.domElements.link;
}
_createClass(MenuItem, [{
key: "initialize",
value: function initialize() {
this.element.setAttribute("role", "menuitem");
this.link.tabIndex = -1;
}
/**
* The menu item element in the DOM.
*
* @returns {object} - The menu item element.
*/
/**
* The item's parent Menu.
*
* @returns {Menu} - The parent menu.
*/
get parentMenu() {
return this.elements.parent;
}
}, {
key: "focus",
/**
* Focuses the menu item's link.
*/
focus() {
this.element.querySelector("a").focus();
/**
* Focuses the menu item's link.
*/
value: function focus() {
this.element.querySelector("a").focus();
}
}, {
key: "element",
get: function get() {
return this.domElements.menuItem;
}
/**
* The link element inside the menu item.
*
* @returns {object} - The link.
*/
}, {
key: "link",
get: function get() {
return this.domElements.link;
}
/**
* The item's parent Menu.
*
* @returns {Menu} - The parent menu.
*/
}, {
key: "parentMenu",
get: function get() {
return this.elements.parent;
}
}]);
return MenuItem;
}();
var validate$1 = {
menuToggleElement: function menuToggleElement(value) {
// Ensure value is an HTML element.
if (!(value instanceof HTMLElement)) {
throw new TypeError("menuToggleElement must be an HTML Element.");
}
},
menu: function menu(value) {
// Ensure value is an Menu element.
if (!(value instanceof Menu)) {
throw new TypeError("menu must be a Menu.");
}
},
openClass: function openClass(value) {
// Ensure value is a string.
if (typeof value !== "string") {
throw TypeError("openClass must be a string.");
} // Ensure value is a valid CSS class name.
var invalidCharacters = value.replace(/[_a-zA-Z0-9-]/g, "");
if (invalidCharacters.length > 0) {
throw Error("openClass must be a valid CSS class.");
}
},
parentMenu: function parentMenu(value) {
// Value is allowed to be null.
if (value === null) return; // Ensure value is an Menu element.
if (!(value instanceof Menu)) {
throw new TypeError("parentMenu must be a Menu.");
}
},
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) {
// Value is allowed to be null.
if (value === null) return; // Ensure value is an Menu element.
if (!(value instanceof Menu)) {
throw new TypeError("rootMenu must be a Menu.");
}
}
}
};
class MenuToggle {
var MenuToggle =
/*#__PURE__*/
function () {
/**

@@ -79,10 +176,17 @@ * Construct the menu toggle.

*/
constructor(
menuToggleElement,
menu,
openClass = "show",
parentMenu = null,
parentMenuItem = null,
rootMenu = null
) {
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;
_classCallCheck(this, MenuToggle);
// Run validations.
validate$1.menuToggleElement(menuToggleElement);
validate$1.menu(menu);
validate$1.openClass(openClass);
validate$1.parentMenu(parentMenu);
validate$1.parentMenuItem(parentMenuItem);
validate$1.rootMenu(rootMenu);
this.domElements = {

@@ -99,3 +203,2 @@ toggle: menuToggleElement

}
/**

@@ -105,220 +208,322 @@ * Initialize the toggle by ensuring WAI-ARIA values are set,

*/
initialize() {
// Add WAI-ARIA properties.
this.element.setAttribute("aria-haspopup", "true");
this.element.setAttribute("aria-expanded", "false");
this.element.setAttribute("role", "button");
// Handle toggling the menu on click.
this.element.addEventListener("click", event => {
event.preventDefault();
event.stopPropagation();
this.toggle();
});
_createClass(MenuToggle, [{
key: "initialize",
value: function initialize() {
var _this = this;
// Add new keydown events.
this.handleKeydown();
}
// Add WAI-ARIA properties.
this.element.setAttribute("aria-haspopup", "true");
this.element.setAttribute("aria-expanded", "false");
this.element.setAttribute("role", "button"); // Ensure both toggle and menu have IDs.
/**
* The toggle element in the DOM.
*
* @returns {object} - The toggle element.
*/
get element() {
return this.domElements.toggle;
}
if (this.element.id === "" || this.menu.element.id === "") {
var randomString = Math.random().toString(36).replace(/[^a-z]+/g, "").substr(0, 10);
var id = "".concat(this.element.innerText.toLowerCase().replace(/[^a-zA-Z0-9\s]/g, "").replace(/\s/g, "-"), "-").concat(randomString);
this.element.id = this.element.id || "".concat(id, "-menu-button");
this.menu.element.id = this.menu.element.id || "".concat(id, "-menu");
} // Set up proper aria label and control.
/**
* The toggle's parent MenuItem.
*
* @returns {MenuItem} - The parent menu item.
*/
get menuItem() {
return this.elements.menuItem;
}
/**
* The menu controlled by the toggle.
*
* @returns {Menu} - The menu element.
*/
get menu() {
return this.elements.menu;
}
this.menu.element.setAttribute("aria-labelledby", this.element.id);
this.element.setAttribute("aria-controls", this.menu.element.id); // Handle toggling the menu on click.
/**
* The menu containing the toggle.
*
* @returns {Menu} - The menu element.
*/
get parentMenu() {
return this.elements.parentMenu;
}
this.element.addEventListener("click", function (event) {
event.preventDefault();
event.stopPropagation();
/**
* The root menu containing the toggle.
*
* @returns {Menu} - The root menu element.
*/
get rootMenu() {
return this.elements.rootMenu;
}
_this.toggle();
}); // Add new keydown events.
/**
* The open state on the menu.
*
* @returns {boolean} - The open state.
*/
get isOpen() {
return this.show;
}
/**
* Set the open state on the menu.
*
* @param {boolean} state - The open state.
*/
set isOpen(state) {
if (typeof state !== "boolean") {
throw new TypeError("Open state must be true or false.");
this.handleKeydown();
}
/**
* The toggle element in the DOM.
*
* @returns {object} - The toggle element.
*/
this.show = state;
}
}, {
key: "open",
/**
* Opens the submenu.
*/
open() {
if (!this.isOpen) {
// Set the open value.
this.isOpen = true;
/**
* Opens the submenu.
*/
value: function open() {
if (!this.isOpen) {
// Set the open value.
this.isOpen = true; // Assign new WAI-ARIA/class values.
// Assign new WAI-ARIA/class values.
this.element.setAttribute("aria-expanded", "true");
this.menuItem.element.classList.add(this.openClass);
this.menu.element.classList.add(this.openClass);
this.element.setAttribute("aria-expanded", "true");
this.menuItem.element.classList.add(this.openClass);
this.menu.element.classList.add(this.openClass); // Close all sibling menus.
// Close all sibling menus.
this.closeSiblings();
this.closeSiblings(); // Set proper focus states to parent & child.
// Set proper focus states to parent & child.
this.parentMenu.currentFocus = "child";
this.menu.currentFocus = "self";
this.parentMenu.currentFocus = "child";
this.menu.currentFocus = "self"; // Set the new focus.
// Set the new focus.
this.menu.focusFirstChild();
this.menu.focusFirstChild();
}
}
}
/**
* Closes the submenu.
*/
/**
* Closes the submenu.
*/
close() {
if (this.isOpen) {
// Set the open value.
this.isOpen = false;
}, {
key: "close",
value: function close() {
if (this.isOpen) {
// Set the open value.
this.isOpen = false; // Assign new WAI-ARIA/class values.
// Assign new WAI-ARIA/class values.
this.element.setAttribute("aria-expanded", "false");
this.menuItem.element.classList.remove(this.openClass);
this.menu.element.classList.remove(this.openClass);
this.element.setAttribute("aria-expanded", "false");
this.menuItem.element.classList.remove(this.openClass);
this.menu.element.classList.remove(this.openClass); // Close all child menus.
// Close all child menus.
this.closeChildren();
this.closeChildren(); // Set proper focus states to parent & child.
// Set proper focus states to parent & child.
this.menu.currentFocus = "none";
this.parentMenu.currentFocus = "self";
this.menu.currentFocus = "none";
this.parentMenu.currentFocus = "self"; // Set the new focus.
// Set the new focus.
this.parentMenu.focusCurrentChild();
this.parentMenu.focusCurrentChild();
}
}
}
/**
* Toggles the open state of the menu.
*/
/**
* Toggles the open state of the menu.
*/
toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}, {
key: "toggle",
value: function toggle() {
if (this.isOpen) {
this.close();
} else {
this.open();
}
}
}
/**
* Closes all sibling menus.
*/
/**
* Closes all sibling menus.
*/
closeSiblings() {
try {
this.parentMenu.menuToggles.forEach(toggle => {
if (toggle !== this) toggle.close();
}, {
key: "closeSiblings",
value: function closeSiblings() {
var _this2 = this;
try {
this.parentMenu.menuToggles.forEach(function (toggle) {
if (toggle !== _this2) toggle.close();
});
} catch (error) {// Fail quietly. No parent exists.
}
}
/**
* Closes all child menus.
*/
}, {
key: "closeChildren",
value: function closeChildren() {
this.menu.menuToggles.forEach(function (toggle) {
return toggle.close();
});
} catch (error) {
// Fail quietly. No parent exists.
}
}
/**
* Sets up the hijacked keydown events.
*/
/**
* Closes all child menus.
*/
closeChildren() {
this.menu.menuToggles.forEach(toggle => toggle.close());
}
}, {
key: "handleKeydown",
value: function handleKeydown() {
var _this3 = this;
/**
* Sets up the hijacked keydown events.
*/
handleKeydown() {
/**
* Short cut to preventing default event actions.
*
* @param {object} event - The event.
*/
function preventDefault(event) {
event.preventDefault();
event.stopPropagation();
}
this.menu.element.addEventListener("keydown", function (event) {
var key = event.key;
if (key === "Escape") {
// The Escape key should close the current menu.
preventDefault(event);
_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);
_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.close();
_this3.parentMenu.focusPreviousChild();
}
});
this.menuItem.element.addEventListener("keydown", function (event) {
var key = event.key;
if (_this3.menu.currentFocus === "none" && _this3.parentMenu.isTopLevel) {
if (key === "ArrowUp") {
// The Up Arrow key should open the submenu and select the last child.
preventDefault(event);
_this3.open();
_this3.menu.focusLastChild();
} else if (key === "ArrowDown") {
// The Down Arrow key should open the submenu and select the first child.
preventDefault(event);
_this3.open();
}
}
});
}
}, {
key: "element",
get: function get() {
return this.domElements.toggle;
}
/**
* Short cut to preventing default event actions.
* The toggle's parent MenuItem.
*
* @param {object} event - The event.
* @returns {MenuItem} - The parent menu item.
*/
function preventDefault(event) {
event.preventDefault();
event.stopPropagation();
}, {
key: "menuItem",
get: function get() {
return this.elements.menuItem;
}
this.menu.element.addEventListener("keydown", event => {
const { key } = event;
/**
* The menu controlled by the toggle.
*
* @returns {Menu} - The menu element.
*/
if (key === "Escape") {
// The Escape key should close the current menu.
preventDefault(event);
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();
}, {
key: "menu",
get: function get() {
return this.elements.menu;
}
/**
* The menu containing the toggle.
*
* @returns {Menu} - The menu element.
*/
}, {
key: "parentMenu",
get: function get() {
return this.elements.parentMenu;
}
/**
* The root menu containing the toggle.
*
* @returns {Menu} - The root menu element.
*/
}, {
key: "rootMenu",
get: function get() {
return this.elements.rootMenu;
}
/**
* The open state on the menu.
*
* @returns {boolean} - The open state.
*/
}, {
key: "isOpen",
get: function get() {
return this.show;
}
/**
* Set the open state on the menu.
*
* @param {boolean} state - The open state.
*/
,
set: function set(state) {
if (typeof state !== "boolean") {
throw new TypeError("Open state must be true or false.");
}
});
this.menuItem.element.addEventListener("keydown", event => {
const { key } = event;
if (this.menu.currentFocus === "none" && this.parentMenu.isTopLevel) {
if (key === "ArrowUp") {
// The Up Arrow key should open the submenu and select the last child.
preventDefault(event);
this.open();
this.menu.focusLastChild();
} else if (key === "ArrowDown") {
// The Down Arrow key should open the submenu and select the first child.
preventDefault(event);
this.open();
}
}
});
this.show = state;
}
}]);
return MenuToggle;
}();
var validate$2 = {
menuElement: function menuElement(value) {
// Ensure value is an HTML element.
if (!(value instanceof HTMLElement)) {
throw new TypeError("menuElement must be an HTML Element.");
}
},
menuItemSelector: function menuItemSelector(value) {
// Ensure value is a string.
if (typeof value !== "string") {
throw new TypeError("menuItemSelector must be a CSS selector string.");
}
},
submenuItemSelector: function submenuItemSelector(value) {
// Ensure value is a string.
if (typeof value !== "string") {
throw new TypeError("submenuItemSelector must be a CSS selector string.");
}
},
submenuToggleSelector: function submenuToggleSelector(value) {
// Ensure value is a string.
if (typeof value !== "string") {
throw new TypeError("submenuToggleSelector must be a CSS selector string.");
}
},
submenuSelector: function submenuSelector(value) {
// Ensure value is a string.
if (typeof value !== "string") {
throw new TypeError("submenuSelector must be a CSS selector string.");
}
},
submenuOpenClass: function submenuOpenClass(value) {
// Ensure value is a string.
if (typeof value !== "string") {
throw TypeError("submenuOpenClass must be a string.");
} // Ensure value is a valid CSS class name.
var invalidCharacters = value.replace(/[_a-zA-Z0-9-]/g, "");
if (invalidCharacters.length > 0) {
throw Error("submenuOpenClass must be a valid CSS class.");
}
},
isTopLevel: function isTopLevel(value) {
// Ensure value is a string.
if (typeof value !== "boolean") {
throw new TypeError("isTopLevel must be true or false");
}
}
}
};
class Menu {
var Menu =
/*#__PURE__*/
function () {
/**

@@ -335,19 +540,24 @@ * Constructs the menu.

*/
constructor(
menuElement,
menuItemSelector,
submenuItemSelector,
submenuToggleSelector,
submenuSelector,
submenuOpenClass = "show",
isTopLevel = true
) {
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;
_classCallCheck(this, Menu);
// Run validations.
validate$2.menuElement(menuElement);
validate$2.menuItemSelector(menuItemSelector);
validate$2.submenuItemSelector(submenuItemSelector);
validate$2.submenuToggleSelector(submenuToggleSelector);
validate$2.submenuSelector(submenuSelector);
validate$2.submenuOpenClass(submenuOpenClass);
validate$2.isTopLevel(isTopLevel);
this.domElements = {
menu: menuElement,
menuItems: Array.from(
menuElement.querySelectorAll(menuItemSelector)
).filter(item => item.parentElement === menuElement),
submenuItems: Array.from(
menuElement.querySelectorAll(submenuItemSelector)
).filter(item => item.parentElement === menuElement)
menuItems: Array.from(menuElement.querySelectorAll(menuItemSelector)).filter(function (item) {
return item.parentElement === menuElement;
}),
submenuItems: Array.from(menuElement.querySelectorAll(submenuItemSelector)).filter(function (item) {
return item.parentElement === menuElement;
})
};

@@ -369,3 +579,2 @@ this.domSelectors = {

}
/**

@@ -378,337 +587,379 @@ * Initializes the menu with proper tab indexing and properties.

*/
initialize() {
this.element.setAttribute("role", "menu");
this.element.tabIndex = 0;
this.createMenuItems();
this.handleKeydown();
this.handleClick();
}
/**
* The menu element in the DOM.
*
* @returns {object} - The menu.
*/
get element() {
return this.domElements.menu;
}
_createClass(Menu, [{
key: "initialize",
value: function initialize() {
this.element.setAttribute("role", "menu");
this.element.tabIndex = 0;
this.createMenuItems();
this.handleKeydown();
this.handleClick();
}
/**
* The menu element in the DOM.
*
* @returns {object} - The menu.
*/
/**
* The menu item DOM elements contained in the menu.
*
* @returns {object[]} - The menu items.
*/
get menuItemElements() {
return this.domElements.menuItems;
}
}, {
key: "createMenuItems",
/**
* The submenu item DOM elements contained in the menu.
*
* @returns {object[]} - The submenu items.
*/
get submenuItemElements() {
return this.domElements.submenuItems;
}
/**
* Creates and initializes all menu items.
*/
value: function createMenuItems() {
var _this4 = this;
/**
* The menu items contained in the menu.
*
* @returns {MenuItem[]} - The menu items.
*/
get menuItems() {
return this.elements.menuItems;
}
this.menuItemElements.forEach(function (element) {
// Create a new MenuItem.
var menuItem = new MenuItem(element, _this4); // Add the item to the list of menu items.
/**
* The menu toggles contained in the menu.
*
* @returns {MenuToggle[]} - The menu toggles.
*/
get menuToggles() {
return this.elements.menuToggles;
}
_this4.elements.menuItems.push(menuItem); // Initialize the menu item.
/**
* The DOM Selectors for the menu.
*
* @returns {object} - The DOM Selectors.
*/
get selector() {
return this.domSelectors;
}
/**
* The focus state of the menu.
*
* @returns {string} - The focus state (self, child, none).
*/
get currentFocus() {
return this.focusState;
}
menuItem.initialize(); // If the menu item is a dropdown, create a SubmenuItem,
// otherwise create a normal MenuItem.
/**
* The class used for open submenus.
*
* @returns {string} - The open class.
*/
get openClass() {
return this.submenuOpenClass;
}
if (_this4.submenuItemElements.includes(element)) {
// The menu's toggle controller DOM element.
var toggler = element.querySelector(_this4.selector["submenu-toggle"]); // The actual menu DOM element.
/**
* The flag to mark as a top-level menu.
*
* @returns {boolean} - The top-level flag.
*/
get isTopLevel() {
return this.root;
}
var submenu = element.querySelector(_this4.selector.submenu); // Create the new Menu and initialize it.
/**
* Set the focus state of the menu.
*
* @param {boolean} state - The focus state (self, child, none).
*/
set currentFocus(state) {
const states = ["self", "child", "none"];
var menu = new Menu(submenu, _this4.selector["menu-items"], _this4.selector["submenu-items"], _this4.selector["submenu-toggle"], _this4.selector.submenu, _this4.openClass, false);
menu.initialize(); // Create the new MenuToggle.
if (!states.includes(state)) {
throw new Error("Focus state must be 'self', 'child', or 'none'.");
var toggle = new MenuToggle(toggler, menu, _this4.openClass, _this4, menuItem);
toggle.initialize(); // Add it to the list of submenu items.
_this4.elements.menuToggles.push(toggle);
}
});
}
/**
* Sets up the hijacked keydown events.
*/
this.focusState = state;
}
}, {
key: "handleKeydown",
value: function handleKeydown() {
var _this5 = this;
/**
* Set the class used for open submenus.
*
* @param {string} value - The open class.
*/
set openClass(value) {
if (typeof value !== "string") {
throw new TypeError("Class must be a string.");
}
/**
* Short cut to preventing default event actions.
*
* @param {object} event - The event.
*/
function preventDefault(event) {
event.preventDefault();
event.stopPropagation();
}
this.submenuOpenClass = value;
}
this.element.addEventListener("keydown", function (event) {
var key = event.key,
code = event.code;
set isTopLevel(value) {
if (typeof value !== "boolean") {
throw new TypeError("Top-level flag must be true or false.");
}
if (_this5.currentFocus === "none") {
if (key === "Enter" || key === " " && code === "Space") {
// The Enter & Space keys should enter the menu.
preventDefault(event);
_this5.currentFocus = "self";
this.root = value;
}
_this5.focusFirstChild();
}
} else if (_this5.currentFocus === "self") {
if (key === "Escape") {
// The Escape key should exit the menu.
preventDefault(event);
/**
* Creates and initializes all menu items.
*/
createMenuItems() {
this.menuItemElements.forEach(element => {
// Create a new MenuItem.
const menuItem = new MenuItem(element, this);
_this5.focus();
// Add the item to the list of menu items.
this.elements.menuItems.push(menuItem);
_this5.currentFocus = "none";
} else if (!_this5.isTopLevel && key === "ArrowUp") {
// The Up Arrow key should focus the previous menu item in submenus.
preventDefault(event);
// Initialize the menu item.
menuItem.initialize();
_this5.focusPreviousChild();
} else if (_this5.isTopLevel && key === "ArrowRight") {
// The Right Arrow key should focus the next menu item.
preventDefault(event);
// If the menu item is a dropdown, create a SubmenuItem,
// otherwise create a normal MenuItem.
if (this.submenuItemElements.includes(element)) {
// The menu's toggle controller DOM element.
const toggler = element.querySelector(this.selector["submenu-toggle"]);
// The actual menu DOM element.
const submenu = element.querySelector(this.selector.submenu);
_this5.focusNextChild();
} else if (!_this5.isTopLevel && key === "ArrowDown") {
// The Down Arrow key should focus the next item in submenus.
preventDefault(event);
// 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
);
menu.initialize();
_this5.focusNextChild();
} else if (_this5.isTopLevel && key === "ArrowLeft") {
// The Left Arrow key should focus the previous menu item.
preventDefault(event);
// Create the new MenuToggle.
const toggle = new MenuToggle(
toggler,
menu,
this.openClass,
this,
menuItem
);
toggle.initialize();
_this5.focusPreviousChild();
} else if (key === "Home") {
// The Home key should focus the first menu item.
preventDefault(event);
// Add it to the list of submenu items.
this.elements.menuToggles.push(toggle);
}
});
}
_this5.focusFirstChild();
} else if (key === "End") {
// The End key should focus the last menu item.
preventDefault(event);
/**
* Sets up the hijacked keydown events.
*/
handleKeydown() {
_this5.focusLastChild();
}
}
if (_this5.currentFocus !== "none") {
if (key === "Tab") {
// The Tab key should select the next element outside of the menu.
_this5.blur();
_this5.closeChildren();
}
}
});
}
/**
* Short cut to preventing default event actions.
*
* @param {object} event - The event.
* Handle click events required for proper menu usage.
*/
function preventDefault(event) {
event.preventDefault();
event.stopPropagation();
}, {
key: "handleClick",
value: function handleClick() {
var _this6 = this;
document.addEventListener("click", function (event) {
if (!_this6.element.contains(event.target) && _this6.element !== event.target) {
_this6.blur();
_this6.closeChildren();
}
});
}
/**
* Focus the menu.
*/
this.element.addEventListener("keydown", event => {
const { key, code } = event;
}, {
key: "focus",
value: function focus() {
this.focussedChild = 0;
this.currentFocus = "self";
this.element.focus();
}
/**
* Unfocus the menu.
*/
if (this.currentFocus === "none") {
if (key === "Enter" || (key === " " && code === "Space")) {
// The Enter & Space keys should enter the menu.
preventDefault(event);
this.currentFocus = "self";
this.focusFirstChild();
}
} else if (this.currentFocus === "self") {
if (key === "Escape") {
// The Escape key should exit the menu.
preventDefault(event);
this.focus();
this.currentFocus = "none";
} else if (!this.isTopLevel && key === "ArrowUp") {
// The Up Arrow key should focus the previous menu item in submenus.
preventDefault(event);
this.focusPreviousChild();
} else if (this.isTopLevel && key === "ArrowRight") {
// The Right Arrow key should focus the next menu item.
preventDefault(event);
this.focusNextChild();
} else if (!this.isTopLevel && key === "ArrowDown") {
// The Down Arrow key should focus the next item in submenus.
preventDefault(event);
this.focusNextChild();
} else if (this.isTopLevel && key === "ArrowLeft") {
// The Left Arrow key should focus the previous menu item.
preventDefault(event);
this.focusPreviousChild();
} else if (key === "Home") {
// The Home key should focus the first menu item.
preventDefault(event);
this.focusFirstChild();
} else if (key === "End") {
// The End key should focus the last menu item.
preventDefault(event);
this.focusLastChild();
}
}, {
key: "blur",
value: function blur() {
this.focussedChild = -1;
this.currentFocus = "none";
this.element.blur();
}
/**
* Focues the menu's first child.
*/
}, {
key: "focusFirstChild",
value: function focusFirstChild() {
this.focussedChild = 0;
this.focusCurrentChild();
}
/**
* Focus the menu's last child.
*/
}, {
key: "focusLastChild",
value: function focusLastChild() {
this.focussedChild = this.menuItems.length - 1;
this.focusCurrentChild();
}
/**
* Focus the menu's next child.
*/
}, {
key: "focusNextChild",
value: function focusNextChild() {
if (this.focussedChild === this.menuItems.length - 1) {
this.focusFirstChild();
} else {
this.focussedChild = this.focussedChild + 1;
this.focusCurrentChild();
}
}
/**
* Focus the menu's last child.
*/
if (this.currentFocus !== "none") {
if (key === "Tab") {
// The Tab key should select the next element outside of the menu.
this.blur();
this.closeChildren();
}
}, {
key: "focusPreviousChild",
value: function focusPreviousChild() {
if (this.focussedChild === 0) {
this.focusLastChild();
} else {
this.focussedChild = this.focussedChild - 1;
this.focusCurrentChild();
}
});
}
}
/**
* Focus the menu's current child.
*/
/**
* Handle click events required for proper menu usage.
*/
handleClick() {
document.addEventListener("click", event => {
if (
!this.element.contains(event.target) &&
this.element !== event.target
) {
this.blur();
this.closeChildren();
}, {
key: "focusCurrentChild",
value: function focusCurrentChild() {
if (this.focussedChild !== -1) {
this.menuItems[this.focussedChild].focus();
}
});
}
}
/**
* Close all submenu children.
*/
/**
* Focus the menu.
*/
focus() {
this.focussedChild = 0;
this.currentFocus = "self";
this.element.focus();
}
}, {
key: "closeChildren",
value: function closeChildren() {
this.menuToggles.forEach(function (toggle) {
return toggle.close();
});
}
}, {
key: "element",
get: function get() {
return this.domElements.menu;
}
/**
* The menu item DOM elements contained in the menu.
*
* @returns {object[]} - The menu items.
*/
/**
* Unfocus the menu.
*/
blur() {
this.focussedChild = -1;
this.currentFocus = "none";
this.element.blur();
}
}, {
key: "menuItemElements",
get: function get() {
return this.domElements.menuItems;
}
/**
* The submenu item DOM elements contained in the menu.
*
* @returns {object[]} - The submenu items.
*/
/**
* Focues the menu's first child.
*/
focusFirstChild() {
this.focussedChild = 0;
this.focusCurrentChild();
}
}, {
key: "submenuItemElements",
get: function get() {
return this.domElements.submenuItems;
}
/**
* The menu items contained in the menu.
*
* @returns {MenuItem[]} - The menu items.
*/
/**
* Focus the menu's last child.
*/
focusLastChild() {
this.focussedChild = this.menuItems.length - 1;
this.focusCurrentChild();
}
}, {
key: "menuItems",
get: function get() {
return this.elements.menuItems;
}
/**
* The menu toggles contained in the menu.
*
* @returns {MenuToggle[]} - The menu toggles.
*/
/**
* Focus the menu's next child.
*/
focusNextChild() {
if (this.focussedChild === this.menuItems.length - 1) {
this.focusFirstChild();
} else {
this.focussedChild = this.focussedChild + 1;
this.focusCurrentChild();
}, {
key: "menuToggles",
get: function get() {
return this.elements.menuToggles;
}
}
/**
* The DOM Selectors for the menu.
*
* @returns {object} - The DOM Selectors.
*/
/**
* Focus the menu's last child.
*/
focusPreviousChild() {
if (this.focussedChild === 0) {
this.focusLastChild();
} else {
this.focussedChild = this.focussedChild - 1;
this.focusCurrentChild();
}, {
key: "selector",
get: function get() {
return this.domSelectors;
}
}
/**
* The focus state of the menu.
*
* @returns {string} - The focus state (self, child, none).
*/
/**
* Focus the menu's current child.
*/
focusCurrentChild() {
if (this.focussedChild !== -1) {
this.menuItems[this.focussedChild].focus();
}, {
key: "currentFocus",
get: function get() {
return this.focusState;
}
}
/**
* The class used for open submenus.
*
* @returns {string} - The open class.
*/
,
/**
* Close all submenu children.
*/
closeChildren() {
this.menuToggles.forEach(toggle => toggle.close());
}
}
/**
* Set the focus state of the menu.
*
* @param {boolean} state - The focus state (self, child, none).
*/
set: function set(state) {
var states = ["self", "child", "none"];
if (!states.includes(state)) {
throw new Error("Focus state must be 'self', 'child', or 'none'.");
}
this.focusState = state;
}
/**
* Set the class used for open submenus.
*
* @param {string} value - The open class.
*/
}, {
key: "openClass",
get: function get() {
return this.submenuOpenClass;
}
/**
* The flag to mark as a top-level menu.
*
* @returns {boolean} - The top-level flag.
*/
,
set: function set(value) {
if (typeof value !== "string") {
throw new TypeError("Class must be a string.");
}
this.submenuOpenClass = value;
}
}, {
key: "isTopLevel",
get: function get() {
return this.root;
},
set: function set(value) {
if (typeof value !== "boolean") {
throw new TypeError("Top-level flag must be true or false.");
}
this.root = value;
}
}]);
return Menu;
}();
return Menu;
}());
}();
{
"name": "accessible-menu",
"version": "1.0.0-beta.1",
"version": "1.0.0-beta.2",
"description": "A JavaScript library to help you generate WAI-ARIA accessible menus in the DOM.",

@@ -11,3 +11,4 @@ "main": "src/menu.js",

"release": "npx standard-version",
"bundle": "npx rollup --config .rollup.config.js"
"bundle": "npx rollup --config .rollup.config.js",
"build": "npm run bundle && npx babel dist -d dist"
},

@@ -35,2 +36,5 @@ "repository": {

"devDependencies": {
"@babel/cli": "^7.7.0",
"@babel/core": "^7.7.2",
"@babel/preset-env": "^7.7.1",
"@commitlint/cli": "^8.2.0",

@@ -37,0 +41,0 @@ "@commitlint/config-conventional": "^8.2.0",

@@ -8,20 +8,75 @@ # accessible-menu

## Committing
## Installation
This project uses the conventional commit standard, which means your commits should follow a basic template of:
### NPM
<type>[optional scope]: <description>
NPM is recommended for large-scale development, since it works well with bundlers like [Webpack](https://webpack.js.org/) or [Rollup](https://rollupjs.org/guide/en/).
[optional body]
```shell
# latest stable
npm install accessible-menu
```
[optional footer(s)]
### CDN
For more detailed information about available types, scopes, breaking changes, etc. please see the [official documentation](https://www.conventionalcommits.org/en/v1.0.0/).
For learning/prototyping purposes you can use the latest version with:
This project also provides a command to assist you in formatting commit messages using [commitizen](https://commitizen.github.io/cz-cli/):
```html
<script src="https://cdn.jsdelivr.net/npm/accessible-menu/dist/accessibleMenu.js"></script>
```
For production environments, it is recommend to use a specific version to avoid unforseen breaking changes:
```html
<script src="https://cdn.jsdelivr.net/npm/accessible-menu@1.0.0-beta.1/dist/accessibleMenu.js"></script>
```
npm run commit
## Usage
To use accessible-menu, you first need to ensure your menu follows a basic menu structure.
```html
<menu>
<menu-item><a>...</a></menu-item>
<menu-item-with-dropdown>
<dropdown-toggle />
<dropdown-menu>
<menu-item><a>...</a></menu-item>
...
</dropdown-menu>
</menu-item-with-dropdown>
<menu-item><a>...</a></menu-item>
...
</menu>
```
include the root menu or bundled library in your project:
```jsx
import AccessibleMenu from 'accessible-menu';
```
or
```html
<script src="path/to/accessible-menu/dist/accessibleMenu.js"></script>
```
Once you have accessible-menu loaded, simply declare a new menu object and initialize it.
```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"
);
menu.initialize();
```
Want more detailed usage information? It's on the way! Check back for a later release which will include much more documentation.
## Versioning

@@ -31,24 +86,32 @@

Given a version number MAJOR.MINOR.PATCH, increment the:
For more detailed information about SemVer, please see the [official documentation](https://semver.org/).
1. MAJOR version when you make incompatible API changes,
2. MINOR version when you add functionality in a backward compatible manner, and
3. PATCH version when you make backwards compatible bug fixes.
## Development
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
### Set up
For more detailed information about SemVer, please see the [official documentation](https://semver.org/).
Run `npm install`.
When making a release, you should use the provided command:
This will ensure you have all the dependencies needed to properly lint your code and commits.
### Committing
This project uses the conventional commit standard, which means your commits should follow a basic template of:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
For more detailed information about available types, scopes, breaking changes, etc. please see the [official documentation](https://www.conventionalcommits.org/en/v1.0.0/).
This project also provides a command to assist you in formatting commit messages using [commitizen](https://commitizen.github.io/cz-cli/):
```
npm run release
npm run commit
```
This command uses [standard-version](https://github.com/conventional-changelog/standard-version) to parse through your commits, decide what kind of release will be created, and automatically generates a CHANGELOG.md file for your project. These changes are then commited using the message `chore(release): <version number>`.
### Coding standards
Once that is done, you can simply run `git push --follow-tags origin` to have your release pushed up to the repository.
## Coding standards
This project follows a set of coding standards combining [StandardJS](https://standardjs.com/), [Prettier](https://prettier.io/), and [JSDoc](https://jsdoc.app/).

@@ -55,0 +118,0 @@

import MenuItem from "./menuItem";
import MenuToggle from "./menuToggle";
const validate = {
menuElement: value => {
// Ensure value is an HTML element.
if (!(value instanceof HTMLElement)) {
throw new TypeError("menuElement must be an HTML Element.");
}
},
menuItemSelector: value => {
// Ensure value is a string.
if (typeof value !== "string") {
throw new TypeError("menuItemSelector must be a CSS selector string.");
}
},
submenuItemSelector: value => {
// Ensure value is a string.
if (typeof value !== "string") {
throw new TypeError("submenuItemSelector must be a CSS selector string.");
}
},
submenuToggleSelector: value => {
// Ensure value is a string.
if (typeof value !== "string") {
throw new TypeError(
"submenuToggleSelector must be a CSS selector string."
);
}
},
submenuSelector: value => {
// Ensure value is a string.
if (typeof value !== "string") {
throw new TypeError("submenuSelector must be a CSS selector string.");
}
},
submenuOpenClass: value => {
// Ensure value is a string.
if (typeof value !== "string") {
throw TypeError("submenuOpenClass must be a string.");
}
// Ensure value is a valid CSS class name.
const invalidCharacters = value.replace(/[_a-zA-Z0-9-]/g, "");
if (invalidCharacters.length > 0) {
throw Error("submenuOpenClass must be a valid CSS class.");
}
},
isTopLevel: value => {
// Ensure value is a string.
if (typeof value !== "boolean") {
throw new TypeError("isTopLevel must be true or false");
}
}
};
class Menu {

@@ -25,2 +78,11 @@ /**

) {
// Run validations.
validate.menuElement(menuElement);
validate.menuItemSelector(menuItemSelector);
validate.submenuItemSelector(submenuItemSelector);
validate.submenuToggleSelector(submenuToggleSelector);
validate.submenuSelector(submenuSelector);
validate.submenuOpenClass(submenuOpenClass);
validate.isTopLevel(isTopLevel);
this.domElements = {

@@ -27,0 +89,0 @@ menu: menuElement,

import Menu from "./menu";
const validate = {
menuItemElement: value => {
// Ensure value is an HTML element.
if (!(value instanceof HTMLElement)) {
throw new TypeError("menuItemElement must be an HTML Element.");
}
},
parentMenu: value => {
// Ensure value is an Menu element.
if (!(value instanceof Menu)) {
throw new TypeError("parentMenu must be a Menu.");
}
}
};
class MenuItem {

@@ -11,2 +26,6 @@ /**

constructor(menuItemElement, parentMenu) {
// Run validations.
validate.menuItemElement(menuItemElement);
validate.parentMenu(parentMenu);
this.domElements = {

@@ -13,0 +32,0 @@ menuItem: menuItemElement,

import Menu from "./menu";
import MenuItem from "./menuItem";
const validate = {
menuToggleElement: value => {
// Ensure value is an HTML element.
if (!(value instanceof HTMLElement)) {
throw new TypeError("menuToggleElement must be an HTML Element.");
}
},
menu: value => {
// Ensure value is an Menu element.
if (!(value instanceof Menu)) {
throw new TypeError("menu must be a Menu.");
}
},
openClass: value => {
// Ensure value is a string.
if (typeof value !== "string") {
throw TypeError("openClass must be a string.");
}
// Ensure value is a valid CSS class name.
const invalidCharacters = value.replace(/[_a-zA-Z0-9-]/g, "");
if (invalidCharacters.length > 0) {
throw Error("openClass must be a valid CSS class.");
}
},
parentMenu: value => {
// Value is allowed to be null.
if (value === null) return;
// Ensure value is an Menu element.
if (!(value instanceof Menu)) {
throw new TypeError("parentMenu must be a Menu.");
}
},
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 => {
// Value is allowed to be null.
if (value === null) return;
// Ensure value is an Menu element.
if (!(value instanceof Menu)) {
throw new TypeError("rootMenu must be a Menu.");
}
}
};
class MenuToggle {

@@ -23,2 +76,10 @@ /**

) {
// Run validations.
validate.menuToggleElement(menuToggleElement);
validate.menu(menu);
validate.openClass(openClass);
validate.parentMenu(parentMenu);
validate.parentMenuItem(parentMenuItem);
validate.rootMenu(rootMenu);
this.domElements = {

@@ -46,2 +107,22 @@ toggle: menuToggleElement

// Ensure both toggle and menu have IDs.
if (this.element.id === "" || this.menu.element.id === "") {
const randomString = Math.random()
.toString(36)
.replace(/[^a-z]+/g, "")
.substr(0, 10);
const id = `${this.element.innerText
.toLowerCase()
.replace(/[^a-zA-Z0-9\s]/g, "")
.replace(/\s/g, "-")}-${randomString}`;
this.element.id = this.element.id || `${id}-menu-button`;
this.menu.element.id = this.menu.element.id || `${id}-menu`;
}
// Set up proper aria label and control.
this.menu.element.setAttribute("aria-labelledby", this.element.id);
this.element.setAttribute("aria-controls", this.menu.element.id);
// Handle toggling the menu on click.

@@ -48,0 +129,0 @@ this.element.addEventListener("click", event => {

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