diff --git a/app/soapbox/components/emoji_selector.js b/app/soapbox/components/emoji_selector.js
index 4cd0395e3..d288f901a 100644
--- a/app/soapbox/components/emoji_selector.js
+++ b/app/soapbox/components/emoji_selector.js
@@ -15,25 +15,63 @@ 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 1336186d9..7bcda9cf9 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 => {
@@ -107,6 +108,7 @@ class ActionBar extends React.PureComponent {
state = {
emojiSelectorVisible: false,
+ emojiSelectorFocused: false,
}
handleReplyClick = () => {
@@ -169,10 +171,29 @@ 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 });
+ }
+
+ handleHotkeyEmoji = () => {
+ const { emojiSelectorVisible } = this.state;
+
+ this.setState({ emojiSelectorVisible: !emojiSelectorVisible });
+ }
+
handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.context.router.history);
}
@@ -262,13 +283,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, isAdmin, allowedEmoji } = this.props;
- const { emojiSelectorVisible } = this.state;
+ const { emojiSelectorVisible, emojiSelectorFocused } = this.state;
const ownAccount = status.getIn(['account', 'id']) === me;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
@@ -351,6 +372,7 @@ class ActionBar extends React.PureComponent {
let reblog_disabled = (status.get('visibility') === 'direct' || status.get('visibility') === 'private');
+
return (
@@ -377,7 +399,12 @@ class ActionBar extends React.PureComponent {
onMouseLeave={this.handleLikeButtonLeave}
ref={this.setRef}
>
-
+
+
{shareButton}
diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js
index fd522c07f..2dee8512b 100644
--- a/app/soapbox/features/ui/index.js
+++ b/app/soapbox/features/ui/index.js
@@ -129,6 +129,7 @@ const keyMap = {
forceNew: 'option+n',
reply: 'r',
favourite: 'f',
+ react: 'e',
boost: 'b',
mention: 'm',
open: ['enter', 'o'],
diff --git a/app/soapbox/locales/pl.json b/app/soapbox/locales/pl.json
index 3c6d845a9..2f0b6e1bc 100644
--- a/app/soapbox/locales/pl.json
+++ b/app/soapbox/locales/pl.json
@@ -692,6 +692,7 @@
"status.reactions.like": "Lubię",
"status.reactions.open_mouth": "Wow",
"status.reactions.weary": "Nuda…",
+ "status.reactions_expand": "Wybierz emoji",
"status.read_more": "Czytaj dalej",
"status.reblog": "Podbij",
"status.reblog_private": "Podbij dla odbiorców oryginalnego wpisu",
diff --git a/app/styles/components/detailed-status.scss b/app/styles/components/detailed-status.scss
index 985f1a079..3daac0805 100644
--- a/app/styles/components/detailed-status.scss
+++ b/app/styles/components/detailed-status.scss
@@ -94,6 +94,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..9b2311099 100644
--- a/app/styles/components/emoji-reacts.scss
+++ b/app/styles/components/emoji-reacts.scss
@@ -80,7 +80,8 @@
transition: 0.1s;
z-index: 999;
- &--visible {
+ &--visible,
+ &--focused {
opacity: 1;
pointer-events: all;
}
@@ -99,7 +100,8 @@
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 c3a975bee..a2d294fbf 100644
--- a/app/styles/components/status.scss
+++ b/app/styles/components/status.scss
@@ -434,7 +434,8 @@
background: var(--brand-color--med);
transition: 0.2s;
- &:hover {
+ &:hover,
+ &:focus {
background: hsla(var(--brand-color_hsl), 0.5);
text-decoration: none;
}
@@ -676,3 +677,22 @@ 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;
+ }
+ }
+ }
+}
\ No newline at end of file