accessible-menu
Advanced tools
Comparing version 1.0.0-beta.1 to 1.0.0-beta.2
@@ -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 @@ |
@@ -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", |
105
README.md
@@ -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 => { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
72417
21
1660
128
19