Improve dropdown menu keyboard navigation (#11491)
* Allow selecting menu items with the space bar in status dropdown menus * Fix modals opened by keyboard navigation being immediately closed * Fix menu items triggering modal actions * Add Tab trapping inside dropdown menu * Give focus back to last focused element when status dropdown menu closes
This commit is contained in:
		| @ -9,8 +9,9 @@ export function openModal(type, props) { | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| export function closeModal() { | ||||
| export function closeModal(type) { | ||||
|   return { | ||||
|     type: MODAL_CLOSE, | ||||
|     modalType: type, | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| @ -45,7 +45,10 @@ class DropdownMenu extends React.PureComponent { | ||||
|     document.addEventListener('click', this.handleDocumentClick, false); | ||||
|     document.addEventListener('keydown', this.handleKeyDown, false); | ||||
|     document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); | ||||
|     if (this.focusedItem && this.props.openedViaKeyboard) this.focusedItem.focus(); | ||||
|     this.activeElement = document.activeElement; | ||||
|     if (this.focusedItem && this.props.openedViaKeyboard) { | ||||
|       this.focusedItem.focus(); | ||||
|     } | ||||
|     this.setState({ mounted: true }); | ||||
|   } | ||||
|  | ||||
| @ -53,6 +56,9 @@ class DropdownMenu extends React.PureComponent { | ||||
|     document.removeEventListener('click', this.handleDocumentClick, false); | ||||
|     document.removeEventListener('keydown', this.handleKeyDown, false); | ||||
|     document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); | ||||
|     if (this.activeElement) { | ||||
|       this.activeElement.focus(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   setRef = c => { | ||||
| @ -81,6 +87,18 @@ class DropdownMenu extends React.PureComponent { | ||||
|         element.focus(); | ||||
|       } | ||||
|       break; | ||||
|     case 'Tab': | ||||
|       if (e.shiftKey) { | ||||
|         element = items[index-1] || items[items.length-1]; | ||||
|       } else { | ||||
|         element = items[index+1] || items[0]; | ||||
|       } | ||||
|       if (element) { | ||||
|         element.focus(); | ||||
|         e.preventDefault(); | ||||
|         e.stopPropagation(); | ||||
|       } | ||||
|       break; | ||||
|     case 'Home': | ||||
|       element = items[0]; | ||||
|       if (element) { | ||||
| @ -93,11 +111,14 @@ class DropdownMenu extends React.PureComponent { | ||||
|         element.focus(); | ||||
|       } | ||||
|       break; | ||||
|     case 'Escape': | ||||
|       this.props.onClose(); | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   handleItemKeyDown = e => { | ||||
|     if (e.key === 'Enter') { | ||||
|   handleItemKeyUp = e => { | ||||
|     if (e.key === 'Enter' || e.key === ' ') { | ||||
|       this.handleClick(e); | ||||
|     } | ||||
|   } | ||||
| @ -126,7 +147,7 @@ class DropdownMenu extends React.PureComponent { | ||||
|  | ||||
|     return ( | ||||
|       <li className='dropdown-menu__item' key={`${text}-${i}`}> | ||||
|         <a href={href} target={target} data-method={method} rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyDown={this.handleItemKeyDown} data-index={i}> | ||||
|         <a href={href} target={target} data-method={method} rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyUp={this.handleItemKeyUp} data-index={i}> | ||||
|           {text} | ||||
|         </a> | ||||
|       </li> | ||||
| @ -202,19 +223,6 @@ export default class Dropdown extends React.PureComponent { | ||||
|     this.props.onClose(this.state.id); | ||||
|   } | ||||
|  | ||||
|   handleKeyDown = e => { | ||||
|     switch(e.key) { | ||||
|     case ' ': | ||||
|     case 'Enter': | ||||
|       this.handleClick(e); | ||||
|       e.preventDefault(); | ||||
|       break; | ||||
|     case 'Escape': | ||||
|       this.handleClose(); | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   handleItemClick = e => { | ||||
|     const i = Number(e.currentTarget.getAttribute('data-index')); | ||||
|     const { action, to } = this.props.items[i]; | ||||
| @ -249,7 +257,7 @@ export default class Dropdown extends React.PureComponent { | ||||
|     const open = this.state.id === openDropdownId; | ||||
|  | ||||
|     return ( | ||||
|       <div onKeyDown={this.handleKeyDown}> | ||||
|       <div> | ||||
|         <IconButton | ||||
|           icon={icon} | ||||
|           title={title} | ||||
|  | ||||
| @ -20,7 +20,7 @@ const mapDispatchToProps = (dispatch, { status, items }) => ({ | ||||
|     }) : openDropdownMenu(id, dropdownPlacement, keyboard)); | ||||
|   }, | ||||
|   onClose(id) { | ||||
|     dispatch(closeModal()); | ||||
|     dispatch(closeModal('ACTIONS')); | ||||
|     dispatch(closeDropdownMenu(id)); | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| @ -10,7 +10,7 @@ export default function modal(state = initialState, action) { | ||||
|   case MODAL_OPEN: | ||||
|     return { modalType: action.modalType, modalProps: action.modalProps }; | ||||
|   case MODAL_CLOSE: | ||||
|     return initialState; | ||||
|     return (action.modalType === undefined || action.modalType === state.modalType) ? initialState : state; | ||||
|   default: | ||||
|     return state; | ||||
|   } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user