diff --git a/app/soapbox/components/emoji_selector.js b/app/soapbox/components/emoji_selector.js index 4cd0395e3..7839712c9 100644 --- a/app/soapbox/components/emoji_selector.js +++ b/app/soapbox/components/emoji_selector.js @@ -15,25 +15,64 @@ class EmojiSelector extends ImmutablePureComponent { static propTypes = { onReact: PropTypes.func.isRequired, + onUnfocus: PropTypes.func, visible: PropTypes.bool, + focused: PropTypes.bool, } static defaultProps = { onReact: () => {}, + onUnfocus: () => {}, visible: false, } + handleBlur = e => { + const { focused, onUnfocus } = this.props; + + if (focused && (!e.relatedTarget || !e.relatedTarget.classList.contains('emoji-react-selector__emoji'))) { + onUnfocus(); + } + } + + handleKeyUp = i => e => { + switch (e.key) { + case 'Left': + case 'ArrowLeft': + if (i !== 0) { + this.node.querySelector(`.emoji-react-selector__emoji:nth-child(${i})`).focus(); + } + break; + case 'Right': + case 'ArrowRight': + if (i !== this.props.allowedEmoji.size - 1) { + this.node.querySelector(`.emoji-react-selector__emoji:nth-child(${i + 2})`).focus(); + } + break; + } + } + + setRef = c => { + this.node = c; + } + + render() { - const { onReact, visible, allowedEmoji } = this.props; + const { onReact, visible, focused, allowedEmoji } = this.props; return ( -
+
{allowedEmoji.map((emoji, i) => (
diff --git a/app/soapbox/components/icon_button.js b/app/soapbox/components/icon_button.js index 949f4e5b4..21ed4ca95 100644 --- a/app/soapbox/components/icon_button.js +++ b/app/soapbox/components/icon_button.js @@ -13,6 +13,8 @@ export default class IconButton extends React.PureComponent { title: PropTypes.string.isRequired, icon: PropTypes.string.isRequired, onClick: PropTypes.func, + onKeyUp: PropTypes.func, + onKeyDown: PropTypes.func, onMouseEnter: PropTypes.func, onMouseLeave: PropTypes.func, size: PropTypes.number, @@ -37,6 +39,8 @@ export default class IconButton extends React.PureComponent { animate: false, overlay: false, tabIndex: '0', + onKeyUp: () => {}, + onKeyDown: () => {}, onClick: () => {}, onMouseEnter: () => {}, onMouseLeave: () => {}, @@ -94,6 +98,8 @@ export default class IconButton extends React.PureComponent { title={title} className={classes} onClick={this.handleClick} + onKeyUp={this.props.onKeyUp} + onKeyDown={this.props.onKeyDown} onMouseEnter={this.props.onMouseEnter} onMouseLeave={this.props.onMouseLeave} tabIndex={tabIndex} @@ -119,6 +125,8 @@ export default class IconButton extends React.PureComponent { title={title} className={classes} onClick={this.handleClick} + onKeyUp={this.props.onKeyUp} + onKeyDown={this.props.onKeyDown} onMouseEnter={this.props.onMouseEnter} onMouseLeave={this.props.onMouseLeave} tabIndex={tabIndex} diff --git a/app/soapbox/features/status/components/action_bar.js b/app/soapbox/features/status/components/action_bar.js index 0642e1f06..2ab2652af 100644 --- a/app/soapbox/features/status/components/action_bar.js +++ b/app/soapbox/features/status/components/action_bar.js @@ -48,6 +48,7 @@ const messages = defineMessages({ reactionOpenMouth: { id: 'status.reactions.open_mouth', defaultMessage: 'Wow' }, reactionCry: { id: 'status.reactions.cry', defaultMessage: 'Sad' }, reactionWeary: { id: 'status.reactions.weary', defaultMessage: 'Weary' }, + emojiPickerExpand: { id: 'status.reactions_expand', defaultMessage: 'Select emoji' }, }); const mapStateToProps = state => { @@ -103,6 +104,7 @@ class ActionBar extends React.PureComponent { state = { emojiSelectorVisible: false, + emojiSelectorFocused: false, } handleReplyClick = () => { @@ -165,10 +167,23 @@ class ActionBar extends React.PureComponent { } else { this.props.onOpenUnauthorizedModal(); } - this.setState({ emojiSelectorVisible: false }); + this.setState({ emojiSelectorVisible: false, emojiSelectorFocused: false }); }; } + 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 }); + } + handleDeleteClick = () => { this.props.onDelete(this.props.status, this.context.router.history); } @@ -258,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 }); + this.setState({ emojiSelectorVisible: false, emojiSelectorFocused: false }); }); } render() { const { status, intl, me, isStaff, allowedEmoji } = this.props; - const { emojiSelectorVisible } = this.state; + const { emojiSelectorVisible, emojiSelectorFocused } = this.state; const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const mutingConversation = status.get('muted'); @@ -364,7 +379,12 @@ class ActionBar extends React.PureComponent { onMouseLeave={this.handleLikeButtonLeave} ref={this.setRef} > - + +
{shareButton} diff --git a/app/styles/components/detailed-status.scss b/app/styles/components/detailed-status.scss index 4cbaad0ed..a15e4f0c8 100644 --- a/app/styles/components/detailed-status.scss +++ b/app/styles/components/detailed-status.scss @@ -87,6 +87,22 @@ transform: translateY(-1px); } } + + .emoji-picker-expand { + display: none; + } + + &:focus-within { + .emoji-picker-expand { + display: inline-flex; + width: 0; + overflow: hidden; + + &:focus-within { + width: unset; + } + } + } } .detailed-status__wrapper { diff --git a/app/styles/components/emoji-reacts.scss b/app/styles/components/emoji-reacts.scss index d9a4450c7..f0ad0efa4 100644 --- a/app/styles/components/emoji-reacts.scss +++ b/app/styles/components/emoji-reacts.scss @@ -80,7 +80,7 @@ transition: 0.1s; z-index: 999; - &--visible { + &--visible, &--focused { opacity: 1; pointer-events: all; } @@ -99,7 +99,7 @@ transition: 0.1s; } - &:hover { + &:hover, &:focus { img { width: 36px; height: 36px; diff --git a/app/styles/components/status.scss b/app/styles/components/status.scss index 7244cf401..347a6c0e4 100644 --- a/app/styles/components/status.scss +++ b/app/styles/components/status.scss @@ -666,3 +666,21 @@ a.status-card.compact:hover { border-radius: 4px; } } + +.status__action-bar, .detailed-status__action-bar { + .emoji-picker-expand { + display: none; + } + + &:focus-within { + .emoji-picker-expand { + display: inline-flex; + width: 0; + overflow: hidden; + + &:focus-within { + width: unset; + } + } + } +}