Dropdowns accessibility (#7318)
* Mark currently selected privacy setting in privacy dropdown * Prevent Enter keypresses from triggering dropdown display toggle twice * Give focus to first/selected item of dropdown menus * Implement keyboard navigation in privacy dropdown * Implement keyboard navigation in generic dropdown menusgh/stable
parent
6793bec4c6
commit
ef7d64c801
|
@ -43,6 +43,7 @@ class DropdownMenu extends React.PureComponent {
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
document.addEventListener('click', this.handleDocumentClick, false);
|
document.addEventListener('click', this.handleDocumentClick, false);
|
||||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
|
if (this.focusedItem) this.focusedItem.focus();
|
||||||
this.setState({ mounted: true });
|
this.setState({ mounted: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,6 +56,46 @@ class DropdownMenu extends React.PureComponent {
|
||||||
this.node = c;
|
this.node = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setFocusRef = c => {
|
||||||
|
this.focusedItem = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown = e => {
|
||||||
|
const items = Array.from(this.node.getElementsByTagName('a'));
|
||||||
|
const index = items.indexOf(e.currentTarget);
|
||||||
|
let element;
|
||||||
|
|
||||||
|
switch(e.key) {
|
||||||
|
case 'Enter':
|
||||||
|
this.handleClick(e);
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
element = items[index+1];
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
element = items[index-1];
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Home':
|
||||||
|
element = items[0];
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'End':
|
||||||
|
element = items[items.length-1];
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleClick = e => {
|
handleClick = e => {
|
||||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||||
const { action, to } = this.props.items[i];
|
const { action, to } = this.props.items[i];
|
||||||
|
@ -79,7 +120,7 @@ class DropdownMenu extends React.PureComponent {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className='dropdown-menu__item' key={`${text}-${i}`}>
|
<li className='dropdown-menu__item' key={`${text}-${i}`}>
|
||||||
<a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' autoFocus={i === 0} onClick={this.handleClick} data-index={i}>
|
<a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyDown={this.handleKeyDown} data-index={i}>
|
||||||
{text}
|
{text}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -156,9 +197,6 @@ export default class Dropdown extends React.PureComponent {
|
||||||
|
|
||||||
handleKeyDown = e => {
|
handleKeyDown = e => {
|
||||||
switch(e.key) {
|
switch(e.key) {
|
||||||
case 'Enter':
|
|
||||||
this.handleClick(e);
|
|
||||||
break;
|
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
this.handleClose();
|
this.handleClose();
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -42,10 +42,53 @@ class PrivacyDropdownMenu extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClick = e => {
|
handleKeyDown = e => {
|
||||||
if (e.key === 'Escape') {
|
const { items } = this.props;
|
||||||
|
const value = e.currentTarget.getAttribute('data-index');
|
||||||
|
const index = items.findIndex(item => {
|
||||||
|
return (item.value === value);
|
||||||
|
});
|
||||||
|
let element;
|
||||||
|
|
||||||
|
switch(e.key) {
|
||||||
|
case 'Escape':
|
||||||
this.props.onClose();
|
this.props.onClose();
|
||||||
} else if (!e.key || e.key === 'Enter') {
|
break;
|
||||||
|
case 'Enter':
|
||||||
|
this.handleClick(e);
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
element = this.node.childNodes[index + 1];
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
this.props.onChange(element.getAttribute('data-index'));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
element = this.node.childNodes[index - 1];
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
this.props.onChange(element.getAttribute('data-index'));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Home':
|
||||||
|
element = this.node.firstChild;
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
this.props.onChange(element.getAttribute('data-index'));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'End':
|
||||||
|
element = this.node.lastChild;
|
||||||
|
if (element) {
|
||||||
|
element.focus();
|
||||||
|
this.props.onChange(element.getAttribute('data-index'));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick = e => {
|
||||||
const value = e.currentTarget.getAttribute('data-index');
|
const value = e.currentTarget.getAttribute('data-index');
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -53,11 +96,11 @@ class PrivacyDropdownMenu extends React.PureComponent {
|
||||||
this.props.onClose();
|
this.props.onClose();
|
||||||
this.props.onChange(value);
|
this.props.onChange(value);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
document.addEventListener('click', this.handleDocumentClick, false);
|
document.addEventListener('click', this.handleDocumentClick, false);
|
||||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||||
|
if (this.focusedItem) this.focusedItem.focus();
|
||||||
this.setState({ mounted: true });
|
this.setState({ mounted: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +113,10 @@ class PrivacyDropdownMenu extends React.PureComponent {
|
||||||
this.node = c;
|
this.node = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setFocusRef = c => {
|
||||||
|
this.focusedItem = c;
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { mounted } = this.state;
|
const { mounted } = this.state;
|
||||||
const { style, items, value } = this.props;
|
const { style, items, value } = this.props;
|
||||||
|
@ -80,9 +127,9 @@ class PrivacyDropdownMenu extends React.PureComponent {
|
||||||
// It should not be transformed when mounting because the resulting
|
// It should not be transformed when mounting because the resulting
|
||||||
// size will be used to determine the coordinate of the menu by
|
// size will be used to determine the coordinate of the menu by
|
||||||
// react-overlays
|
// react-overlays
|
||||||
<div className='privacy-dropdown__dropdown' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}>
|
<div className='privacy-dropdown__dropdown' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} role='listbox' ref={this.setRef}>
|
||||||
{items.map(item => (
|
{items.map(item => (
|
||||||
<div role='button' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleClick} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })}>
|
<div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
|
||||||
<div className='privacy-dropdown__option__icon'>
|
<div className='privacy-dropdown__option__icon'>
|
||||||
<i className={`fa fa-fw fa-${item.icon}`} />
|
<i className={`fa fa-fw fa-${item.icon}`} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -147,9 +194,6 @@ export default class PrivacyDropdown extends React.PureComponent {
|
||||||
|
|
||||||
handleKeyDown = e => {
|
handleKeyDown = e => {
|
||||||
switch(e.key) {
|
switch(e.key) {
|
||||||
case 'Enter':
|
|
||||||
this.handleToggle(e);
|
|
||||||
break;
|
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
this.handleClose();
|
this.handleClose();
|
||||||
break;
|
break;
|
||||||
|
|
Reference in New Issue