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 menus
This commit is contained in:
		@@ -43,6 +43,7 @@ class DropdownMenu extends React.PureComponent {
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    document.addEventListener('click', this.handleDocumentClick, false);
 | 
			
		||||
    document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
 | 
			
		||||
    if (this.focusedItem) this.focusedItem.focus();
 | 
			
		||||
    this.setState({ mounted: true });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -55,6 +56,46 @@ class DropdownMenu extends React.PureComponent {
 | 
			
		||||
    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 => {
 | 
			
		||||
    const i = Number(e.currentTarget.getAttribute('data-index'));
 | 
			
		||||
    const { action, to } = this.props.items[i];
 | 
			
		||||
@@ -79,7 +120,7 @@ class DropdownMenu extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <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}
 | 
			
		||||
        </a>
 | 
			
		||||
      </li>
 | 
			
		||||
@@ -156,9 +197,6 @@ export default class Dropdown extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  handleKeyDown = e => {
 | 
			
		||||
    switch(e.key) {
 | 
			
		||||
    case 'Enter':
 | 
			
		||||
      this.handleClick(e);
 | 
			
		||||
      break;
 | 
			
		||||
    case 'Escape':
 | 
			
		||||
      this.handleClose();
 | 
			
		||||
      break;
 | 
			
		||||
 
 | 
			
		||||
@@ -42,22 +42,65 @@ class PrivacyDropdownMenu extends React.PureComponent {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleClick = e => {
 | 
			
		||||
    if (e.key === 'Escape') {
 | 
			
		||||
      this.props.onClose();
 | 
			
		||||
    } else if (!e.key || e.key === 'Enter') {
 | 
			
		||||
      const value = e.currentTarget.getAttribute('data-index');
 | 
			
		||||
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
  handleKeyDown = e => {
 | 
			
		||||
    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.onChange(value);
 | 
			
		||||
      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');
 | 
			
		||||
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
 | 
			
		||||
    this.props.onClose();
 | 
			
		||||
    this.props.onChange(value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    document.addEventListener('click', this.handleDocumentClick, false);
 | 
			
		||||
    document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
 | 
			
		||||
    if (this.focusedItem) this.focusedItem.focus();
 | 
			
		||||
    this.setState({ mounted: true });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -70,6 +113,10 @@ class PrivacyDropdownMenu extends React.PureComponent {
 | 
			
		||||
    this.node = c;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setFocusRef = c => {
 | 
			
		||||
    this.focusedItem = c;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { mounted } = this.state;
 | 
			
		||||
    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
 | 
			
		||||
          // size will be used to determine the coordinate of the menu by
 | 
			
		||||
          // 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 => (
 | 
			
		||||
              <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'>
 | 
			
		||||
                  <i className={`fa fa-fw fa-${item.icon}`} />
 | 
			
		||||
                </div>
 | 
			
		||||
@@ -147,9 +194,6 @@ export default class PrivacyDropdown extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  handleKeyDown = e => {
 | 
			
		||||
    switch(e.key) {
 | 
			
		||||
    case 'Enter':
 | 
			
		||||
      this.handleToggle(e);
 | 
			
		||||
      break;
 | 
			
		||||
    case 'Escape':
 | 
			
		||||
      this.handleClose();
 | 
			
		||||
      break;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user