Keyboard-accessible emoji picker
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
13092271de
commit
5ee92b47ce
|
@ -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,17 +51,37 @@ 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 (
|
||||
<HotKeys
|
||||
handlers={this.handlers}
|
||||
>
|
||||
<div
|
||||
className={classNames('emoji-react-selector', { 'emoji-react-selector--visible': visible, 'emoji-react-selector--focused': focused })}
|
||||
onBlur={this.handleBlur}
|
||||
|
@ -69,12 +92,13 @@ class EmojiSelector extends ImmutablePureComponent {
|
|||
key={i}
|
||||
className='emoji-react-selector__emoji'
|
||||
dangerouslySetInnerHTML={{ __html: emojify(emoji) }}
|
||||
onClick={onReact(emoji)}
|
||||
onKeyUp={this.handleKeyUp(i)}
|
||||
onClick={this.handleReact(emoji)}
|
||||
onKeyUp={this.handleKeyUp(i, emoji)}
|
||||
tabIndex={(visible || focused) ? 0 : -1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
|||
</button>
|
||||
)}
|
||||
|
||||
<StatusActionBar status={status} account={account} {...other} />
|
||||
<StatusActionBar
|
||||
status={status}
|
||||
account={account}
|
||||
emojiSelectorFocused={this.state.emojiSelectorFocused}
|
||||
handleEmojiSelectorUnfocus={this.handleEmojiSelectorUnfocus}
|
||||
{...other}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</HotKeys>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
<EmojiSelector onReact={this.handleReactClick} visible={emojiSelectorVisible} />
|
||||
<EmojiSelector
|
||||
onReact={this.handleReactClick}
|
||||
visible={emojiSelectorVisible}
|
||||
focused={emojiSelectorFocused}
|
||||
onUnfocus={handleEmojiSelectorUnfocus}
|
||||
/>
|
||||
<IconButton
|
||||
className='status__action-bar-button star-icon'
|
||||
animate
|
||||
|
|
|
@ -46,9 +46,9 @@ export default class StatusList extends ImmutablePureComponent {
|
|||
|
||||
getCurrentStatusIndex = (id, featured) => {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -288,8 +278,8 @@ class ActionBar extends React.PureComponent {
|
|||
}
|
||||
|
||||
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}
|
||||
/>
|
||||
<IconButton
|
||||
className='star-icon'
|
||||
|
@ -420,7 +410,7 @@ class ActionBar extends React.PureComponent {
|
|||
animate
|
||||
title={intl.formatMessage(messages.emojiPickerExpand)}
|
||||
icon='caret-down'
|
||||
onKeyUp={this.handleEmojiSelectorExpand}
|
||||
onKeyUp={handleEmojiSelectorExpand}
|
||||
onHover
|
||||
/>
|
||||
</div>
|
||||
|
@ -435,4 +425,5 @@ class ActionBar extends React.PureComponent {
|
|||
|
||||
}
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ActionBar));
|
||||
export default injectIntl(
|
||||
connect(mapStateToProps, mapDispatchToProps)(ActionBar));
|
||||
|
|
|
@ -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}
|
||||
|
||||
<HotKeys handlers={handlers}>
|
||||
<div className={classNames('focusable', 'detailed-status__wrapper')} tabIndex='0' aria-label={textForScreenReader(intl, status, false)}>
|
||||
<div ref={this.setStatusRef} className={classNames('focusable', 'detailed-status__wrapper')} tabIndex='0' aria-label={textForScreenReader(intl, status, false)}>
|
||||
<DetailedStatus
|
||||
status={status}
|
||||
onOpenVideo={this.handleOpenVideo}
|
||||
|
@ -569,6 +596,9 @@ class Status extends ImmutablePureComponent {
|
|||
onToggleStatusSensitivity={this.handleToggleStatusSensitivity}
|
||||
onDeleteStatus={this.handleDeleteStatus}
|
||||
allowedEmoji={this.props.allowedEmoji}
|
||||
emojiSelectorFocused={this.state.emojiSelectorFocused}
|
||||
handleEmojiSelectorExpand={this.handleEmojiSelectorExpand}
|
||||
handleEmojiSelectorUnfocus={this.handleEmojiSelectorUnfocus}
|
||||
/>
|
||||
</div>
|
||||
</HotKeys>
|
||||
|
|
|
@ -49,6 +49,10 @@ class HotkeysModal extends ImmutablePureComponent {
|
|||
<td><kbd>f</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.favourite' defaultMessage='to like' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>e</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.react' defaultMessage='to react' /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><kbd>b</kbd></td>
|
||||
<td><FormattedMessage id='keyboard_shortcuts.boost' defaultMessage='to repost' /></td>
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue