From 64665df23642c91a4a4d22e27b5ae75993be88af Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sat, 28 Aug 2021 14:52:39 +0200 Subject: [PATCH] Improve dropdown menu keyboard navigation * 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 --- app/soapbox/actions/modal.js | 3 +- app/soapbox/components/dropdown_menu.js | 43 +++++++++++-------- .../containers/dropdown_menu_container.js | 2 +- app/soapbox/reducers/modal.js | 2 +- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/app/soapbox/actions/modal.js b/app/soapbox/actions/modal.js index eaa5a315d..72604ecc6 100644 --- a/app/soapbox/actions/modal.js +++ b/app/soapbox/actions/modal.js @@ -9,8 +9,9 @@ export function openModal(type, props) { }; } -export function closeModal() { +export function closeModal(type) { return { type: MODAL_CLOSE, + modalType: type, }; } diff --git a/app/soapbox/components/dropdown_menu.js b/app/soapbox/components/dropdown_menu.js index 2f2412928..8c1d709d1 100644 --- a/app/soapbox/components/dropdown_menu.js +++ b/app/soapbox/components/dropdown_menu.js @@ -46,6 +46,10 @@ class DropdownMenu extends React.PureComponent { 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 +57,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 +88,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 +112,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); } } @@ -151,7 +173,7 @@ class DropdownMenu extends React.PureComponent { ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onAuxClick={this.handleAuxClick} - onKeyDown={this.handleItemKeyDown} + onKeyUp={this.handleItemKeyUp} data-index={i} target={newTab ? '_blank' : null} data-method={isLogout ? 'delete' : null} @@ -229,19 +251,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]; @@ -276,7 +285,7 @@ export default class Dropdown extends React.PureComponent { const open = this.state.id === openDropdownId; return ( -
+
({ }) : openDropdownMenu(id, dropdownPlacement, keyboard)); }, onClose(id) { - dispatch(closeModal()); + dispatch(closeModal('ACTIONS')); dispatch(closeDropdownMenu(id)); }, }); diff --git a/app/soapbox/reducers/modal.js b/app/soapbox/reducers/modal.js index 6572d619e..69a991fa7 100644 --- a/app/soapbox/reducers/modal.js +++ b/app/soapbox/reducers/modal.js @@ -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; }