Accessible emoiji picker
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
8f53134b5e
commit
4d3f4c5680
6 changed files with 117 additions and 8 deletions
|
@ -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 (
|
||||
<div className={classNames('emoji-react-selector', { 'emoji-react-selector--visible': visible })}>
|
||||
<div
|
||||
className={classNames('emoji-react-selector', { 'emoji-react-selector--visible': visible, 'emoji-react-selector--focused': focused })}
|
||||
onBlur={this.handleBlur}
|
||||
ref={this.setRef}
|
||||
>
|
||||
{allowedEmoji.map((emoji, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className='emoji-react-selector__emoji'
|
||||
dangerouslySetInnerHTML={{ __html: emojify(emoji) }}
|
||||
onClick={onReact(emoji)}
|
||||
onKeyUp={this.handleKeyUp(i)}
|
||||
tabIndex={(visible || focused) ? 0 : -1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
<EmojiSelector onReact={this.handleReactClick} visible={emojiSelectorVisible} />
|
||||
<EmojiSelector
|
||||
onReact={this.handleReactClick}
|
||||
visible={emojiSelectorVisible}
|
||||
focused={emojiSelectorFocused}
|
||||
onUnfocus={this.handleEmojiSelectorUnfocus}
|
||||
/>
|
||||
<IconButton
|
||||
className='star-icon'
|
||||
animate
|
||||
|
@ -375,6 +395,14 @@ class ActionBar extends React.PureComponent {
|
|||
text={meEmojiTitle}
|
||||
onClick={this.handleLikeButtonClick}
|
||||
/>
|
||||
<IconButton
|
||||
className='emoji-picker-expand'
|
||||
animate
|
||||
title={intl.formatMessage(messages.emojiPickerExpand)}
|
||||
icon='caret-down'
|
||||
onKeyUp={this.handleEmojiSelectorExpand}
|
||||
onHover
|
||||
/>
|
||||
</div>
|
||||
{shareButton}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue