aria-components
Advanced tools
Comparing version 0.2.0 to 0.3.0
# Change Log | ||
This project adheres to [Semantic Versioning](http://semver.org/). | ||
## 0.3.0 | ||
**Changed** | ||
- Uses `aria-hidden="false"` rather than removing the attribute (#28) | ||
- Uses documented methods for nested classes (4e58d45) | ||
**Added** | ||
- Menu submenus can be instantiated as Disclosures by passing `collapse: true` (#27) | ||
- Uses the `hidden` attribute where `aria-hidden="true"` (#29) | ||
- Documents additional class properties (#34) | ||
- Adds a helper function for getting the first and last item from an Array or NodeList (#35) | ||
**Removed** | ||
- Menu and MenuBar components no longer require the `aria-describedby` help text (#33) | ||
**Fixed** | ||
- Updates NPM dependencies (#25) | ||
- Corrects issues with the reliability of `destroy` methods (#26 & #31) | ||
- Updates NPM dependencies (...again) (#36) | ||
**BREAKING CHANGES** | ||
- MenuBar no longer tracks internal Popup state (51ab17c) | ||
- Corrects ambiguity with native DOM `firstChild` and `lastChild` properties (4795b2a, 1312a99) | ||
## 0.2.0 | ||
@@ -8,7 +37,2 @@ | ||
- BREAKING: Moves helper functions to `utils/` (#17) | ||
- BREAKING: Deprecates the Menu and MenuBar `menu` config property in favor of `list` (#20) | ||
- BREAKING: Deprecates the Tablist `tablist` config property in favor of `tabs` (#20) | ||
- BREAKING: Updates the way the `componentName` and self references are managed (#21) | ||
- BREAKING: Deprecates MenuBar `onPopupStateChange` and `onPopupDestroy` callbacks (#22) | ||
- Improves tracking of internal Popup state (#22) | ||
@@ -25,2 +49,10 @@ | ||
**BREAKING CHANGES** | ||
- Moves helper functions to `utils/` (#17) | ||
- Deprecates the Menu and MenuBar `menu` config property in favor of `list` (#20) | ||
- Deprecates the Tablist `tablist` config property in favor of `tabs` (#20) | ||
- Updates the way the `componentName` and self references are managed (#21) | ||
- Deprecates MenuBar `onPopupStateChange` and `onPopupDestroy` callbacks (#22) | ||
## 0.1.2 | ||
@@ -27,0 +59,0 @@ |
{ | ||
"name": "aria-components", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"description": "JavaScript classes to aid in accessible web development.", | ||
@@ -23,3 +23,3 @@ "keywords": [ | ||
"babel-eslint": "^10.0.3", | ||
"babel-jest": "^24.9.0", | ||
"babel-jest": "^25.5.1", | ||
"babel-loader": "^8.0.4", | ||
@@ -32,3 +32,3 @@ "babel-minify-webpack-plugin": "^0.3.1", | ||
"clean-webpack-plugin": "^1.0.0", | ||
"concurrently": "^4.1.0", | ||
"concurrently": "^5.2.0", | ||
"css-loader": "^1.0.1", | ||
@@ -44,6 +44,6 @@ "eslint": "^5.16.0", | ||
"file-loader": "^2.0.0", | ||
"jest": "^24.7.1", | ||
"jest-cli": "^24.7.1", | ||
"jest": "^25.5.3", | ||
"jest-cli": "^25.5.3", | ||
"mini-css-extract-plugin": "^0.4.5", | ||
"node-sass": "^4.11.0", | ||
"node-sass": "^4.14.0", | ||
"optimize-css-assets-webpack-plugin": "^5.0.1", | ||
@@ -59,8 +59,8 @@ "postcss": "^7.0.6", | ||
"style-loader": "^0.23.1", | ||
"stylelint": "^9.8.0", | ||
"stylelint": "^13.3.3", | ||
"stylelint-webpack-plugin": "^1.0.2", | ||
"url-loader": "^1.1.2", | ||
"webpack": "^4.41.1", | ||
"webpack": "^4.43.0", | ||
"webpack-cli": "^3.3.9", | ||
"webpack-dev-server": "^3.8.2", | ||
"webpack-dev-server": "^3.10.3", | ||
"webpack-livereload-plugin": "^2.1.1", | ||
@@ -67,0 +67,0 @@ "webpack-stats-plugin": "^0.2.1", |
@@ -31,21 +31,2 @@ [![npm version][npmjs-img]][npmjs] [![Build Status][ci-img]][ci] | ||
## Help text elements | ||
> Elements used for keyboard navigation description and referenced on the | ||
element via `aria-labelledby` need to exist in the DOM. | ||
The Menu and MenuBar components reference such elements. As a result, authors | ||
will need to manually add the elements to their page(s). | ||
Examples can be found in the docs directory: | ||
- [docs/\_includes/help-text.html](docs/_includes/help-text.html) | ||
- [docs/\_includes/help-text.php](docs/_includes/help-text.php) | ||
See also the [Menu](src/Menu/) and [Menubar](src/MenuBar/) components' README | ||
Aside from the help text examples above, authors are responsible for adding all | ||
necessary `aria-labelledby`, `aria-label` and `aria-describedby` to the revelant | ||
elements. | ||
**Note**: | ||
@@ -52,0 +33,0 @@ <!-- @todo is this still true? --> |
@@ -5,2 +5,3 @@ import AriaComponent from '../AriaComponent'; | ||
import keyCodes from '../lib/keyCodes'; | ||
import getFirstAndLastItems from '../lib/getFirstAndLastItems'; | ||
@@ -142,2 +143,3 @@ /** | ||
* The Popup instance controlling the Dialog. | ||
* | ||
* @type {Popup} | ||
@@ -157,3 +159,3 @@ */ | ||
*/ | ||
this.interactiveChildren = interactiveChildren(this.target); | ||
this.interactiveChildElements = interactiveChildren(this.target); | ||
@@ -201,10 +203,12 @@ // Add event listeners. | ||
this.interactiveChildren = interactiveChildren(this.target); | ||
this.interactiveChildElements = interactiveChildren(this.target); | ||
if (expanded) { | ||
this.content.setAttribute('aria-hidden', 'true'); | ||
this.content.setAttribute('hidden', ''); | ||
document.body.addEventListener('keydown', this.handleKeydownEsc); | ||
this.close.focus(); | ||
} else { | ||
this.content.removeAttribute('aria-hidden'); | ||
this.content.setAttribute('aria-hidden', 'false'); | ||
this.content.removeAttribute('hidden'); | ||
document.body.removeEventListener('keydown', this.handleKeydownEsc); | ||
@@ -224,3 +228,3 @@ this.controller.focus(); | ||
outsideClick(event) { | ||
const { expanded } = this.popup.getState(); | ||
const { expanded } = this.state; | ||
@@ -240,10 +244,12 @@ if (expanded && ! this.target.contains(event.target)) { | ||
const { keyCode, shiftKey } = event; | ||
const { expanded } = this.state; | ||
if (this.popup.getState().expanded && keyCode === TAB) { | ||
if (expanded && keyCode === TAB) { | ||
const { activeElement } = document; | ||
const lastIndex = this.interactiveChildren.length - 1; | ||
const [firstChild] = this.interactiveChildren; | ||
const lastChild = this.interactiveChildren[lastIndex]; | ||
const [ | ||
firstInteractiveChild, | ||
lastInteractiveChild, | ||
] = getFirstAndLastItems(this.interactiveChildElements); | ||
if (shiftKey && firstChild === activeElement) { | ||
if (shiftKey && firstInteractiveChild === activeElement) { | ||
event.preventDefault(); | ||
@@ -254,4 +260,4 @@ /* | ||
*/ | ||
lastChild.focus(); | ||
} else if (! shiftKey && lastChild === activeElement) { | ||
lastInteractiveChild.focus(); | ||
} else if (! shiftKey && lastInteractiveChild === activeElement) { | ||
event.preventDefault(); | ||
@@ -262,3 +268,3 @@ /* | ||
*/ | ||
firstChild.focus(); | ||
firstInteractiveChild.focus(); | ||
} | ||
@@ -294,5 +300,9 @@ } | ||
// Remove the `aria-hidden` attribute from the content wrapper. | ||
this.content.removeAttribute('aria-hidden'); | ||
// Remove event listeners. | ||
this.close.removeEventListener('click', this.hide); | ||
this.target.removeEventListener('keydown', this.handleTargetKeydown); | ||
document.body.removeEventListener('keydown', this.handleKeydownEsc); | ||
@@ -299,0 +309,0 @@ /* Run {destroyCallback} */ |
@@ -109,2 +109,4 @@ Dialog | ||
* The config.controller property. | ||
* | ||
* @type {HTMLButtonElement} | ||
*/ | ||
@@ -117,2 +119,4 @@ Dialog.controller | ||
* The config.target property. | ||
* | ||
* @type {HTMLElement} | ||
*/ | ||
@@ -125,2 +129,4 @@ Dialog.target | ||
* The config.content property. | ||
* | ||
* @type {HTMLElement} | ||
*/ | ||
@@ -132,2 +138,11 @@ Dialog.content | ||
/** | ||
* The config.close property, or the button created in its absence. | ||
* | ||
* @type {HTMLButtonElement} | ||
*/ | ||
Dialog.close | ||
``` | ||
```javascript | ||
/** | ||
* The Popup instance controlling the Dialog. | ||
@@ -134,0 +149,0 @@ * |
@@ -168,2 +168,3 @@ import AriaComponent from '../AriaComponent'; | ||
this.target.setAttribute('aria-hidden', 'true'); | ||
this.target.setAttribute('hidden', ''); | ||
} | ||
@@ -209,5 +210,7 @@ | ||
if (expanded) { | ||
this.target.removeAttribute('aria-hidden'); | ||
this.target.setAttribute('aria-hidden', 'false'); | ||
this.target.removeAttribute('hidden'); | ||
} else { | ||
this.target.setAttribute('aria-hidden', 'true'); | ||
this.target.setAttribute('hidden', ''); | ||
} | ||
@@ -281,2 +284,9 @@ | ||
// Remove IDs set by this class. | ||
[this.controller, this.target].forEach((element) => { | ||
if (element.getAttribute('id').includes('id_')) { | ||
element.removeAttribute('id'); | ||
} | ||
}); | ||
// Remove controller attributes. | ||
@@ -288,7 +298,16 @@ this.controller.removeAttribute('aria-expanded'); | ||
if ('BUTTON' !== this.controller.nodeName) { | ||
this.controller.removeAttribute('role'); | ||
} | ||
// Remove target attributes. | ||
this.target.removeAttribute('aria-hidden'); | ||
this.target.removeAttribute('hidden'); | ||
// Remove tabindex attributes. | ||
tabIndexAllow(this.interactiveChildElements); | ||
// Remove event listeners. | ||
this.controller.removeEventListener('click', this.toggleExpandedState); | ||
this.controller.removeEventListener('keydown', this.handleControllerKeydown); | ||
document.body.removeEventListener('click', this.closeOnOutsideClick); | ||
@@ -295,0 +314,0 @@ |
@@ -97,2 +97,4 @@ Disclosure | ||
* The config.controller property. | ||
* | ||
* @type {HTMLButtonElement} | ||
*/ | ||
@@ -105,2 +107,4 @@ Disclosure.controller | ||
* The config.target property. | ||
* | ||
* @type {HTMLElement} | ||
*/ | ||
@@ -107,0 +111,0 @@ Disclosure.target |
@@ -6,2 +6,3 @@ import AriaComponent from '../AriaComponent'; | ||
import Search from '../lib/Search'; | ||
import getFirstAndLastItems from '../lib/getFirstAndLastItems'; | ||
@@ -123,17 +124,4 @@ /** | ||
/** | ||
* First [role="option"] | ||
* | ||
* @type {HTMLElement} | ||
*/ | ||
const [firstOption] = this.options; | ||
/** | ||
* Last [role="option"] | ||
* | ||
* @type {HTMLElement} | ||
*/ | ||
const lastOption = this.options[this.options.length - 1]; | ||
// Save first and last option as properties. | ||
const [ firstOption, lastOption ] = getFirstAndLastItems(this.options); | ||
Object.assign(this, { firstOption, lastOption }); | ||
@@ -407,2 +395,3 @@ | ||
handleTargetBlur() { | ||
// Use Popup state here, since the Popup drives the Listbox state. | ||
if (this.popup.getState().expanded) { | ||
@@ -445,2 +434,8 @@ this.hide(); | ||
listItem.removeAttribute('role'); | ||
listItem.removeAttribute('aria-selected'); | ||
// Remove IDs set by this class. | ||
if (listItem.getAttribute('id').includes('id_')) { | ||
listItem.removeAttribute('id'); | ||
} | ||
}); | ||
@@ -454,2 +449,3 @@ | ||
this.target.removeAttribute('tabindex'); | ||
this.target.removeAttribute('aria-activedescendant'); | ||
@@ -456,0 +452,0 @@ // Remove event listeners. |
@@ -82,2 +82,4 @@ Listbox | ||
* The config.controller property. | ||
* | ||
* @type {HTMLButtonElement} | ||
*/ | ||
@@ -90,2 +92,4 @@ ListBox.controller | ||
* The config.target property. | ||
* | ||
* @type {HTMLUListElement} | ||
*/ | ||
@@ -97,2 +101,29 @@ ListBox.target | ||
/** | ||
* The target list items. | ||
* | ||
* @type {array} | ||
*/ | ||
Listbox.options | ||
``` | ||
```javascript | ||
/** | ||
* The first Listbox option. | ||
* | ||
* @type {HTMLLIElement} | ||
*/ | ||
ListBox.firstOption | ||
``` | ||
```javascript | ||
/** | ||
* The last Listbox option. | ||
* | ||
* @type {HTMLLIElement} | ||
*/ | ||
ListBox.lastOption | ||
``` | ||
```javascript | ||
/** | ||
* The Popup instance controlling the ListBox. | ||
@@ -99,0 +130,0 @@ * |
import AriaComponent from '../AriaComponent'; | ||
import Disclosure from '../Disclosure'; | ||
import keyCodes from '../lib/keyCodes'; | ||
import isInstanceOf from '../lib/isInstanceOf'; | ||
import { nextPreviousFromUpDown } from '../lib/nextPrevious'; | ||
import { missingDescribedByWarning } from '../lib/ariaDescribedbyElementsFound'; | ||
import Search from '../lib/Search'; | ||
import getFirstAndLastItems from '../lib/getFirstAndLastItems'; | ||
@@ -15,16 +16,2 @@ /** | ||
/** | ||
* HTML IDs for elements containing help text. | ||
* | ||
* @return {array} | ||
*/ | ||
static getHelpIds() { | ||
return [ | ||
'#ac-describe-submenu-help', | ||
'#ac-describe-esc-help', | ||
'#ac-describe-submenu-explore', | ||
'#ac-describe-submenu-back', | ||
]; | ||
} | ||
/** | ||
* Test for a list as the next sibling element. | ||
@@ -78,2 +65,9 @@ * | ||
/** | ||
* Instantiate submenus as Disclosures. | ||
* | ||
* @type {Boolean} | ||
*/ | ||
collapse: false, | ||
/** | ||
* Callback to run after the component initializes. | ||
@@ -162,8 +156,8 @@ * | ||
/* | ||
* Warn if aria-decribedby elements are not found. | ||
* Without these elements, the references will be broken and potentially | ||
* confusing to users. | ||
/** | ||
* The submenu Disclosures. | ||
* | ||
* @type {array} | ||
*/ | ||
missingDescribedByWarning(Menu.getHelpIds()); | ||
this.disclosures = []; | ||
@@ -180,8 +174,2 @@ /* | ||
link.setAttribute( | ||
'aria-describedby', | ||
// eslint-disable-next-line max-len | ||
'ac-describe-submenu-explore ac-describe-submenu-help ac-describe-submenu-back ac-describe-esc-help' | ||
); | ||
// Add size and position attributes. | ||
@@ -193,2 +181,13 @@ link.setAttribute('aria-setsize', this.menuItemsLength); | ||
if (siblingList) { | ||
// Instantiate submenu Disclosures | ||
if (this.collapse) { | ||
const disclosure = new Disclosure({ | ||
controller: link, | ||
target: siblingList, | ||
}); | ||
this.disclosures.push(disclosure); | ||
} | ||
// Instantiate sub-Menus. | ||
const subList = new Menu({ list: siblingList }); | ||
@@ -201,4 +200,3 @@ // Save the list's previous sibling. | ||
// Save the menu's first and last items. | ||
const [firstItem] = this.menuItems; | ||
const lastItem = this.menuItems[this.menuItems.length - 1]; | ||
const [ firstItem, lastItem ] = getFirstAndLastItems(this.menuItems); | ||
Object.assign(this, { firstItem, lastItem }); | ||
@@ -285,2 +283,7 @@ | ||
// Open the submenu Disclosure. | ||
if (isInstanceOf(activeDescendant.disclosure, Disclosure)) { | ||
activeDescendant.disclosure.open(); | ||
} | ||
const { menu } = siblingElement; | ||
@@ -304,2 +307,8 @@ menu.firstItem.focus(); | ||
event.stopPropagation(); | ||
// Close the submenu Disclosure. | ||
if (isInstanceOf(this.previousSibling.disclosure, Disclosure)) { | ||
this.previousSibling.disclosure.close(); | ||
} | ||
this.previousSibling.focus(); | ||
@@ -343,2 +352,8 @@ } | ||
// Remove the list's role attritbute. | ||
this.list.removeAttribute('role'); | ||
// Remove event listener. | ||
this.list.removeEventListener('keydown', this.handleListKeydown); | ||
this.menuItems.forEach((link) => { | ||
@@ -350,9 +365,5 @@ // Remove list item role. | ||
link.removeAttribute('role'); | ||
link.removeAttribute('aria-describedby'); | ||
link.removeAttribute('aria-setsize'); | ||
link.removeAttribute('aria-posinset'); | ||
// Remove event listeners. | ||
link.removeEventListener('keydown', this.handleListKeydown); | ||
// Destroy nested Menus. | ||
@@ -365,2 +376,7 @@ const siblingList = this.constructor.nextElementIsUl(link); | ||
// Destroy inner Disclosure(s). | ||
this.disclosures.forEach((disclosure) => { | ||
disclosure.destroy(); | ||
}); | ||
// Run {destroyCallback} | ||
@@ -367,0 +383,0 @@ this.onDestroy.call(this); |
@@ -18,2 +18,9 @@ Menu | ||
/** | ||
* Instantiate submenus as Disclosures. | ||
* | ||
* @type {Boolean} | ||
*/ | ||
collapse: false, | ||
/** | ||
* Callback to run after the component initializes. | ||
@@ -56,2 +63,9 @@ * | ||
Menu.menu | ||
/** | ||
* The submenu Disclosures. | ||
* | ||
* @type {array} | ||
*/ | ||
Menu.disclosures | ||
``` | ||
@@ -82,14 +96,2 @@ | ||
</ul> | ||
<!-- | ||
These elements are required by this component, but must be added manually. | ||
Feel free to update the text how you see fit, but make sure it's helpful and | ||
the elements have the correct `id` attribute. | ||
--> | ||
<div class="screen-reader-only"> | ||
<span id="ac-describe-submenu-help">Use right arrow key to move into submenus.</span> | ||
<span id="ac-describe-esc-help">Use escape to exit the menu.</span> | ||
<span id="ac-describe-submenu-explore">Use up and down arrow keys to explore.</span> | ||
<span id="ac-describe-submenu-back">Use left arrow key to move back to the parent list.</span> | ||
</div> | ||
``` | ||
@@ -104,2 +106,3 @@ | ||
list, | ||
collapse: true, | ||
onInit: () => { | ||
@@ -106,0 +109,0 @@ console.log('Menu initialized.'); |
@@ -8,4 +8,4 @@ import AriaComponent from '../AriaComponent'; | ||
import isInstanceOf from '../lib/isInstanceOf'; | ||
import { missingDescribedByWarning } from '../lib/ariaDescribedbyElementsFound'; | ||
import Search from '../lib/Search'; | ||
import getFirstAndLastItems from '../lib/getFirstAndLastItems'; | ||
@@ -33,15 +33,2 @@ /** | ||
/** | ||
* HTML IDs for elements containing help text | ||
* | ||
* @return {array} | ||
*/ | ||
static getHelpIds() { | ||
return [ | ||
'#ac-describe-top-level-help', | ||
'#ac-describe-submenu-help', | ||
'#ac-describe-esc-help', | ||
]; | ||
} | ||
/** | ||
* Create a MenuBar. | ||
@@ -128,3 +115,2 @@ * @constructor | ||
this.stateWasUpdated = this.stateWasUpdated.bind(this); | ||
this.trackPopupState = this.trackPopupState.bind(this); | ||
this.destroy = this.destroy.bind(this); | ||
@@ -187,9 +173,2 @@ | ||
/* | ||
* Warn if aria-decribedby elements are not found. | ||
* Without these elements, the references will be broken and potentially | ||
* confusing to users. | ||
*/ | ||
missingDescribedByWarning(MenuBar.getHelpIds()); | ||
/* | ||
* Set menubar link attributes. | ||
@@ -201,9 +180,2 @@ */ | ||
// Add a reference to the help text. | ||
link.setAttribute( | ||
'aria-describedby', | ||
// eslint-disable-next-line max-len | ||
'ac-describe-top-level-help ac-describe-submenu-help ac-describe-esc-help' | ||
); | ||
// Add size and position attributes. | ||
@@ -220,8 +192,5 @@ link.setAttribute('aria-setsize', this.menuLength); | ||
/** | ||
* The index of the last menubar item | ||
* | ||
* @type {number} | ||
*/ | ||
this.lastIndex = (this.menuLength - 1); | ||
// Collect first and last MenuBar items and merge them in as instance properties. | ||
const [ firstItem, lastItem ] = getFirstAndLastItems(this.menuBarItems); | ||
Object.assign(this, { firstItem, lastItem }); | ||
@@ -248,3 +217,2 @@ /** | ||
onInit: this.onPopupInit, | ||
onStateChange: this.trackPopupState, | ||
type: 'menu', | ||
@@ -271,6 +239,5 @@ }); | ||
*/ | ||
const [menubarItem] = this.menuBarItems; | ||
this.state = { | ||
menubarItem, | ||
popup: this.constructor.getPopupFromMenubarItem(menubarItem), | ||
menubarItem: this.firstItem, | ||
popup: this.constructor.getPopupFromMenubarItem(this.firstItem), | ||
expanded: false, | ||
@@ -280,3 +247,3 @@ }; | ||
// Set up initial tabindex. | ||
rovingTabIndex(this.menuBarItems, menubarItem); | ||
rovingTabIndex(this.menuBarItems, this.firstItem); | ||
@@ -288,23 +255,2 @@ // Run {initCallback} | ||
/** | ||
* Refresh component state when Popup state is updated. | ||
* | ||
* @param {object} state The Popup state. | ||
*/ | ||
trackPopupState(state = {}) { | ||
const { menubarItem } = this.state; | ||
const popup = this.constructor.getPopupFromMenubarItem(menubarItem); | ||
/* | ||
* Use the current MenuBar state if there's no popup or if an expanded state | ||
* was passed in, otherwise make sure to use the current popup's state. | ||
*/ | ||
const expanded = ( | ||
false === popup | ||
|| Object.prototype.hasOwnProperty.call(state, 'expanded') | ||
) ? state.expanded : popup.getState(); | ||
// Add the Popup state to this component's state. | ||
this.state = Object.assign({ menubarItem, popup, expanded }); | ||
} | ||
/** | ||
* Manage menubar state. | ||
@@ -317,5 +263,2 @@ * | ||
// Make sure we're tracking the Popup state along with this. | ||
this.trackPopupState(); | ||
// Prevent tabbing to all but the currently-active menubar item. | ||
@@ -346,3 +289,4 @@ rovingTabIndex(this.menuBarItems, menubarItem); | ||
const { keyCode } = event; | ||
const { menubarItem, popup } = this.state; | ||
const { menubarItem } = this.state; | ||
const popup = this.constructor.getPopupFromMenubarItem(menubarItem); | ||
@@ -367,3 +311,3 @@ switch (keyCode) { | ||
if (popup) { | ||
popup.setState({ expanded: false }); | ||
popup.hide(); | ||
} | ||
@@ -390,6 +334,6 @@ | ||
if (! popup.state.expanded) { | ||
popup.setState({ expanded: true }); | ||
popup.show(); | ||
} | ||
popup.firstChild.focus(); | ||
popup.firstInteractiveChild.focus(); | ||
} | ||
@@ -419,3 +363,3 @@ | ||
this.setState({ | ||
menubarItem: this.menuBarItems[this.lastIndex], | ||
menubarItem: this.lastItem, | ||
}); | ||
@@ -484,5 +428,2 @@ | ||
// Remove reference to the help text. | ||
link.removeAttribute('aria-describedby'); | ||
// Remove size and position attributes. | ||
@@ -501,2 +442,5 @@ link.removeAttribute('aria-setsize'); | ||
// Remove tabindex attribute. | ||
tabIndexAllow(this.menuBarItems); | ||
// Destroy nested components. | ||
@@ -507,8 +451,7 @@ this.popups.forEach((popup) => { | ||
} | ||
popup.target.removeEventListener('keydown', this.handleMenuItemKeydown); | ||
popup.destroy(); | ||
}); | ||
// Revert tabindex attributes. | ||
tabIndexAllow(this.menuBarItems); | ||
// Run {destroyCallback} | ||
@@ -515,0 +458,0 @@ this.onDestroy.call(this); |
@@ -79,2 +79,11 @@ MenuBar | ||
```javascript | ||
/** | ||
* Collected menubar links. | ||
* | ||
* @type {array} | ||
*/ | ||
MenuBar.menuBarItems | ||
``` | ||
## Example | ||
@@ -105,13 +114,2 @@ | ||
</ul> | ||
<!-- | ||
These elements are required by this component, but must be added manually. | ||
Feel free to update the text how you see fit, but make sure it's helpful and | ||
the elements have the correct `id` attribute. | ||
--> | ||
<div class="screen-reader-only"> | ||
<span id="ac-describe-top-level-help">Use left and right arrow keys to navigate between menu items.</span> | ||
<span id="ac-describe-submenu-help">Use right arrow key to move into submenus.</span> | ||
<span id="ac-describe-esc-help">Use escape to exit the menu.</span> | ||
</div> | ||
``` | ||
@@ -118,0 +116,0 @@ |
@@ -208,2 +208,5 @@ import AriaComponent from '../AriaComponent'; | ||
// Remove event listener. | ||
this.controller.removeEventListener('keydown', this.handleControllerKeydown); | ||
// Run {destroyCallback} | ||
@@ -210,0 +213,0 @@ this.onDestroy.call(this); |
@@ -90,2 +90,4 @@ MenuButton | ||
* The config.controller property. | ||
* | ||
* @type {HTMLButtonElement} | ||
*/ | ||
@@ -98,2 +100,4 @@ MenuButton.controller | ||
* The config.target property. | ||
* | ||
* @type {HTMLElement} | ||
*/ | ||
@@ -106,2 +110,4 @@ MenuButton.target | ||
* The config.list property. | ||
* | ||
* @type {HTMLUListElement} | ||
*/ | ||
@@ -108,0 +114,0 @@ MenuButton.list |
@@ -6,2 +6,3 @@ import AriaComponent from '../AriaComponent'; | ||
import { setUniqueId } from '../lib/uniqueId'; | ||
import getFirstAndLastItems from '../lib/getFirstAndLastItems'; | ||
@@ -97,2 +98,13 @@ /** | ||
/** | ||
* Check if the controller is a button, but only if it doesn't already have | ||
* a role attribute, since we'll be adding the role and allowing focus. | ||
* | ||
* @type {bool} | ||
*/ | ||
this.controllerIsNotAButton = ( | ||
'BUTTON' !== this.controller.nodeName | ||
&& null === this.controller.getAttribute('role') | ||
); | ||
// Check for a valid controller and target before initializing. | ||
@@ -129,8 +141,8 @@ if (null !== this.controller && null !== this.target) { | ||
if (0 < this.interactiveChildElements.length) { | ||
const [firstChild] = this.interactiveChildElements; | ||
const lastChild = ( | ||
this.interactiveChildElements[this.interactiveChildElements.length - 1] | ||
); | ||
const [ | ||
firstInteractiveChild, | ||
lastInteractiveChild, | ||
] = getFirstAndLastItems(this.interactiveChildElements); | ||
Object.assign(this, { firstChild, lastChild }); | ||
Object.assign(this, { firstInteractiveChild, lastInteractiveChild }); | ||
} | ||
@@ -149,8 +161,4 @@ | ||
* Use the button role on non-button elements. | ||
* But only if it doesn't already have a role attribute. | ||
*/ | ||
if ( | ||
'BUTTON' !== this.controller.nodeName | ||
&& null === this.controller.getAttribute('role') | ||
) { | ||
if (this.controllerIsNotAButton) { | ||
// https://www.w3.org/TR/wai-aria-1.1/#button | ||
@@ -175,2 +183,3 @@ this.controller.setAttribute('role', 'button'); | ||
this.target.setAttribute('aria-hidden', 'true'); | ||
this.target.setAttribute('hidden', ''); | ||
@@ -198,10 +207,7 @@ // Add event listeners | ||
/* | ||
* https://developer.paciellogroup.com/blog/2016/01/the-state-of-hidden-content-support-in-2016/ | ||
* | ||
* > In some browser and screen reader combinations aria-hidden=false on an | ||
* element that is hidden using the hidden attribute or CSS display:none | ||
* results in the content being unhidden. | ||
* Update Popup and interactive children's attributes. | ||
*/ | ||
if (expanded) { | ||
this.target.removeAttribute('aria-hidden'); | ||
this.target.setAttribute('aria-hidden', 'false'); | ||
this.target.removeAttribute('hidden'); | ||
@@ -211,2 +217,3 @@ tabIndexAllow(this.interactiveChildElements); | ||
this.target.setAttribute('aria-hidden', 'true'); | ||
this.target.setAttribute('hidden', ''); | ||
@@ -261,3 +268,3 @@ // Focusable content should have tabindex='-1' or be removed from the DOM. | ||
*/ | ||
this.firstChild.focus(); | ||
this.firstInteractiveChild.focus(); | ||
} | ||
@@ -294,3 +301,3 @@ } | ||
if (shiftKey) { | ||
if ([this.firstChild, this.target].includes(activeElement)) { | ||
if ([this.firstInteractiveChild, this.target].includes(activeElement)) { | ||
event.preventDefault(); | ||
@@ -304,3 +311,3 @@ /* | ||
} | ||
} else if (this.lastChild === activeElement) { | ||
} else if (this.lastInteractiveChild === activeElement) { | ||
/* | ||
@@ -368,2 +375,9 @@ * Close the Popup when tabbing from the last child. | ||
// Remove IDs set by this class. | ||
[this.controller, this.target].forEach((element) => { | ||
if (element.getAttribute('id').includes('id_')) { | ||
element.removeAttribute('id'); | ||
} | ||
}); | ||
// Remove controller attributes. | ||
@@ -375,5 +389,15 @@ this.controller.removeAttribute('aria-haspopup'); | ||
// Remove role and tabindex added to a link controller. | ||
if (this.controllerIsNotAButton) { | ||
this.controller.removeAttribute('role'); | ||
this.controller.removeAttribute('tabindex'); | ||
} | ||
// Remove target attributes. | ||
this.target.removeAttribute('aria-hidden'); | ||
this.target.removeAttribute('hidden'); | ||
// Remove tabindex attribute. | ||
tabIndexAllow(this.interactiveChildElements); | ||
// Remove event listeners. | ||
@@ -380,0 +404,0 @@ this.controller.removeEventListener('click', this.controllerClickHandler); |
@@ -14,3 +14,3 @@ Popup | ||
* | ||
* @type {HTMLElement} | ||
* @type {HTMLButtonElement} | ||
*/ | ||
@@ -92,2 +92,4 @@ controller: null, | ||
* The config.controller property. | ||
* | ||
* @type {HTMLButtonElement} | ||
*/ | ||
@@ -100,4 +102,20 @@ Popup.controller | ||
* The config.target property. | ||
* | ||
* @type {HTMLElement} | ||
*/ | ||
Popup.target | ||
/** | ||
* The target's first interactive child element. | ||
* | ||
* @type {HTMLElement} | ||
*/ | ||
Popup.firstInteractiveChild | ||
/** | ||
* The target's last interactive child element. | ||
* | ||
* @type {HTMLElement} | ||
*/ | ||
Popup.lastInteractiveChild | ||
``` | ||
@@ -104,0 +122,0 @@ |
@@ -202,4 +202,7 @@ import AriaComponent from '../AriaComponent'; | ||
panel.setAttribute('tabindex', '0'); | ||
panel.setAttribute('aria-hidden', 'false'); | ||
panel.removeAttribute('hidden'); | ||
} else { | ||
panel.setAttribute('aria-hidden', 'true'); | ||
panel.setAttribute('hidden', ''); | ||
} | ||
@@ -212,3 +215,3 @@ | ||
// Save the active panel's interactive children. | ||
this.interactiveChildren = interactiveChildren(this.panels[activeIndex]); | ||
this.interactiveChildElements = interactiveChildren(this.panels[activeIndex]); | ||
@@ -239,2 +242,3 @@ // Run {initCallback} | ||
this.panels[deactiveIndex].setAttribute('aria-hidden', 'true'); | ||
this.panels[deactiveIndex].setAttribute('hidden', ''); | ||
this.panels[deactiveIndex].removeAttribute('tabindex'); | ||
@@ -249,8 +253,9 @@ | ||
this.tabLinks[activeIndex].setAttribute('aria-selected', 'true'); | ||
this.panels[activeIndex].removeAttribute('aria-hidden'); | ||
this.panels[activeIndex].setAttribute('aria-hidden', 'false'); | ||
this.panels[activeIndex].removeAttribute('hidden'); | ||
this.panels[activeIndex].setAttribute('tabindex', '0'); | ||
// Allow tabbing to the newly-active panel. | ||
this.interactiveChildren = interactiveChildren(this.panels[activeIndex]); | ||
tabIndexAllow(this.interactiveChildren); | ||
this.interactiveChildElements = interactiveChildren(this.panels[activeIndex]); | ||
tabIndexAllow(this.interactiveChildElements); | ||
@@ -271,3 +276,3 @@ // Run {stateChangeCallback} | ||
const { activeElement } = document; | ||
const [firstChild] = this.interactiveChildren; | ||
const [firstInteractiveChild] = this.interactiveChildElements; | ||
@@ -278,3 +283,3 @@ if (keyCode === TAB && shiftKey) { | ||
this.tabLinks[activeIndex].focus(); | ||
} else if (activeElement === firstChild) { | ||
} else if (activeElement === firstInteractiveChild) { | ||
/* | ||
@@ -441,3 +446,5 @@ * Ensure navigating with Shift-TAB from the first interactive child of | ||
panel.removeAttribute('aria-hidden'); | ||
panel.removeAttribute('hidden'); | ||
panel.removeAttribute('tabindex'); | ||
panel.removeAttribute('aria-labelledby'); | ||
@@ -444,0 +451,0 @@ // Make sure to allow tabbing to all children of all panels. |
@@ -14,3 +14,3 @@ Tablist | ||
* | ||
* @type {HTMLElement} | ||
* @type {HTMLUListElement} | ||
*/ | ||
@@ -80,5 +80,7 @@ tabs: null, | ||
/** | ||
* The config.tablist property. | ||
* The config.tabs property. | ||
* | ||
* @type {HTMLUListElement} | ||
*/ | ||
Tablist.tablist | ||
Tablist.tabs | ||
``` | ||
@@ -89,2 +91,4 @@ | ||
* The config.panels property. | ||
* | ||
* @type {array} | ||
*/ | ||
@@ -94,2 +98,10 @@ Tablist.panels | ||
```javascript | ||
/** | ||
* Collected anchors from inside of each list items. | ||
* | ||
* @type {array} | ||
*/ | ||
Tablist.tabLinks | ||
``` | ||
@@ -96,0 +108,0 @@ ## Example |
@@ -9,2 +9,3 @@ /** | ||
import Search from '../src/lib/Search'; | ||
import getFirstAndLastItems from '../src/lib/getFirstAndLastItems'; | ||
@@ -20,2 +21,3 @@ export { | ||
Search, | ||
getFirstAndLastItems, | ||
}; |
@@ -69,3 +69,3 @@ src/lib/ | ||
const newId = getUniqueId(button); // 'id_9y0541qs1tk' | ||
button.id = getUniqueId(); // 'id_9y0541qs1tk' | ||
``` | ||
@@ -92,1 +92,16 @@ | ||
``` | ||
## `getFirstAndLastItems` | ||
```javascript | ||
import { getFirstAndLastItems } from 'aria-components/utils'; | ||
const listItems = document.querySelectorAll('li'); | ||
// Pass a NodeList. | ||
const [firstItem, lastItem] = getFirstAndLastItems(listItems); | ||
// Pass an Array. | ||
const listItemsArray = Array.from(listItems); | ||
const [firstItem, lastItem] = getFirstAndLastItems(listItemsArray); | ||
``` |
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
125193
3108
51