Merge branch 'accessible-emoji-picker' into 'develop'
Keyboard-accessible emoji picker See merge request soapbox-pub/soapbox-fe!634
This commit is contained in:
commit
b22f20a390
|
@ -2,55 +2,80 @@
|
||||||
|
|
||||||
exports[`<EmojiSelector /> renders correctly 1`] = `
|
exports[`<EmojiSelector /> renders correctly 1`] = `
|
||||||
<div
|
<div
|
||||||
className="emoji-react-selector"
|
onBlur={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
tabIndex="-1"
|
||||||
>
|
>
|
||||||
<button
|
<div
|
||||||
className="emoji-react-selector__emoji"
|
className="emoji-react-selector"
|
||||||
dangerouslySetInnerHTML={
|
onBlur={[Function]}
|
||||||
Object {
|
>
|
||||||
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"👍\\" title=\\":+1:\\" src=\\"/emoji/1f44d.svg\\" />",
|
<button
|
||||||
|
className="emoji-react-selector__emoji"
|
||||||
|
dangerouslySetInnerHTML={
|
||||||
|
Object {
|
||||||
|
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"👍\\" title=\\":+1:\\" src=\\"/emoji/1f44d.svg\\" />",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
onClick={[Function]}
|
||||||
/>
|
onKeyUp={[Function]}
|
||||||
<button
|
tabIndex={-1}
|
||||||
className="emoji-react-selector__emoji"
|
/>
|
||||||
dangerouslySetInnerHTML={
|
<button
|
||||||
Object {
|
className="emoji-react-selector__emoji"
|
||||||
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"❤\\" title=\\":heart:\\" src=\\"/emoji/2764.svg\\" />",
|
dangerouslySetInnerHTML={
|
||||||
|
Object {
|
||||||
|
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"❤\\" title=\\":heart:\\" src=\\"/emoji/2764.svg\\" />",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
onClick={[Function]}
|
||||||
/>
|
onKeyUp={[Function]}
|
||||||
<button
|
tabIndex={-1}
|
||||||
className="emoji-react-selector__emoji"
|
/>
|
||||||
dangerouslySetInnerHTML={
|
<button
|
||||||
Object {
|
className="emoji-react-selector__emoji"
|
||||||
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😆\\" title=\\":laughing:\\" src=\\"/emoji/1f606.svg\\" />",
|
dangerouslySetInnerHTML={
|
||||||
|
Object {
|
||||||
|
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😆\\" title=\\":laughing:\\" src=\\"/emoji/1f606.svg\\" />",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
onClick={[Function]}
|
||||||
/>
|
onKeyUp={[Function]}
|
||||||
<button
|
tabIndex={-1}
|
||||||
className="emoji-react-selector__emoji"
|
/>
|
||||||
dangerouslySetInnerHTML={
|
<button
|
||||||
Object {
|
className="emoji-react-selector__emoji"
|
||||||
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😮\\" title=\\":open_mouth:\\" src=\\"/emoji/1f62e.svg\\" />",
|
dangerouslySetInnerHTML={
|
||||||
|
Object {
|
||||||
|
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😮\\" title=\\":open_mouth:\\" src=\\"/emoji/1f62e.svg\\" />",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
onClick={[Function]}
|
||||||
/>
|
onKeyUp={[Function]}
|
||||||
<button
|
tabIndex={-1}
|
||||||
className="emoji-react-selector__emoji"
|
/>
|
||||||
dangerouslySetInnerHTML={
|
<button
|
||||||
Object {
|
className="emoji-react-selector__emoji"
|
||||||
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😢\\" title=\\":cry:\\" src=\\"/emoji/1f622.svg\\" />",
|
dangerouslySetInnerHTML={
|
||||||
|
Object {
|
||||||
|
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😢\\" title=\\":cry:\\" src=\\"/emoji/1f622.svg\\" />",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
onClick={[Function]}
|
||||||
/>
|
onKeyUp={[Function]}
|
||||||
<button
|
tabIndex={-1}
|
||||||
className="emoji-react-selector__emoji"
|
/>
|
||||||
dangerouslySetInnerHTML={
|
<button
|
||||||
Object {
|
className="emoji-react-selector__emoji"
|
||||||
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😩\\" title=\\":weary:\\" src=\\"/emoji/1f629.svg\\" />",
|
dangerouslySetInnerHTML={
|
||||||
|
Object {
|
||||||
|
"__html": "<img draggable=\\"false\\" class=\\"emojione\\" alt=\\"😩\\" title=\\":weary:\\" src=\\"/emoji/1f629.svg\\" />",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
onClick={[Function]}
|
||||||
/>
|
onKeyUp={[Function]}
|
||||||
|
tabIndex={-1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -4,7 +4,10 @@ import EmojiSelector from '../emoji_selector';
|
||||||
|
|
||||||
describe('<EmojiSelector />', () => {
|
describe('<EmojiSelector />', () => {
|
||||||
it('renders correctly', () => {
|
it('renders correctly', () => {
|
||||||
const component = createComponent(<EmojiSelector />);
|
const children = <EmojiSelector />;
|
||||||
|
children.__proto__.addEventListener = () => {};
|
||||||
|
|
||||||
|
const component = createComponent(children, {}, true);
|
||||||
const tree = component.toJSON();
|
const tree = component.toJSON();
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { HotKeys } from 'react-hotkeys';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import emojify from 'soapbox/features/emoji/emoji';
|
import emojify from 'soapbox/features/emoji/emoji';
|
||||||
|
@ -15,28 +16,89 @@ class EmojiSelector extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onReact: PropTypes.func.isRequired,
|
onReact: PropTypes.func.isRequired,
|
||||||
|
onUnfocus: PropTypes.func,
|
||||||
visible: PropTypes.bool,
|
visible: PropTypes.bool,
|
||||||
|
focused: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onReact: () => {},
|
onReact: () => {},
|
||||||
|
onUnfocus: () => {},
|
||||||
visible: false,
|
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 => {
|
||||||
|
const { onUnfocus } = this.props;
|
||||||
|
|
||||||
|
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;
|
||||||
|
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() {
|
render() {
|
||||||
const { onReact, visible, allowedEmoji } = this.props;
|
const { visible, focused, allowedEmoji } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames('emoji-react-selector', { 'emoji-react-selector--visible': visible })}>
|
<HotKeys
|
||||||
{allowedEmoji.map((emoji, i) => (
|
handlers={this.handlers}
|
||||||
<button
|
>
|
||||||
key={i}
|
<div
|
||||||
className='emoji-react-selector__emoji'
|
className={classNames('emoji-react-selector', { 'emoji-react-selector--visible': visible, 'emoji-react-selector--focused': focused })}
|
||||||
dangerouslySetInnerHTML={{ __html: emojify(emoji) }}
|
onBlur={this.handleBlur}
|
||||||
onClick={onReact(emoji)}
|
ref={this.setRef}
|
||||||
/>
|
>
|
||||||
))}
|
{allowedEmoji.map((emoji, i) => (
|
||||||
</div>
|
<button
|
||||||
|
key={i}
|
||||||
|
className='emoji-react-selector__emoji'
|
||||||
|
dangerouslySetInnerHTML={{ __html: emojify(emoji) }}
|
||||||
|
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 = {
|
state = {
|
||||||
showMedia: defaultMediaVisibility(this.props.status, this.props.displayMedia),
|
showMedia: defaultMediaVisibility(this.props.status, this.props.displayMedia),
|
||||||
statusId: undefined,
|
statusId: undefined,
|
||||||
|
emojiSelectorFocused: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Track height changes we know about to compensate scrolling
|
// Track height changes we know about to compensate scrolling
|
||||||
|
@ -255,6 +256,27 @@ class Status extends ImmutablePureComponent {
|
||||||
this.handleToggleMediaVisibility();
|
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() {
|
_properStatus() {
|
||||||
const { status } = this.props;
|
const { status } = this.props;
|
||||||
|
|
||||||
|
@ -278,6 +300,7 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
let { status, account, ...other } = this.props;
|
let { status, account, ...other } = this.props;
|
||||||
|
|
||||||
|
|
||||||
if (status === null) {
|
if (status === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -443,6 +466,7 @@ class Status extends ImmutablePureComponent {
|
||||||
moveDown: this.handleHotkeyMoveDown,
|
moveDown: this.handleHotkeyMoveDown,
|
||||||
toggleHidden: this.handleHotkeyToggleHidden,
|
toggleHidden: this.handleHotkeyToggleHidden,
|
||||||
toggleSensitive: this.handleHotkeyToggleSensitive,
|
toggleSensitive: this.handleHotkeyToggleSensitive,
|
||||||
|
react: this.handleHotkeyReact,
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusUrl = `/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`;
|
const statusUrl = `/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`;
|
||||||
|
@ -506,7 +530,13 @@ class Status extends ImmutablePureComponent {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<StatusActionBar status={status} account={account} {...other} />
|
<StatusActionBar
|
||||||
|
status={status}
|
||||||
|
account={account}
|
||||||
|
emojiSelectorFocused={this.state.emojiSelectorFocused}
|
||||||
|
handleEmojiSelectorUnfocus={this.handleEmojiSelectorUnfocus}
|
||||||
|
{...other}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
|
|
|
@ -91,6 +91,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
isStaff: PropTypes.bool.isRequired,
|
isStaff: PropTypes.bool.isRequired,
|
||||||
isAdmin: PropTypes.bool.isRequired,
|
isAdmin: PropTypes.bool.isRequired,
|
||||||
allowedEmoji: ImmutablePropTypes.list,
|
allowedEmoji: ImmutablePropTypes.list,
|
||||||
|
emojiSelectorFocused: PropTypes.bool,
|
||||||
|
handleEmojiSelectorUnfocus: PropTypes.func.isRequired,
|
||||||
|
emojiSelectorFocused: PropTypes.bool,
|
||||||
|
handleEmojiSelectorUnfocus: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -106,6 +110,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
updateOnProps = [
|
updateOnProps = [
|
||||||
'status',
|
'status',
|
||||||
'withDismiss',
|
'withDismiss',
|
||||||
|
'emojiSelectorFocused',
|
||||||
]
|
]
|
||||||
|
|
||||||
handleReplyClick = () => {
|
handleReplyClick = () => {
|
||||||
|
@ -359,7 +364,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { status, intl, allowedEmoji } = this.props;
|
const { status, intl, allowedEmoji, emojiSelectorFocused, handleEmojiSelectorUnfocus } = this.props;
|
||||||
const { emojiSelectorVisible } = this.state;
|
const { emojiSelectorVisible } = this.state;
|
||||||
|
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
|
@ -422,7 +427,12 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
onMouseLeave={this.handleLikeButtonLeave}
|
onMouseLeave={this.handleLikeButtonLeave}
|
||||||
ref={this.setRef}
|
ref={this.setRef}
|
||||||
>
|
>
|
||||||
<EmojiSelector onReact={this.handleReactClick} visible={emojiSelectorVisible} />
|
<EmojiSelector
|
||||||
|
onReact={this.handleReactClick}
|
||||||
|
visible={emojiSelectorVisible}
|
||||||
|
focused={emojiSelectorFocused}
|
||||||
|
onUnfocus={handleEmojiSelectorUnfocus}
|
||||||
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
className='status__action-bar-button star-icon'
|
className='status__action-bar-button star-icon'
|
||||||
animate
|
animate
|
||||||
|
|
|
@ -46,9 +46,9 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
|
|
||||||
getCurrentStatusIndex = (id, featured) => {
|
getCurrentStatusIndex = (id, featured) => {
|
||||||
if (featured) {
|
if (featured) {
|
||||||
return this.props.featuredStatusIds.indexOf(id);
|
return this.props.featuredStatusIds.keySeq().findIndex(key => key === id);
|
||||||
} else {
|
} else {
|
||||||
return this.props.statusIds.indexOf(id) + this.getFeaturedStatusCount();
|
return this.props.statusIds.keySeq().findIndex(key => key === id) + this.getFeaturedStatusCount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ const messages = defineMessages({
|
||||||
reactionOpenMouth: { id: 'status.reactions.open_mouth', defaultMessage: 'Wow' },
|
reactionOpenMouth: { id: 'status.reactions.open_mouth', defaultMessage: 'Wow' },
|
||||||
reactionCry: { id: 'status.reactions.cry', defaultMessage: 'Sad' },
|
reactionCry: { id: 'status.reactions.cry', defaultMessage: 'Sad' },
|
||||||
reactionWeary: { id: 'status.reactions.weary', defaultMessage: 'Weary' },
|
reactionWeary: { id: 'status.reactions.weary', defaultMessage: 'Weary' },
|
||||||
|
emojiPickerExpand: { id: 'status.reactions_expand', defaultMessage: 'Select emoji' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
|
@ -99,6 +100,9 @@ class ActionBar extends React.PureComponent {
|
||||||
isStaff: PropTypes.bool.isRequired,
|
isStaff: PropTypes.bool.isRequired,
|
||||||
isAdmin: PropTypes.bool.isRequired,
|
isAdmin: PropTypes.bool.isRequired,
|
||||||
allowedEmoji: ImmutablePropTypes.list,
|
allowedEmoji: ImmutablePropTypes.list,
|
||||||
|
emojiSelectorFocused: PropTypes.bool,
|
||||||
|
handleEmojiSelectorExpand: PropTypes.func.isRequired,
|
||||||
|
handleEmojiSelectorUnfocus: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -107,6 +111,7 @@ class ActionBar extends React.PureComponent {
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
emojiSelectorVisible: false,
|
emojiSelectorVisible: false,
|
||||||
|
emojiSelectorFocused: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReplyClick = () => {
|
handleReplyClick = () => {
|
||||||
|
@ -169,10 +174,16 @@ class ActionBar extends React.PureComponent {
|
||||||
} else {
|
} else {
|
||||||
this.props.onOpenUnauthorizedModal();
|
this.props.onOpenUnauthorizedModal();
|
||||||
}
|
}
|
||||||
this.setState({ emojiSelectorVisible: false });
|
this.setState({ emojiSelectorVisible: false, emojiSelectorFocused: false });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleHotkeyEmoji = () => {
|
||||||
|
const { emojiSelectorVisible } = this.state;
|
||||||
|
|
||||||
|
this.setState({ emojiSelectorVisible: !emojiSelectorVisible });
|
||||||
|
}
|
||||||
|
|
||||||
handleDeleteClick = () => {
|
handleDeleteClick = () => {
|
||||||
this.props.onDelete(this.props.status, this.context.router.history);
|
this.props.onDelete(this.props.status, this.context.router.history);
|
||||||
}
|
}
|
||||||
|
@ -262,12 +273,12 @@ class ActionBar extends React.PureComponent {
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.addEventListener('click', e => {
|
document.addEventListener('click', e => {
|
||||||
if (this.node && !this.node.contains(e.target))
|
if (this.node && !this.node.contains(e.target))
|
||||||
this.setState({ emojiSelectorVisible: false });
|
this.setState({ emojiSelectorVisible: false, emojiSelectorFocused: false });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { status, intl, me, isStaff, isAdmin, allowedEmoji } = this.props;
|
const { status, intl, me, isStaff, isAdmin, allowedEmoji, emojiSelectorFocused, handleEmojiSelectorExpand, handleEmojiSelectorUnfocus } = this.props;
|
||||||
const { emojiSelectorVisible } = this.state;
|
const { emojiSelectorVisible } = this.state;
|
||||||
const ownAccount = status.getIn(['account', 'id']) === me;
|
const ownAccount = status.getIn(['account', 'id']) === me;
|
||||||
|
|
||||||
|
@ -351,6 +362,7 @@ class ActionBar extends React.PureComponent {
|
||||||
|
|
||||||
let reblog_disabled = (status.get('visibility') === 'direct' || status.get('visibility') === 'private');
|
let reblog_disabled = (status.get('visibility') === 'direct' || status.get('visibility') === 'private');
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='detailed-status__action-bar'>
|
<div className='detailed-status__action-bar'>
|
||||||
<div className='detailed-status__button'>
|
<div className='detailed-status__button'>
|
||||||
|
@ -377,7 +389,12 @@ class ActionBar extends React.PureComponent {
|
||||||
onMouseLeave={this.handleLikeButtonLeave}
|
onMouseLeave={this.handleLikeButtonLeave}
|
||||||
ref={this.setRef}
|
ref={this.setRef}
|
||||||
>
|
>
|
||||||
<EmojiSelector onReact={this.handleReactClick} visible={emojiSelectorVisible} />
|
<EmojiSelector
|
||||||
|
onReact={this.handleReactClick}
|
||||||
|
visible={emojiSelectorVisible}
|
||||||
|
focused={emojiSelectorFocused}
|
||||||
|
onUnfocus={handleEmojiSelectorUnfocus}
|
||||||
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
className='star-icon'
|
className='star-icon'
|
||||||
animate
|
animate
|
||||||
|
@ -388,6 +405,14 @@ class ActionBar extends React.PureComponent {
|
||||||
text={meEmojiTitle}
|
text={meEmojiTitle}
|
||||||
onClick={this.handleLikeButtonClick}
|
onClick={this.handleLikeButtonClick}
|
||||||
/>
|
/>
|
||||||
|
<IconButton
|
||||||
|
className='emoji-picker-expand'
|
||||||
|
animate
|
||||||
|
title={intl.formatMessage(messages.emojiPickerExpand)}
|
||||||
|
icon='caret-down'
|
||||||
|
onKeyUp={handleEmojiSelectorExpand}
|
||||||
|
onHover
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{shareButton}
|
{shareButton}
|
||||||
|
|
||||||
|
@ -400,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,
|
fullscreen: false,
|
||||||
showMedia: defaultMediaVisibility(this.props.status, this.props.displayMedia),
|
showMedia: defaultMediaVisibility(this.props.status, this.props.displayMedia),
|
||||||
loadedStatusId: undefined,
|
loadedStatusId: undefined,
|
||||||
|
emojiSelectorFocused: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -363,6 +364,10 @@ class Status extends ImmutablePureComponent {
|
||||||
this.handleToggleMediaVisibility();
|
this.handleToggleMediaVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleHotkeyReact = () => {
|
||||||
|
this._expandEmojiSelector();
|
||||||
|
}
|
||||||
|
|
||||||
handleMoveUp = id => {
|
handleMoveUp = id => {
|
||||||
const { status, ancestorsIds, descendantsIds } = this.props;
|
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) {
|
_selectChild(index, align_top) {
|
||||||
const container = this.node;
|
const container = this.node;
|
||||||
const element = container.querySelectorAll('.focusable')[index];
|
const element = container.querySelectorAll('.focusable')[index];
|
||||||
|
@ -445,6 +467,10 @@ class Status extends ImmutablePureComponent {
|
||||||
this.node = c;
|
this.node = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setStatusRef = c => {
|
||||||
|
this.status = c;
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
const { params, status } = this.props;
|
const { params, status } = this.props;
|
||||||
const { ancestorsIds } = prevProps;
|
const { ancestorsIds } = prevProps;
|
||||||
|
@ -510,6 +536,7 @@ class Status extends ImmutablePureComponent {
|
||||||
openProfile: this.handleHotkeyOpenProfile,
|
openProfile: this.handleHotkeyOpenProfile,
|
||||||
toggleHidden: this.handleHotkeyToggleHidden,
|
toggleHidden: this.handleHotkeyToggleHidden,
|
||||||
toggleSensitive: this.handleHotkeyToggleSensitive,
|
toggleSensitive: this.handleHotkeyToggleSensitive,
|
||||||
|
react: this.handleHotkeyReact,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -537,7 +564,7 @@ class Status extends ImmutablePureComponent {
|
||||||
{ancestors}
|
{ancestors}
|
||||||
|
|
||||||
<HotKeys handlers={handlers}>
|
<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
|
<DetailedStatus
|
||||||
status={status}
|
status={status}
|
||||||
onOpenVideo={this.handleOpenVideo}
|
onOpenVideo={this.handleOpenVideo}
|
||||||
|
@ -569,6 +596,9 @@ class Status extends ImmutablePureComponent {
|
||||||
onToggleStatusSensitivity={this.handleToggleStatusSensitivity}
|
onToggleStatusSensitivity={this.handleToggleStatusSensitivity}
|
||||||
onDeleteStatus={this.handleDeleteStatus}
|
onDeleteStatus={this.handleDeleteStatus}
|
||||||
allowedEmoji={this.props.allowedEmoji}
|
allowedEmoji={this.props.allowedEmoji}
|
||||||
|
emojiSelectorFocused={this.state.emojiSelectorFocused}
|
||||||
|
handleEmojiSelectorExpand={this.handleEmojiSelectorExpand}
|
||||||
|
handleEmojiSelectorUnfocus={this.handleEmojiSelectorUnfocus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</HotKeys>
|
</HotKeys>
|
||||||
|
|
|
@ -49,6 +49,10 @@ class HotkeysModal extends ImmutablePureComponent {
|
||||||
<td><kbd>f</kbd></td>
|
<td><kbd>f</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.favourite' defaultMessage='to like' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.favourite' defaultMessage='to like' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><kbd>e</kbd></td>
|
||||||
|
<td><FormattedMessage id='keyboard_shortcuts.react' defaultMessage='to react' /></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><kbd>b</kbd></td>
|
<td><kbd>b</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.boost' defaultMessage='to repost' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.boost' defaultMessage='to repost' /></td>
|
||||||
|
|
|
@ -130,6 +130,7 @@ const keyMap = {
|
||||||
forceNew: 'option+n',
|
forceNew: 'option+n',
|
||||||
reply: 'r',
|
reply: 'r',
|
||||||
favourite: 'f',
|
favourite: 'f',
|
||||||
|
react: 'e',
|
||||||
boost: 'b',
|
boost: 'b',
|
||||||
mention: 'm',
|
mention: 'm',
|
||||||
open: ['enter', 'o'],
|
open: ['enter', 'o'],
|
||||||
|
|
|
@ -403,6 +403,7 @@
|
||||||
"keyboard_shortcuts.notifications": "aby otworzyć kolumnę powiadomień",
|
"keyboard_shortcuts.notifications": "aby otworzyć kolumnę powiadomień",
|
||||||
"keyboard_shortcuts.pinned": "aby przejść do listy przypiętych wpisów",
|
"keyboard_shortcuts.pinned": "aby przejść do listy przypiętych wpisów",
|
||||||
"keyboard_shortcuts.profile": "aby przejść do profilu autora wpisu",
|
"keyboard_shortcuts.profile": "aby przejść do profilu autora wpisu",
|
||||||
|
"keyboard_shortcuts.react": "aby zareagować na wpis",
|
||||||
"keyboard_shortcuts.reply": "aby odpowiedzieć",
|
"keyboard_shortcuts.reply": "aby odpowiedzieć",
|
||||||
"keyboard_shortcuts.requests": "aby przejść do listy próśb o możliwość śledzenia",
|
"keyboard_shortcuts.requests": "aby przejść do listy próśb o możliwość śledzenia",
|
||||||
"keyboard_shortcuts.search": "aby przejść do pola wyszukiwania",
|
"keyboard_shortcuts.search": "aby przejść do pola wyszukiwania",
|
||||||
|
@ -707,6 +708,7 @@
|
||||||
"status.reactions.like": "Lubię",
|
"status.reactions.like": "Lubię",
|
||||||
"status.reactions.open_mouth": "Wow",
|
"status.reactions.open_mouth": "Wow",
|
||||||
"status.reactions.weary": "Nuda…",
|
"status.reactions.weary": "Nuda…",
|
||||||
|
"status.reactions_expand": "Wybierz emoji",
|
||||||
"status.read_more": "Czytaj dalej",
|
"status.read_more": "Czytaj dalej",
|
||||||
"status.reblog": "Podbij",
|
"status.reblog": "Podbij",
|
||||||
"status.reblog_private": "Podbij dla odbiorców oryginalnego wpisu",
|
"status.reblog_private": "Podbij dla odbiorców oryginalnego wpisu",
|
||||||
|
|
|
@ -94,6 +94,22 @@
|
||||||
transform: translateY(-1px);
|
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 {
|
.detailed-status__wrapper {
|
||||||
|
|
|
@ -80,7 +80,8 @@
|
||||||
transition: 0.1s;
|
transition: 0.1s;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
|
|
||||||
&--visible {
|
&--visible,
|
||||||
|
&--focused {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
@ -99,7 +100,8 @@
|
||||||
transition: 0.1s;
|
transition: 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
|
&:focus {
|
||||||
img {
|
img {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
|
|
|
@ -677,3 +677,22 @@ a.status-card.compact:hover {
|
||||||
border-radius: 4px;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue