From 6c034e3a41d451a5cf99200abc5ec5d93ec8a42c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 28 Aug 2021 14:09:29 +0200 Subject: [PATCH 01/11] Minor accessibility improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/button.js | 2 +- app/soapbox/components/modal_root.js | 1 + app/styles/components/account-header.scss | 1 + package.json | 3 ++- yarn.lock | 5 +++++ 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/soapbox/components/button.js b/app/soapbox/components/button.js index 6705b77df..214ce7b53 100644 --- a/app/soapbox/components/button.js +++ b/app/soapbox/components/button.js @@ -66,7 +66,7 @@ export default class Button extends React.PureComponent { if (this.props.to) { return ( - + {btn} ); diff --git a/app/soapbox/components/modal_root.js b/app/soapbox/components/modal_root.js index 1ffb12d3c..96de00f1c 100644 --- a/app/soapbox/components/modal_root.js +++ b/app/soapbox/components/modal_root.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import 'wicg-inert'; import { injectIntl, FormattedMessage, defineMessages } from 'react-intl'; import { connect } from 'react-redux'; import { openModal } from '../actions/modal'; diff --git a/app/styles/components/account-header.scss b/app/styles/components/account-header.scss index 55386987a..cc3cbd95e 100644 --- a/app/styles/components/account-header.scss +++ b/app/styles/components/account-header.scss @@ -221,6 +221,7 @@ } &:hover, + &:focus, &.active { border-bottom: 2px solid var(--primary-text-color); } diff --git a/package.json b/package.json index cc252e867..6ce9d397e 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,8 @@ "webpack-bundle-analyzer": "^4.0.0", "webpack-cli": "^3.3.2", "webpack-merge": "^5.2.0", - "websocket.js": "^0.1.12" + "websocket.js": "^0.1.12", + "wicg-inert": "^3.1.1" }, "devDependencies": { "axios-mock-adapter": "^1.18.1", diff --git a/yarn.lock b/yarn.lock index 031ea8ab9..edbe92d7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13062,6 +13062,11 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +wicg-inert@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/wicg-inert/-/wicg-inert-3.1.1.tgz#b033fd4fbfb9e3fd709e5d84becbdf2e06e5c229" + integrity sha512-PhBaNh8ur9Xm4Ggy4umelwNIP6pPP1bv3EaWaKqfb/QNme2rdLjm7wIInvV4WhxVHhzA4Spgw9qNSqWtB/ca2A== + wide-align@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" From 4b4e815e40c32aefd5ac26862af519bb19afbead Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 28 Aug 2021 14:17:14 +0200 Subject: [PATCH 02/11] Add hotkey for opening media files --- app/soapbox/components/status.js | 16 +++++++++++++ app/soapbox/features/status/index.js | 16 +++++++++++++ .../features/ui/components/hotkeys_modal.js | 24 +++++++++++-------- app/soapbox/features/ui/index.js | 1 + app/soapbox/features/video/index.js | 2 +- 5 files changed, 48 insertions(+), 11 deletions(-) diff --git a/app/soapbox/components/status.js b/app/soapbox/components/status.js index 151a3e926..95c7ffb0c 100644 --- a/app/soapbox/components/status.js +++ b/app/soapbox/components/status.js @@ -213,6 +213,21 @@ class Status extends ImmutablePureComponent { this.props.OnOpenAudio(media, startTime); } + handleHotkeyOpenMedia = e => { + const { onOpenMedia, onOpenVideo } = this.props; + const status = this._properStatus(); + + e.preventDefault(); + + if (status.get('media_attachments').size > 0) { + if (status.getIn(['media_attachments', 0, 'type']) === 'video') { + onOpenVideo(status.getIn(['media_attachments', 0]), 0); + } else { + onOpenMedia(status.get('media_attachments'), 0); + } + } + } + handleHotkeyReply = e => { e.preventDefault(); this.props.onReply(this._properStatus(), this.context.router.history); @@ -461,6 +476,7 @@ class Status extends ImmutablePureComponent { moveDown: this.handleHotkeyMoveDown, toggleHidden: this.handleHotkeyToggleHidden, toggleSensitive: this.handleHotkeyToggleSensitive, + openMedia: this.handleHotkeyOpenMedia, react: this.handleHotkeyReact, }; diff --git a/app/soapbox/features/status/index.js b/app/soapbox/features/status/index.js index 5bac4fff9..03890cb26 100644 --- a/app/soapbox/features/status/index.js +++ b/app/soapbox/features/status/index.js @@ -263,6 +263,21 @@ class Status extends ImmutablePureComponent { this.props.dispatch(openModal('VIDEO', { media, time })); } + handleHotkeyOpenMedia = e => { + const { onOpenMedia, onOpenVideo } = this.props; + const status = this._properStatus(); + + e.preventDefault(); + + if (status.get('media_attachments').size > 0) { + if (status.getIn(['media_attachments', 0, 'type']) === 'video') { + onOpenVideo(status.getIn(['media_attachments', 0]), 0); + } else { + onOpenMedia(status.get('media_attachments'), 0); + } + } + } + handleMuteClick = (account) => { this.props.dispatch(initMuteModal(account)); } @@ -548,6 +563,7 @@ class Status extends ImmutablePureComponent { openProfile: this.handleHotkeyOpenProfile, toggleHidden: this.handleHotkeyToggleHidden, toggleSensitive: this.handleHotkeyToggleSensitive, + openMedia: this.handleHotkeyOpenMedia, react: this.handleHotkeyReact, }; diff --git a/app/soapbox/features/ui/components/hotkeys_modal.js b/app/soapbox/features/ui/components/hotkeys_modal.js index c646a0923..6d1434135 100644 --- a/app/soapbox/features/ui/components/hotkeys_modal.js +++ b/app/soapbox/features/ui/components/hotkeys_modal.js @@ -62,12 +62,8 @@ class HotkeysModal extends ImmutablePureComponent { - x - - - - h - + a + @@ -78,6 +74,14 @@ class HotkeysModal extends ImmutablePureComponent { + + x + + + + h + + up, k @@ -106,10 +110,6 @@ class HotkeysModal extends ImmutablePureComponent { esc - - g + h - - @@ -119,6 +119,10 @@ class HotkeysModal extends ImmutablePureComponent { + + + + diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 35c768593..010242b0d 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -155,6 +155,7 @@ const keyMap = { goToRequests: 'g r', toggleHidden: 'x', toggleSensitive: 'h', + openMedia: 'a', }; class SwitchingColumnsArea extends React.PureComponent { diff --git a/app/soapbox/features/video/index.js b/app/soapbox/features/video/index.js index 2b70d6db3..286b9755e 100644 --- a/app/soapbox/features/video/index.js +++ b/app/soapbox/features/video/index.js @@ -461,7 +461,7 @@ class Video extends React.PureComponent {
- +
From 2ef0cdb71b13278b2b0fa7d02cb0dd13dc13991a Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 28 Aug 2021 14:26:20 +0200 Subject: [PATCH 03/11] Fix ordering of keyboard access between CW field, textarea and emoji picker --- app/soapbox/features/compose/components/compose_form.js | 5 +---- app/styles/components/compose-form.scss | 4 +--- app/styles/ui.scss | 4 ---- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/app/soapbox/features/compose/components/compose_form.js b/app/soapbox/features/compose/components/compose_form.js index 502fbd7f8..0f34cc24d 100644 --- a/app/soapbox/features/compose/components/compose_form.js +++ b/app/soapbox/features/compose/components/compose_form.js @@ -314,10 +314,6 @@ export default class ComposeForm extends ImmutablePureComponent { />
-
- -
- + { !condensed &&
diff --git a/app/styles/components/compose-form.scss b/app/styles/components/compose-form.scss index 9daba415c..23c2ae359 100644 --- a/app/styles/components/compose-form.scss +++ b/app/styles/components/compose-form.scss @@ -62,9 +62,8 @@ .emoji-picker-dropdown { position: absolute; - top: 5px; + top: 10px; right: 5px; - z-index: 1; } .compose-form__autosuggest-wrapper { @@ -141,7 +140,6 @@ } } - .emoji-picker-wrapper, .autosuggest-textarea__suggestions-wrapper { position: relative; height: 0; diff --git a/app/styles/ui.scss b/app/styles/ui.scss index f96f26aae..b9cf99e38 100644 --- a/app/styles/ui.scss +++ b/app/styles/ui.scss @@ -148,10 +148,6 @@ padding: 20px; margin-bottom: 20px; - .emoji-picker-wrapper { - .emoji-picker-dropdown { top: 10px; } - } - .compose-form { flex: 1 1; padding: 0 0 0 20px !important; From e43692b4cece18f3d1d7295bde7b8f5089f88883 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sat, 28 Aug 2021 14:30:58 +0200 Subject: [PATCH 04/11] Trap tab in modals --- app/soapbox/components/modal_root.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/soapbox/components/modal_root.js b/app/soapbox/components/modal_root.js index 96de00f1c..a8e8fe746 100644 --- a/app/soapbox/components/modal_root.js +++ b/app/soapbox/components/modal_root.js @@ -75,8 +75,31 @@ class ModalRoot extends React.PureComponent { } }; + + handleKeyDown = (e) => { + if (e.key === 'Tab') { + const focusable = Array.from(this.node.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')).filter((x) => window.getComputedStyle(x).display !== 'none'); + const index = focusable.indexOf(e.target); + + let element; + + if (e.shiftKey) { + element = focusable[index - 1] || focusable[focusable.length - 1]; + } else { + element = focusable[index + 1] || focusable[0]; + } + + if (element) { + element.focus(); + e.stopPropagation(); + e.preventDefault(); + } + } + } + componentDidMount() { window.addEventListener('keyup', this.handleKeyUp, false); + window.addEventListener('keydown', this.handleKeyDown, false); } componentDidUpdate(prevProps) { @@ -102,6 +125,7 @@ class ModalRoot extends React.PureComponent { componentWillUnmount() { window.removeEventListener('keyup', this.handleKeyUp); + window.removeEventListener('keydown', this.handleKeyDown); } getSiblings = () => { From 64665df23642c91a4a4d22e27b5ae75993be88af Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sat, 28 Aug 2021 14:52:39 +0200 Subject: [PATCH 05/11] 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; } From 675c5a4df2781bdbd70dce3b708a73e00941162b Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sat, 28 Aug 2021 15:01:21 +0200 Subject: [PATCH 06/11] Improve keyboard navigation in privacy dropdown * Trap tab in privacy dropdown * Give focus back to last focused element when privacy dropdown menu closes * Actually give back focus to the element that had it before clicking the dropdown --- app/soapbox/components/icon_button.js | 29 ++++++++++++--- .../compose/components/privacy_dropdown.js | 36 +++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/app/soapbox/components/icon_button.js b/app/soapbox/components/icon_button.js index 21ed4ca95..9231ee133 100644 --- a/app/soapbox/components/icon_button.js +++ b/app/soapbox/components/icon_button.js @@ -13,6 +13,7 @@ export default class IconButton extends React.PureComponent { title: PropTypes.string.isRequired, icon: PropTypes.string.isRequired, onClick: PropTypes.func, + onMouseDown: PropTypes.func, onKeyUp: PropTypes.func, onKeyDown: PropTypes.func, onMouseEnter: PropTypes.func, @@ -54,6 +55,24 @@ export default class IconButton extends React.PureComponent { } } + handleMouseDown = (e) => { + if (!this.props.disabled && this.props.onMouseDown) { + this.props.onMouseDown(e); + } + } + + handleKeyDown = (e) => { + if (!this.props.disabled && this.props.onKeyDown) { + this.props.onKeyDown(e); + } + } + + handleKeyUp = (e) => { + if (!this.props.disabled && this.props.onKeyUp) { + this.props.onKeyUp(e); + } + } + render() { const style = { fontSize: `${this.props.size}px`, @@ -98,8 +117,9 @@ export default class IconButton extends React.PureComponent { title={title} className={classes} onClick={this.handleClick} - onKeyUp={this.props.onKeyUp} - onKeyDown={this.props.onKeyDown} + onMouseDown={this.handleMouseDown} + onKeyDown={this.handleKeyDown} + onKeyUp={this.handleKeyUp} onMouseEnter={this.props.onMouseEnter} onMouseLeave={this.props.onMouseLeave} tabIndex={tabIndex} @@ -125,8 +145,9 @@ export default class IconButton extends React.PureComponent { title={title} className={classes} onClick={this.handleClick} - onKeyUp={this.props.onKeyUp} - onKeyDown={this.props.onKeyDown} + onMouseDown={this.handleMouseDown} + onKeyDown={this.handleKeyDown} + onKeyUp={this.handleKeyUp} onMouseEnter={this.props.onMouseEnter} onMouseLeave={this.props.onMouseLeave} tabIndex={tabIndex} diff --git a/app/soapbox/features/compose/components/privacy_dropdown.js b/app/soapbox/features/compose/components/privacy_dropdown.js index 2e5f4e5f4..d59859603 100644 --- a/app/soapbox/features/compose/components/privacy_dropdown.js +++ b/app/soapbox/features/compose/components/privacy_dropdown.js @@ -73,6 +73,19 @@ class PrivacyDropdownMenu extends React.PureComponent { this.props.onChange(element.getAttribute('data-index')); } break; + case 'Tab': + if (e.shiftKey) { + element = this.node.childNodes[index - 1] || this.node.lastChild; + } else { + element = this.node.childNodes[index + 1] || this.node.firstChild; + } + if (element) { + element.focus(); + this.props.onChange(element.getAttribute('data-index')); + e.preventDefault(); + e.stopPropagation(); + } + break; case 'Home': element = this.node.firstChild; if (element) { @@ -192,6 +205,9 @@ class PrivacyDropdown extends React.PureComponent { } } else { const { top } = target.getBoundingClientRect(); + if (this.state.open && this.activeElement) { + this.activeElement.focus(); + } this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); this.setState({ open: !this.state.open }); } @@ -214,7 +230,25 @@ class PrivacyDropdown extends React.PureComponent { } } + handleMouseDown = () => { + if (!this.state.open) { + this.activeElement = document.activeElement; + } + } + + handleButtonKeyDown = (e) => { + switch(e.key) { + case ' ': + case 'Enter': + this.handleMouseDown(); + break; + } + } + handleClose = () => { + if (this.state.open && this.activeElement) { + this.activeElement.focus(); + } this.setState({ open: false }); } @@ -240,6 +274,8 @@ class PrivacyDropdown extends React.PureComponent { active={open} inverted onClick={this.handleToggle} + onMouseDown={this.handleMouseDown} + onKeyDown={this.handleButtonKeyDown} style={{ height: null, lineHeight: '27px' }} />
From 11057970ef4a0236153b97de9e74baf20aa3faec Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sat, 28 Aug 2021 15:05:26 +0200 Subject: [PATCH 07/11] Improve focus handling with dropdown menus - Focus first item when activated via keyboard - When the dropdown menu closes, give back the focus to the actual element which was focused prior to opening the menu --- app/soapbox/components/dropdown_menu.js | 42 ++++++++++++++++++++----- app/soapbox/components/icon_button.js | 9 ++++++ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/app/soapbox/components/dropdown_menu.js b/app/soapbox/components/dropdown_menu.js index 8c1d709d1..7067e8e9e 100644 --- a/app/soapbox/components/dropdown_menu.js +++ b/app/soapbox/components/dropdown_menu.js @@ -45,8 +45,6 @@ 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(); } @@ -57,9 +55,6 @@ 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 => { @@ -118,7 +113,7 @@ class DropdownMenu extends React.PureComponent { } } - handleItemKeyUp = e => { + handleItemKeyPress = e => { if (e.key === 'Enter' || e.key === ' ') { this.handleClick(e); } @@ -173,7 +168,7 @@ class DropdownMenu extends React.PureComponent { ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onAuxClick={this.handleAuxClick} - onKeyUp={this.handleItemKeyUp} + onKeyPress={this.handleItemKeyPress} data-index={i} target={newTab ? '_blank' : null} data-method={isLogout ? 'delete' : null} @@ -248,9 +243,39 @@ export default class Dropdown extends React.PureComponent { } handleClose = () => { + if (this.activeElement) { + this.activeElement.focus(); + this.activeElement = null; + } this.props.onClose(this.state.id); } + handleMouseDown = () => { + if (!this.state.open) { + this.activeElement = document.activeElement; + } + } + + handleButtonKeyDown = (e) => { + switch(e.key) { + case ' ': + case 'Enter': + this.handleMouseDown(); + break; + } + } + + handleKeyPress = (e) => { + switch(e.key) { + case ' ': + case 'Enter': + this.handleClick(e); + e.stopPropagation(); + e.preventDefault(); + break; + } + } + handleItemClick = e => { const i = Number(e.currentTarget.getAttribute('data-index')); const { action, to } = this.props.items[i]; @@ -294,6 +319,9 @@ export default class Dropdown extends React.PureComponent { size={size} ref={this.setTargetRef} onClick={this.handleClick} + onMouseDown={this.handleMouseDown} + onKeyDown={this.handleButtonKeyDown} + onKeyPress={this.handleKeyPress} /> diff --git a/app/soapbox/components/icon_button.js b/app/soapbox/components/icon_button.js index 9231ee133..0d290c781 100644 --- a/app/soapbox/components/icon_button.js +++ b/app/soapbox/components/icon_button.js @@ -16,6 +16,7 @@ export default class IconButton extends React.PureComponent { onMouseDown: PropTypes.func, onKeyUp: PropTypes.func, onKeyDown: PropTypes.func, + onKeyPress: PropTypes.func, onMouseEnter: PropTypes.func, onMouseLeave: PropTypes.func, size: PropTypes.number, @@ -73,6 +74,12 @@ export default class IconButton extends React.PureComponent { } } + handleKeyPress = (e) => { + if (this.props.onKeyPress && !this.props.disabled) { + this.props.onKeyPress(e); + } + } + render() { const style = { fontSize: `${this.props.size}px`, @@ -120,6 +127,7 @@ export default class IconButton extends React.PureComponent { onMouseDown={this.handleMouseDown} onKeyDown={this.handleKeyDown} onKeyUp={this.handleKeyUp} + onKeyPress={this.handleKeyPress} onMouseEnter={this.props.onMouseEnter} onMouseLeave={this.props.onMouseLeave} tabIndex={tabIndex} @@ -148,6 +156,7 @@ export default class IconButton extends React.PureComponent { onMouseDown={this.handleMouseDown} onKeyDown={this.handleKeyDown} onKeyUp={this.handleKeyUp} + onKeyPress={this.handleKeyPress} onMouseEnter={this.props.onMouseEnter} onMouseLeave={this.props.onMouseLeave} tabIndex={tabIndex} From 14c094e9dba9f847d04b05326d0137dfb8ceb173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 28 Aug 2021 15:13:08 +0200 Subject: [PATCH 08/11] styles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/styles/components/columns.scss | 3 ++- app/styles/ui.scss | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/styles/components/columns.scss b/app/styles/components/columns.scss index 5bd24e1e2..205546845 100644 --- a/app/styles/components/columns.scss +++ b/app/styles/components/columns.scss @@ -480,7 +480,8 @@ color: var(--primary-text-color--faint); background: transparent; - &:hover { + &:hover, + &:focus { color: hsla(var(--primary-text-color_hsl), 0.8); } diff --git a/app/styles/ui.scss b/app/styles/ui.scss index b9cf99e38..bf8dd9f7e 100644 --- a/app/styles/ui.scss +++ b/app/styles/ui.scss @@ -43,7 +43,7 @@ &:hover, &:active, &:focus { - color: var(--primary-text-color--faint); + color: var(--primary-text-color); } &.disabled { @@ -93,7 +93,7 @@ &:hover, &:active, &:focus { - color: var(--primary-text-color--faint); + color: var(--primary-text-color); transition: color 200ms ease-out; } From a8501fb44dca6a655dced04c14c1117241bf5375 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 28 Aug 2021 15:17:28 +0200 Subject: [PATCH 09/11] Fix scroll position resetting when opening media modals in web UI --- app/soapbox/containers/soapbox.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/soapbox/containers/soapbox.js b/app/soapbox/containers/soapbox.js index 27f7ef614..8a0c327da 100644 --- a/app/soapbox/containers/soapbox.js +++ b/app/soapbox/containers/soapbox.js @@ -15,6 +15,8 @@ import UI from '../features/ui'; // import Introduction from '../features/introduction'; import { preload } from '../actions/preload'; import { IntlProvider } from 'react-intl'; +import { previewState as previewMediaState } from 'soapbox/features/ui/components/media_modal'; +import { previewState as previewVideoState } from 'soapbox/features/ui/components/video_modal'; import ErrorBoundary from '../components/error_boundary'; import { fetchInstance } from 'soapbox/actions/instance'; import { fetchSoapboxConfig } from 'soapbox/actions/soapbox'; @@ -104,6 +106,10 @@ class SoapboxMount extends React.PureComponent { this.maybeUpdateMessages(prevProps); } + shouldUpdateScroll(_, { location }) { + return location.state !== previewMediaState && location.state !== previewVideoState; + } + render() { const { me, themeCss, locale, customCss } = this.props; if (me === null) return null; @@ -137,7 +143,7 @@ class SoapboxMount extends React.PureComponent { - + {!me && } From 7b7808defe368160ab50379541724e6750e43bc0 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sat, 28 Aug 2021 15:37:53 +0200 Subject: [PATCH 10/11] Fix and refactor keyboard navigation in dropdown menus --- app/soapbox/components/dropdown_menu.js | 29 +++++---------- .../compose/components/privacy_dropdown.js | 35 ++++++------------- 2 files changed, 19 insertions(+), 45 deletions(-) diff --git a/app/soapbox/components/dropdown_menu.js b/app/soapbox/components/dropdown_menu.js index 7067e8e9e..75bbcbec6 100644 --- a/app/soapbox/components/dropdown_menu.js +++ b/app/soapbox/components/dropdown_menu.js @@ -68,20 +68,14 @@ class DropdownMenu extends React.PureComponent { handleKeyDown = e => { const items = Array.from(this.node.getElementsByTagName('a')); const index = items.indexOf(document.activeElement); - let element; + let element = null; switch(e.key) { case 'ArrowDown': - element = items[index+1]; - if (element) { - element.focus(); - } + element = items[index+1] || items[0]; break; case 'ArrowUp': - element = items[index-1]; - if (element) { - element.focus(); - } + element = items[index-1] || items[items.length-1]; break; case 'Tab': if (e.shiftKey) { @@ -89,28 +83,23 @@ class DropdownMenu extends React.PureComponent { } else { element = items[index+1] || items[0]; } - if (element) { - element.focus(); - e.preventDefault(); - e.stopPropagation(); - } break; case 'Home': element = items[0]; - if (element) { - element.focus(); - } break; case 'End': element = items[items.length-1]; - if (element) { - element.focus(); - } break; case 'Escape': this.props.onClose(); break; } + + if (element) { + element.focus(); + e.preventDefault(); + e.stopPropagation(); + } } handleItemKeyPress = e => { diff --git a/app/soapbox/features/compose/components/privacy_dropdown.js b/app/soapbox/features/compose/components/privacy_dropdown.js index d59859603..b3a4271d8 100644 --- a/app/soapbox/features/compose/components/privacy_dropdown.js +++ b/app/soapbox/features/compose/components/privacy_dropdown.js @@ -50,7 +50,7 @@ class PrivacyDropdownMenu extends React.PureComponent { const index = items.findIndex(item => { return (item.value === value); }); - let element; + let element = null; switch(e.key) { case 'Escape': @@ -60,18 +60,10 @@ class PrivacyDropdownMenu extends React.PureComponent { this.handleClick(e); break; case 'ArrowDown': - element = this.node.childNodes[index + 1]; - if (element) { - element.focus(); - this.props.onChange(element.getAttribute('data-index')); - } + element = this.node.childNodes[index + 1] || this.node.firstChild; break; case 'ArrowUp': - element = this.node.childNodes[index - 1]; - if (element) { - element.focus(); - this.props.onChange(element.getAttribute('data-index')); - } + element = this.node.childNodes[index - 1] || this.node.lastChild; break; case 'Tab': if (e.shiftKey) { @@ -79,28 +71,21 @@ class PrivacyDropdownMenu extends React.PureComponent { } else { element = this.node.childNodes[index + 1] || this.node.firstChild; } - if (element) { - element.focus(); - this.props.onChange(element.getAttribute('data-index')); - e.preventDefault(); - e.stopPropagation(); - } 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; } + + if (element) { + element.focus(); + this.props.onChange(element.getAttribute('data-index')); + e.preventDefault(); + e.stopPropagation(); + } } handleClick = e => { From 9b879b693135af174835c79161562e84204ac440 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sat, 28 Aug 2021 15:38:51 +0200 Subject: [PATCH 11/11] Fix page incorrectly scrolling when bringing up dropdown menus --- app/soapbox/components/dropdown_menu.js | 2 +- app/soapbox/features/compose/components/privacy_dropdown.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/soapbox/components/dropdown_menu.js b/app/soapbox/components/dropdown_menu.js index 75bbcbec6..37f1a8c2b 100644 --- a/app/soapbox/components/dropdown_menu.js +++ b/app/soapbox/components/dropdown_menu.js @@ -46,7 +46,7 @@ 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.focusedItem.focus({ preventScroll: true }); } this.setState({ mounted: true }); } diff --git a/app/soapbox/features/compose/components/privacy_dropdown.js b/app/soapbox/features/compose/components/privacy_dropdown.js index b3a4271d8..f6fd27d28 100644 --- a/app/soapbox/features/compose/components/privacy_dropdown.js +++ b/app/soapbox/features/compose/components/privacy_dropdown.js @@ -100,7 +100,7 @@ class PrivacyDropdownMenu extends React.PureComponent { componentDidMount() { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - if (this.focusedItem) this.focusedItem.focus(); + if (this.focusedItem) this.focusedItem.focus({ preventScroll: true }); this.setState({ mounted: true }); }
g + h
g + n