aria-components
Advanced tools
Comparing version 0.3.1 to 0.3.2-beta-1
# Change Log | ||
This project adheres to [Semantic Versioning](http://semver.org/). | ||
## 0.3.2-beta-1 | ||
**Changed** | ||
- Loosens MenuBar and Menu components' menuitems' markup requirements (#48, 3385f2e) | ||
- Dialog no longer re-queries for interactive child elements on every TAB keydown (167cc70) | ||
**Added** | ||
- Adds support for validating Menu & MenuBar menu items (#49) | ||
- Adds support for refreshing elements tracked by components: MenuBar and Menu items, interactive child elements | ||
within Dialog, Disclosure, MenuButton and Popup targets, and Listbox options (#50) | ||
- MenuBar Popup state changes now update state in the MenuBar itself (2e1dcb1) | ||
**Fixed** | ||
- Scopes MenuBar Popup events to the controller (0374543) | ||
## 0.3.1 | ||
@@ -5,0 +23,0 @@ |
{ | ||
"name": "aria-components", | ||
"version": "0.3.1", | ||
"version": "0.3.2-beta-1", | ||
"description": "JavaScript classes to aid in accessible web development.", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
@@ -108,2 +108,3 @@ import AriaComponent from '../AriaComponent'; | ||
// Bind class methods | ||
this.setInteractiveChildren = this.setInteractiveChildren.bind(this); | ||
this.onPopupStateChange = this.onPopupStateChange.bind(this); | ||
@@ -132,2 +133,17 @@ this.handleTargetKeydown = this.handleTargetKeydown.bind(this); | ||
/** | ||
* Collect the Dialog's interactive child elements. | ||
*/ | ||
setInteractiveChildren() { | ||
this.interactiveChildElements = interactiveChildren(this.target); | ||
const [ | ||
firstInteractiveChild, | ||
lastInteractiveChild, | ||
] = getFirstAndLastItems(this.interactiveChildElements); | ||
// Save as instance properties. | ||
Object.assign(this, { firstInteractiveChild, lastInteractiveChild }); | ||
} | ||
/** | ||
* Set the component's DOM attributes and event listeners. | ||
@@ -155,7 +171,7 @@ */ | ||
/* | ||
* Collect the Dialog's interactive child elements. This is an initial pass | ||
* to ensure values exists, but the interactive children will be collected | ||
* each time the dialog opens, in case the dialog's contents change. | ||
* This is an initial pass to ensure values exists, but the interactive | ||
* children will be collected each time the dialog opens, in case the | ||
* dialog's contents change. | ||
*/ | ||
this.interactiveChildElements = interactiveChildren(this.target); | ||
this.setInteractiveChildren(); | ||
@@ -203,3 +219,3 @@ // Add event listeners. | ||
this.interactiveChildElements = interactiveChildren(this.target); | ||
this.setInteractiveChildren(); | ||
@@ -247,8 +263,4 @@ if (expanded) { | ||
const { activeElement } = document; | ||
const [ | ||
firstInteractiveChild, | ||
lastInteractiveChild, | ||
] = getFirstAndLastItems(this.interactiveChildElements); | ||
if (shiftKey && firstInteractiveChild === activeElement) { | ||
if (shiftKey && this.firstInteractiveChild === activeElement) { | ||
event.preventDefault(); | ||
@@ -259,4 +271,4 @@ /* | ||
*/ | ||
lastInteractiveChild.focus(); | ||
} else if (! shiftKey && lastInteractiveChild === activeElement) { | ||
this.lastInteractiveChild.focus(); | ||
} else if (! shiftKey && this.lastInteractiveChild === activeElement) { | ||
event.preventDefault(); | ||
@@ -267,3 +279,3 @@ /* | ||
*/ | ||
firstInteractiveChild.focus(); | ||
this.firstInteractiveChild.focus(); | ||
} | ||
@@ -270,0 +282,0 @@ } |
@@ -98,2 +98,7 @@ Dialog | ||
/** | ||
* Collect the Dialog's interactive child elements. | ||
*/ | ||
setInteractiveChildren(); | ||
/** | ||
* Destroy the Dialog and Popup. | ||
@@ -100,0 +105,0 @@ */ |
@@ -93,2 +93,3 @@ import AriaComponent from '../AriaComponent'; | ||
// Bind class methods. | ||
this.setInteractiveChildren = this.setInteractiveChildren.bind(this); | ||
this.init = this.init.bind(this); | ||
@@ -110,2 +111,30 @@ this.destroy = this.destroy.bind(this); | ||
/** | ||
* Collect the Disclosure's interactive child elements. | ||
*/ | ||
setInteractiveChildren() { | ||
const { expanded } = this.state; | ||
/** | ||
* Collect the target element's interactive child elements. | ||
* | ||
* @type {array} | ||
*/ | ||
this.interactiveChildElements = interactiveChildren(this.target); | ||
/** | ||
* Allow or deny keyboard focus depending on component state. | ||
* | ||
* Prevent focus on interactive elements in the target when the target is | ||
* hidden. This isn't such an issue when the target is hidden with | ||
* `display:none`, but is necessary if the target is hidden by other means, | ||
* such as minimized height or width. | ||
*/ | ||
if (expanded) { | ||
tabIndexAllow(this.interactiveChildElements); | ||
} else { | ||
tabIndexDeny(this.interactiveChildElements); | ||
} | ||
} | ||
/** | ||
* Add initial attributes, establish relationships, and listen for events | ||
@@ -123,8 +152,2 @@ */ | ||
/** | ||
* Collect the target element's interactive child elements. | ||
* @type {array} | ||
*/ | ||
this.interactiveChildElements = interactiveChildren(this.target); | ||
// Ensure the target and controller each have an ID attribute. | ||
@@ -181,9 +204,4 @@ [this.controller, this.target].forEach((element) => { | ||
/* | ||
* Prevent focus on interactive elements in the target when the target is | ||
* hidden. This isn't such an issue when the target is hidden with | ||
* `display:none`, but is necessary if the target is hidden by other means, | ||
* such as minimized height or width. | ||
*/ | ||
tabIndexDeny(this.interactiveChildElements); | ||
// Collect the target element's interactive child elements. | ||
this.setInteractiveChildren(); | ||
@@ -190,0 +208,0 @@ // Run {initCallback} |
@@ -86,2 +86,7 @@ Disclosure | ||
/** | ||
* Collect the Disclosure's interactive child elements. | ||
*/ | ||
setInteractiveChildren(); | ||
/** | ||
* Remove all ARIA attributes added by this class. | ||
@@ -88,0 +93,0 @@ */ |
@@ -76,2 +76,3 @@ import AriaComponent from '../AriaComponent'; | ||
// Bind class methods. | ||
this.setListBoxOptions = this.setListBoxOptions.bind(this); | ||
this.preventWindowScroll = this.preventWindowScroll.bind(this); | ||
@@ -93,11 +94,5 @@ this.handleControllerKeyup = this.handleControllerKeyup.bind(this); | ||
/** | ||
* Set up the component's DOM attributes and event listeners. | ||
* Collect and configure ListBox options. | ||
*/ | ||
init() { | ||
/* | ||
* A reference to the class instance added to the controller and target | ||
* elements to enable external interactions with this instance. | ||
*/ | ||
super.setSelfReference([this.controller, this.target]); | ||
setListBoxOptions() { | ||
/** | ||
@@ -108,10 +103,4 @@ * The target list items. | ||
*/ | ||
this.options = Array.prototype.slice.call(this.target.children, 0); | ||
this.options = Array.from(this.target.children); | ||
/** | ||
* Initialize search. | ||
* @type {Search} | ||
*/ | ||
this.search = new Search(this.options); | ||
/* | ||
@@ -131,2 +120,23 @@ * Set the `option` role for each list itme and ensure each has a unique ID. | ||
/** | ||
* Initialize search. | ||
* | ||
* @type {Search} | ||
*/ | ||
this.search = new Search(this.options); | ||
} | ||
/** | ||
* Set up the component's DOM attributes and event listeners. | ||
*/ | ||
init() { | ||
/* | ||
* A reference to the class instance added to the controller and target | ||
* elements to enable external interactions with this instance. | ||
*/ | ||
super.setSelfReference([this.controller, this.target]); | ||
// Set up initial ListBox options. | ||
this.setListBoxOptions(); | ||
/** | ||
* The initial default state. | ||
@@ -133,0 +143,0 @@ * |
@@ -71,2 +71,7 @@ Listbox | ||
/** | ||
* Collect and configure ListBox options. | ||
*/ | ||
setListBoxOptions(); | ||
/** | ||
* Destroy the Listbox and Popup. | ||
@@ -73,0 +78,0 @@ */ |
@@ -71,2 +71,12 @@ import AriaComponent from '../AriaComponent'; | ||
/** | ||
* Selector used to validate menu items. | ||
* | ||
* This can also be used to exclude items that would otherwise be given a | ||
* "menuitem" role; e.g., `:not(.hidden)`. | ||
* | ||
* @type {string} | ||
*/ | ||
itemMatches: '*', | ||
/** | ||
* Callback to run after the component initializes. | ||
@@ -90,2 +100,3 @@ * | ||
// Bind class methods | ||
this.setMenuItems = this.setMenuItems.bind(this); | ||
this.handleListKeydown = this.handleListKeydown.bind(this); | ||
@@ -103,21 +114,9 @@ this.destroy = this.destroy.bind(this); | ||
*/ | ||
init() { | ||
/* | ||
* A reference to the class instance added to the controller and target | ||
* elements to enable external interactions with this instance. | ||
*/ | ||
super.setSelfReference([this.list]); | ||
/* | ||
* Add the 'menu' role to signify a widget that offers a list of choices to | ||
* the user, such as a set of actions or functions. | ||
*/ | ||
this.list.setAttribute('role', 'menu'); | ||
setMenuItems() { | ||
/** | ||
* The list's child elements. | ||
* The submenu Disclosures. | ||
* | ||
* @type {array} | ||
*/ | ||
this.listItems = Array.prototype.slice.call(this.list.children); | ||
this.disclosures = []; | ||
@@ -129,6 +128,15 @@ /** | ||
*/ | ||
this.menuItems = this.listItems.reduce((acc, item) => { | ||
const itemLink = item.firstElementChild; | ||
this.menuItems = Array.from(this.list.children).reduce((acc, item) => { | ||
const [firstChild, ...theRest] = Array.from(item.children); | ||
if (null !== itemLink && 'A' === itemLink.nodeName) { | ||
// Try to use the first child of the menu item. | ||
let itemLink = firstChild; | ||
// If the first child isn't a link or button, find the first instance of either. | ||
if (null === itemLink || ! itemLink.matches('a,button')) { | ||
[itemLink] = Array.from(theRest) | ||
.filter((child) => child.matches('a,button')); | ||
} | ||
if (undefined !== itemLink && itemLink.matches(this.itemMatches)) { | ||
return [...acc, itemLink]; | ||
@@ -146,21 +154,2 @@ } | ||
/** | ||
* The number of menu items. | ||
* | ||
* @type {number} | ||
*/ | ||
this.menuItemsLength = this.menuItems.length; | ||
/** | ||
* Listen for keydown events on the menu. | ||
*/ | ||
this.list.addEventListener('keydown', this.handleListKeydown); | ||
/** | ||
* The submenu Disclosures. | ||
* | ||
* @type {array} | ||
*/ | ||
this.disclosures = []; | ||
/* | ||
@@ -177,3 +166,3 @@ * Set menu link attributes and instantiate submenus. | ||
// Add size and position attributes. | ||
link.setAttribute('aria-setsize', this.menuItemsLength); | ||
link.setAttribute('aria-setsize', this.menuItems.length); | ||
link.setAttribute('aria-posinset', index + 1); | ||
@@ -194,3 +183,7 @@ | ||
// Instantiate sub-Menus. | ||
const subList = new Menu({ list: siblingList }); | ||
const subList = new Menu({ | ||
list: siblingList, | ||
itemMatches: this.itemMatches, | ||
}); | ||
// Save the list's previous sibling. | ||
@@ -204,3 +197,28 @@ subList.previousSibling = link; | ||
Object.assign(this, { firstItem, lastItem }); | ||
} | ||
/** | ||
* Initialize the Menu. | ||
*/ | ||
init() { | ||
/* | ||
* A reference to the class instance added to the controller and target | ||
* elements to enable external interactions with this instance. | ||
*/ | ||
super.setSelfReference([this.list]); | ||
/* | ||
* Add the 'menu' role to signify a widget that offers a list of choices to | ||
* the user, such as a set of actions or functions. | ||
*/ | ||
this.list.setAttribute('role', 'menu'); | ||
// Set menuitem roles and attributes, including any submenu Disclosures. | ||
this.setMenuItems(); | ||
/** | ||
* Listen for keydown events on the menu. | ||
*/ | ||
this.list.addEventListener('keydown', this.handleListKeydown); | ||
// Run {initCallback} | ||
@@ -207,0 +225,0 @@ this.onInit.call(this); |
@@ -25,2 +25,12 @@ Menu | ||
/** | ||
* Selector used to validate menu items. | ||
* | ||
* This can also be used to exclude items that would otherwise be given a | ||
* "menuitem" role; e.g., `:not(.hidden)`. | ||
* | ||
* @type {string} | ||
*/ | ||
itemMatches: '*', | ||
/** | ||
* Callback to run after the component initializes. | ||
@@ -48,2 +58,9 @@ * | ||
/** | ||
* Set menu items. | ||
* | ||
* Use this if your menu is dynamically updated. | ||
*/ | ||
setMenuItems(); | ||
/** | ||
* Destroy the Menu and any submenus. | ||
@@ -50,0 +67,0 @@ */ |
@@ -77,2 +77,12 @@ import AriaComponent from '../AriaComponent'; | ||
/** | ||
* Selector used to validate menu items. | ||
* | ||
* This can also be used to exclude items that would otherwise be given a | ||
* "menuitem" role; e.g., `:not(.hidden)`. | ||
* | ||
* @type {string} | ||
*/ | ||
itemMatches: '*', | ||
/** | ||
* Callback to run after the component initializes. | ||
@@ -110,2 +120,4 @@ * | ||
// Bind class methods. | ||
this.setMenuBarItems = this.setMenuBarItems.bind(this); | ||
this.setMenuBarSubMenuItems = this.setMenuBarSubMenuItems.bind(this); | ||
this.handleMenuBarKeydown = this.handleMenuBarKeydown.bind(this); | ||
@@ -115,2 +127,3 @@ this.handleMenuBarClick = this.handleMenuBarClick.bind(this); | ||
this.stateWasUpdated = this.stateWasUpdated.bind(this); | ||
this.onPopupStateChange = this.onPopupStateChange.bind(this); | ||
this.destroy = this.destroy.bind(this); | ||
@@ -127,20 +140,4 @@ | ||
*/ | ||
init() { | ||
/* | ||
* A reference to the class instance added to the controller and target | ||
* elements to enable external interactions with this instance. | ||
*/ | ||
super.setSelfReference([this.list]); | ||
// Set the menu role. | ||
this.list.setAttribute('role', 'menubar'); | ||
setMenuBarItems() { | ||
/** | ||
* The menubar's child elements. | ||
* | ||
* @type {array} | ||
*/ | ||
this.menuBarChildren = Array.prototype.slice.call(this.list.children); | ||
/** | ||
* Collected menubar links. | ||
@@ -150,6 +147,15 @@ * | ||
*/ | ||
this.menuBarItems = this.menuBarChildren.reduce((acc, item) => { | ||
const itemLink = item.firstElementChild; | ||
this.menuBarItems = Array.from(this.list.children).reduce((acc, item) => { | ||
const [firstChild, ...theRest] = Array.from(item.children); | ||
if (null !== itemLink && 'A' === itemLink.nodeName) { | ||
// Try to use the first child of the menu item. | ||
let itemLink = firstChild; | ||
// If the first child isn't a link or button, find the first instance of either. | ||
if (null === itemLink || ! itemLink.matches('a,button')) { | ||
[itemLink] = Array.from(theRest) | ||
.filter((child) => child.matches('a,button')); | ||
} | ||
if (undefined !== itemLink && itemLink.matches(this.itemMatches)) { | ||
return [...acc, itemLink]; | ||
@@ -163,2 +169,3 @@ } | ||
* Initialize search. | ||
* | ||
* @type {Search} | ||
@@ -168,9 +175,2 @@ */ | ||
/** | ||
* The number of menubar items. | ||
* | ||
* @type {number} | ||
*/ | ||
this.menuLength = this.menuBarItems.length; | ||
/* | ||
@@ -184,3 +184,3 @@ * Set menubar link attributes. | ||
// Add size and position attributes. | ||
link.setAttribute('aria-setsize', this.menuLength); | ||
link.setAttribute('aria-setsize', this.menuBarItems.length); | ||
link.setAttribute('aria-posinset', index + 1); | ||
@@ -199,14 +199,17 @@ | ||
/** | ||
* A mouse 'click' event. | ||
* | ||
* @type {MouseEvent} | ||
/* | ||
* Set up tabindex. | ||
* The first item, by default, is "allowed", but if any of the menu items is | ||
* active, it should be allowed. | ||
*/ | ||
this.clickEvent = new MouseEvent('click', { | ||
view: window, | ||
bubbles: true, | ||
cancelable: true, | ||
}); | ||
const allowedItem = this.menuBarItems.includes(document.activeElement) | ||
? document.activeElement | ||
: this.firstItem; | ||
rovingTabIndex(this.menuBarItems, allowedItem); | ||
} | ||
// Initialize popups for nested lists. | ||
/** | ||
* Initialize Menus and Popups for nested lists. | ||
*/ | ||
setMenuBarSubMenuItems() { | ||
const { popups, subMenus } = this.menuBarItems.reduce((acc, controller) => { | ||
@@ -224,2 +227,3 @@ const target = controller.nextElementSibling; | ||
onInit: this.onPopupInit, | ||
onStateChange: this.onPopupStateChange, | ||
type: 'menu', | ||
@@ -241,3 +245,3 @@ }); | ||
// Initialize submenu Menus. | ||
const subMenu = new Menu({ list }); | ||
const subMenu = new Menu({ list, itemMatches: this.itemMatches }); | ||
target.addEventListener('keydown', this.handleMenuItemKeydown); | ||
@@ -254,4 +258,35 @@ | ||
Object.assign(this, { popups, subMenus }); | ||
} | ||
/** | ||
* Initialize the Menu. | ||
*/ | ||
init() { | ||
/* | ||
* A reference to the class instance added to the controller and target | ||
* elements to enable external interactions with this instance. | ||
*/ | ||
super.setSelfReference([this.list]); | ||
// Set the menu role. | ||
this.list.setAttribute('role', 'menubar'); | ||
// Set menuitem roles and attributes. | ||
this.setMenuBarItems(); | ||
/** | ||
* A mouse 'click' event. | ||
* | ||
* @type {MouseEvent} | ||
*/ | ||
this.clickEvent = new MouseEvent('click', { | ||
view: window, | ||
bubbles: true, | ||
cancelable: true, | ||
}); | ||
// Initialize Menus and Popups for nested lists. | ||
this.setMenuBarSubMenuItems(); | ||
/** | ||
* Set initial state. | ||
@@ -267,5 +302,2 @@ * | ||
// Set up initial tabindex. | ||
rovingTabIndex(this.menuBarItems, this.firstItem); | ||
// Run {initCallback} | ||
@@ -293,2 +325,11 @@ this.onInit.call(this); | ||
/** | ||
* Track Popup state changes. | ||
* | ||
* @param {boolean} options.expanded The Popup state, | ||
*/ | ||
onPopupStateChange({ expanded }) { | ||
this.setState({ expanded }); | ||
} | ||
/** | ||
* Handle keydown events on the menuList element. | ||
@@ -329,3 +370,3 @@ * | ||
// Close the popup. | ||
if (popup) { | ||
if (false !== popup) { | ||
popup.hide(); | ||
@@ -348,3 +389,3 @@ } | ||
case DOWN: { | ||
if (popup) { | ||
if (false !== popup && event.target === popup.controller) { | ||
event.stopPropagation(); | ||
@@ -351,0 +392,0 @@ event.preventDefault(); |
@@ -19,2 +19,12 @@ MenuBar | ||
/** | ||
* Selector used to validate menu items. | ||
* | ||
* This can also be used to exclude items that would otherwise be given a | ||
* "menuitem" role; e.g., `:not(.hidden)`. | ||
* | ||
* @type {string} | ||
*/ | ||
itemMatches: '*', | ||
/** | ||
* Callback to run after the component initializes. | ||
@@ -63,2 +73,12 @@ * | ||
/** | ||
* Collect top-level menu items and set up event handlers. | ||
*/ | ||
setMenuBarItems(); | ||
/** | ||
* Initialize Menus and Popups for nested lists. | ||
*/ | ||
setMenuBarSubMenuItems(); | ||
/** | ||
* Destroy the MenuBar and any submenu Popups. | ||
@@ -92,7 +112,10 @@ */ | ||
If a menu item has a sibling, that sibling will be turned into a "submenu popup". | ||
If that element is not a UL element, the script will search the popup target with | ||
`Element.querySelector('ul')` and use that as the `list` passed to the `Menu` | ||
component. | ||
The first anchor or button element found in each item will be used as the | ||
`role="menuitem"`. | ||
If a `menuitem` has a `nextElementSibling`, that element will be turned into a | ||
"submenu popup". If a submenu popup's element is not a UL element, the script | ||
will search the popup target with `Element.querySelector('ul')` and use that as | ||
the `list` passed to the `Menu` component. | ||
## Example | ||
@@ -99,0 +122,0 @@ |
@@ -87,2 +87,3 @@ import AriaComponent from '../AriaComponent'; | ||
this.init = this.init.bind(this); | ||
this.setInteractiveChildren = this.setInteractiveChildren.bind(this); | ||
this.stateWasUpdated = this.stateWasUpdated.bind(this); | ||
@@ -116,13 +117,8 @@ this.hide = this.hide.bind(this); | ||
/** | ||
* Set up the component's DOM attributes and event listeners. | ||
* Collect and prepare the target element's interactive child elements. | ||
*/ | ||
init() { | ||
/* | ||
* A reference to the class instance added to the controller and target | ||
* elements to enable external interactions with this instance. | ||
*/ | ||
super.setSelfReference([this.controller, this.target]); | ||
setInteractiveChildren() { | ||
const { expanded } = this.state; | ||
/** | ||
* Collect the target element's interactive child elements. | ||
* The target element's interactive child elements. | ||
* | ||
@@ -133,4 +129,11 @@ * @type {array} | ||
// Focusable content should initially have tabindex='-1'. | ||
tabIndexDeny(this.interactiveChildElements); | ||
/* | ||
* Allow/deny tabbing to interactive children. | ||
* Focusable content should have tabindex='-1' unless the popup is expanded. | ||
*/ | ||
if (expanded) { | ||
tabIndexAllow(this.interactiveChildElements); | ||
} else { | ||
tabIndexDeny(this.interactiveChildElements); | ||
} | ||
@@ -149,3 +152,17 @@ /* | ||
} | ||
} | ||
/** | ||
* Set up the component's DOM attributes and event listeners. | ||
*/ | ||
init() { | ||
/* | ||
* A reference to the class instance added to the controller and target | ||
* elements to enable external interactions with this instance. | ||
*/ | ||
super.setSelfReference([this.controller, this.target]); | ||
// Set up interactive child elements. | ||
this.setInteractiveChildren(); | ||
// Add target attribute. | ||
@@ -152,0 +169,0 @@ setUniqueId(this.target); |
@@ -80,2 +80,7 @@ Popup | ||
/** | ||
* Collect and prepare the target element's interactive child elements. | ||
*/ | ||
setInteractiveChildren(); | ||
/** | ||
* Remove all attributes and event listeners added by this class. | ||
@@ -82,0 +87,0 @@ */ |
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
133384
3313