From 5ee92b47cec8227118ab41ef8933b2e7069c5fb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 21 Jul 2021 13:58:22 +0200 Subject: [PATCH] Keyboard-accessible emoji picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/emoji_selector.js | 56 +++++++++++++------ app/soapbox/components/status.js | 32 ++++++++++- app/soapbox/components/status_action_bar.js | 14 ++++- app/soapbox/components/status_list.js | 4 +- .../features/status/components/action_bar.js | 29 ++++------ app/soapbox/features/status/index.js | 32 ++++++++++- .../features/ui/components/hotkeys_modal.js | 4 ++ app/soapbox/locales/pl.json | 1 + 8 files changed, 131 insertions(+), 41 deletions(-) diff --git a/app/soapbox/components/emoji_selector.js b/app/soapbox/components/emoji_selector.js index d288f901a..7fd52d965 100644 --- a/app/soapbox/components/emoji_selector.js +++ b/app/soapbox/components/emoji_selector.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { HotKeys } from 'react-hotkeys'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; import emojify from 'soapbox/features/emoji/emoji'; @@ -35,6 +36,8 @@ class EmojiSelector extends ImmutablePureComponent { } handleKeyUp = i => e => { + const { onUnfocus } = this.props; + switch (e.key) { case 'Left': case 'ArrowLeft': @@ -48,33 +51,54 @@ class EmojiSelector extends ImmutablePureComponent { this.node.querySelector(`.emoji-react-selector__emoji:nth-child(${i + 2})`).focus(); } break; + case 'Escape': + onUnfocus(); + break; } } + handleReact = emoji => () => { + const { onReact, focused, onUnfocus } = this.props; + + onReact(emoji)(); + + if (focused) { + onUnfocus(); + } + } + + handlers = { + open: () => {}, + }; + setRef = c => { this.node = c; } render() { - const { onReact, visible, focused, allowedEmoji } = this.props; + const { visible, focused, allowedEmoji } = this.props; return ( -
- {allowedEmoji.map((emoji, i) => ( -
+
+ {allowedEmoji.map((emoji, i) => ( +
+ ); } diff --git a/app/soapbox/components/status.js b/app/soapbox/components/status.js index 1ff0b1e03..fcdddccb2 100644 --- a/app/soapbox/components/status.js +++ b/app/soapbox/components/status.js @@ -108,6 +108,7 @@ class Status extends ImmutablePureComponent { state = { showMedia: defaultMediaVisibility(this.props.status, this.props.displayMedia), statusId: undefined, + emojiSelectorFocused: false, }; // Track height changes we know about to compensate scrolling @@ -255,6 +256,27 @@ class Status extends ImmutablePureComponent { this.handleToggleMediaVisibility(); } + handleHotkeyReact = () => { + this._expandEmojiSelector(); + } + + handleEmojiSelectorExpand = e => { + if (e.key === 'Enter') { + this._expandEmojiSelector(); + } + e.preventDefault(); + } + + handleEmojiSelectorUnfocus = () => { + this.setState({ emojiSelectorFocused: false }); + } + + _expandEmojiSelector = () => { + this.setState({ emojiSelectorFocused: true }); + const firstEmoji = this.node.querySelector('.emoji-react-selector .emoji-react-selector__emoji'); + firstEmoji.focus(); + }; + _properStatus() { const { status } = this.props; @@ -278,6 +300,7 @@ class Status extends ImmutablePureComponent { let { status, account, ...other } = this.props; + if (status === null) { return null; } @@ -443,6 +466,7 @@ class Status extends ImmutablePureComponent { moveDown: this.handleHotkeyMoveDown, toggleHidden: this.handleHotkeyToggleHidden, toggleSensitive: this.handleHotkeyToggleSensitive, + react: this.handleHotkeyReact, }; const statusUrl = `/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`; @@ -506,7 +530,13 @@ class Status extends ImmutablePureComponent { )} - + diff --git a/app/soapbox/components/status_action_bar.js b/app/soapbox/components/status_action_bar.js index b58991ea3..206170e24 100644 --- a/app/soapbox/components/status_action_bar.js +++ b/app/soapbox/components/status_action_bar.js @@ -91,6 +91,10 @@ class StatusActionBar extends ImmutablePureComponent { isStaff: PropTypes.bool.isRequired, isAdmin: PropTypes.bool.isRequired, allowedEmoji: ImmutablePropTypes.list, + emojiSelectorFocused: PropTypes.bool, + handleEmojiSelectorUnfocus: PropTypes.func.isRequired, + emojiSelectorFocused: PropTypes.bool, + handleEmojiSelectorUnfocus: PropTypes.func.isRequired, }; static defaultProps = { @@ -106,6 +110,7 @@ class StatusActionBar extends ImmutablePureComponent { updateOnProps = [ 'status', 'withDismiss', + 'emojiSelectorFocused', ] handleReplyClick = () => { @@ -359,7 +364,7 @@ class StatusActionBar extends ImmutablePureComponent { } render() { - const { status, intl, allowedEmoji } = this.props; + const { status, intl, allowedEmoji, emojiSelectorFocused, handleEmojiSelectorUnfocus } = this.props; const { emojiSelectorVisible } = this.state; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); @@ -422,7 +427,12 @@ class StatusActionBar extends ImmutablePureComponent { onMouseLeave={this.handleLikeButtonLeave} ref={this.setRef} > - + { if (featured) { - return this.props.featuredStatusIds.indexOf(id); + return this.props.featuredStatusIds.keySeq().findIndex(key => key === id); } else { - return this.props.statusIds.indexOf(id) + this.getFeaturedStatusCount(); + return this.props.statusIds.keySeq().findIndex(key => key === id) + this.getFeaturedStatusCount(); } } diff --git a/app/soapbox/features/status/components/action_bar.js b/app/soapbox/features/status/components/action_bar.js index 7bcda9cf9..8cc300e5d 100644 --- a/app/soapbox/features/status/components/action_bar.js +++ b/app/soapbox/features/status/components/action_bar.js @@ -100,6 +100,9 @@ class ActionBar extends React.PureComponent { isStaff: PropTypes.bool.isRequired, isAdmin: PropTypes.bool.isRequired, allowedEmoji: ImmutablePropTypes.list, + emojiSelectorFocused: PropTypes.bool, + handleEmojiSelectorExpand: PropTypes.func.isRequired, + handleEmojiSelectorUnfocus: PropTypes.func.isRequired, }; static defaultProps = { @@ -175,19 +178,6 @@ class ActionBar extends React.PureComponent { }; } - handleEmojiSelectorExpand = e => { - if (e.key === 'Enter') { - this.setState({ emojiSelectorFocused: true }); - const firstEmoji = this.node.querySelector('.emoji-react-selector .emoji-react-selector__emoji'); - firstEmoji.focus(); - } - e.preventDefault(); - } - - handleEmojiSelectorUnfocus = () => { - this.setState({ emojiSelectorFocused: false }); - } - handleHotkeyEmoji = () => { const { emojiSelectorVisible } = this.state; @@ -283,13 +273,13 @@ class ActionBar extends React.PureComponent { componentDidMount() { document.addEventListener('click', e => { if (this.node && !this.node.contains(e.target)) - this.setState({ emojiSelectorVisible: false, emojiSelectorFocused: false }); + this.setState({ emojiSelectorVisible: false, emojiSelectorFocused: false }); }); } render() { - const { status, intl, me, isStaff, isAdmin, allowedEmoji } = this.props; - const { emojiSelectorVisible, emojiSelectorFocused } = this.state; + const { status, intl, me, isStaff, isAdmin, allowedEmoji, emojiSelectorFocused, handleEmojiSelectorExpand, handleEmojiSelectorUnfocus } = this.props; + const { emojiSelectorVisible } = this.state; const ownAccount = status.getIn(['account', 'id']) === me; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); @@ -403,7 +393,7 @@ class ActionBar extends React.PureComponent { onReact={this.handleReactClick} visible={emojiSelectorVisible} focused={emojiSelectorFocused} - onUnfocus={this.handleEmojiSelectorUnfocus} + onUnfocus={handleEmojiSelectorUnfocus} /> @@ -435,4 +425,5 @@ class ActionBar extends React.PureComponent { } -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ActionBar)); +export default injectIntl( + connect(mapStateToProps, mapDispatchToProps)(ActionBar)); diff --git a/app/soapbox/features/status/index.js b/app/soapbox/features/status/index.js index 08135b7f9..0ecf98e8c 100644 --- a/app/soapbox/features/status/index.js +++ b/app/soapbox/features/status/index.js @@ -146,6 +146,7 @@ class Status extends ImmutablePureComponent { fullscreen: false, showMedia: defaultMediaVisibility(this.props.status, this.props.displayMedia), loadedStatusId: undefined, + emojiSelectorFocused: false, }; componentDidMount() { @@ -363,6 +364,10 @@ class Status extends ImmutablePureComponent { this.handleToggleMediaVisibility(); } + handleHotkeyReact = () => { + this._expandEmojiSelector(); + } + handleMoveUp = id => { const { status, ancestorsIds, descendantsIds } = this.props; @@ -397,6 +402,23 @@ class Status extends ImmutablePureComponent { } } + handleEmojiSelectorExpand = e => { + if (e.key === 'Enter') { + this._expandEmojiSelector(); + } + e.preventDefault(); + } + + handleEmojiSelectorUnfocus = () => { + this.setState({ emojiSelectorFocused: false }); + } + + _expandEmojiSelector = () => { + this.setState({ emojiSelectorFocused: true }); + const firstEmoji = this.status.querySelector('.emoji-react-selector .emoji-react-selector__emoji'); + firstEmoji.focus(); + }; + _selectChild(index, align_top) { const container = this.node; const element = container.querySelectorAll('.focusable')[index]; @@ -445,6 +467,10 @@ class Status extends ImmutablePureComponent { this.node = c; } + setStatusRef = c => { + this.status = c; + } + componentDidUpdate(prevProps, prevState) { const { params, status } = this.props; const { ancestorsIds } = prevProps; @@ -510,6 +536,7 @@ class Status extends ImmutablePureComponent { openProfile: this.handleHotkeyOpenProfile, toggleHidden: this.handleHotkeyToggleHidden, toggleSensitive: this.handleHotkeyToggleSensitive, + react: this.handleHotkeyReact, }; return ( @@ -537,7 +564,7 @@ class Status extends ImmutablePureComponent { {ancestors} -
+
diff --git a/app/soapbox/features/ui/components/hotkeys_modal.js b/app/soapbox/features/ui/components/hotkeys_modal.js index 319e2e0e5..c646a0923 100644 --- a/app/soapbox/features/ui/components/hotkeys_modal.js +++ b/app/soapbox/features/ui/components/hotkeys_modal.js @@ -49,6 +49,10 @@ class HotkeysModal extends ImmutablePureComponent { f + + e + + b diff --git a/app/soapbox/locales/pl.json b/app/soapbox/locales/pl.json index 2f0b6e1bc..5a87d306a 100644 --- a/app/soapbox/locales/pl.json +++ b/app/soapbox/locales/pl.json @@ -390,6 +390,7 @@ "keyboard_shortcuts.notifications": "aby otworzyć kolumnę powiadomień", "keyboard_shortcuts.pinned": "aby przejść do listy przypiętych wpisów", "keyboard_shortcuts.profile": "aby przejść do profilu autora wpisu", + "keyboard_shortcuts.react": "aby zareagować na wpis", "keyboard_shortcuts.reply": "aby odpowiedzieć", "keyboard_shortcuts.requests": "aby przejść do listy próśb o możliwość śledzenia", "keyboard_shortcuts.search": "aby przejść do pola wyszukiwania",