Emoji Typeahead Enhancements (Feedback)

* Wrap around completion menu with arrow keys
* Replace emoji text when typing final colon
This commit is contained in:
Ken Powers 2019-07-23 19:19:00 -04:00 committed by Scott Nonnenberg
parent cd8421caf2
commit c952d628c1

View file

@ -16,7 +16,7 @@ import {
} from 'draft-js'; } from 'draft-js';
import Measure, { ContentRect } from 'react-measure'; import Measure, { ContentRect } from 'react-measure';
import { Manager, Popper, Reference } from 'react-popper'; import { Manager, Popper, Reference } from 'react-popper';
import { clamp, noop } from 'lodash'; import { head, noop, trimEnd } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import emojiRegex from 'emoji-regex'; import emojiRegex from 'emoji-regex';
import { Emoji } from './emoji/Emoji'; import { Emoji } from './emoji/Emoji';
@ -241,7 +241,18 @@ export const CompositionInput = ({
// Update the state to indicate emojiable text at the current position. // Update the state to indicate emojiable text at the current position.
const newSearchText = match ? match.trim().substr(1) : ''; const newSearchText = match ? match.trim().substr(1) : '';
if (newSearchText.length >= 2 && focusRef.current) { if (newSearchText.endsWith(':')) {
const bareText = trimEnd(newSearchText, ':');
const emoji = head(search(bareText));
if (emoji && bareText === emoji.short_name) {
handleEditorCommand('enter-emoji', newState, emoji);
// Prevent inserted colon from persisting to state
return;
} else {
resetEmojiResults();
}
} else if (newSearchText.length >= 2 && focusRef.current) {
setEmojiResults(search(newSearchText, 10)); setEmojiResults(search(newSearchText, 10));
setSearchText(newSearchText); setSearchText(newSearchText);
setEmojiResultsIndex(0); setEmojiResultsIndex(0);
@ -300,19 +311,31 @@ export const CompositionInput = ({
} }
if (dir === 'next') { if (dir === 'next') {
setEmojiResultsIndex( setEmojiResultsIndex(index => {
clamp(emojiResultsIndex + 1, 0, emojiResults.length - 1) const next = index + 1;
);
if (next >= emojiResults.length) {
return 0;
}
return next;
});
} }
if (dir === 'prev') { if (dir === 'prev') {
setEmojiResultsIndex( setEmojiResultsIndex(index => {
clamp(emojiResultsIndex - 1, 0, emojiResults.length - 1) const next = index - 1;
);
if (next < 0) {
return emojiResults.length - 1;
}
return next;
});
} }
} }
}, },
[setEmojiResultsIndex, emojiResultsIndex, emojiResults] [emojiResultsIndex, emojiResults]
); );
const handleEditorArrowKey = React.useCallback( const handleEditorArrowKey = React.useCallback(
@ -338,21 +361,18 @@ export const CompositionInput = ({
[resetEmojiResults, emojiResults] [resetEmojiResults, emojiResults]
); );
const getWordAtCaret = React.useCallback( const getWordAtCaret = React.useCallback((state = editorStateRef.current) => {
() => { const selection = state.getSelection();
const selection = editorState.getSelection();
const index = selection.getAnchorOffset(); const index = selection.getAnchorOffset();
return getWordAtIndex( return getWordAtIndex(
editorState state
.getCurrentContent() .getCurrentContent()
.getBlockForKey(selection.getAnchorKey()) .getBlockForKey(selection.getAnchorKey())
.getText(), .getText(),
index index
); );
}, }, []);
[editorState]
);
const insertEmoji = React.useCallback( const insertEmoji = React.useCallback(
(e: EmojiPickDataType, replaceWord: boolean = false) => { (e: EmojiPickDataType, replaceWord: boolean = false) => {
@ -409,14 +429,16 @@ export const CompositionInput = ({
const handleEditorCommand = React.useCallback( const handleEditorCommand = React.useCallback(
( (
command: CompositionInputEditorCommand, command: CompositionInputEditorCommand,
state: EditorState state: EditorState,
emojiOverride?: EmojiData
): DraftHandleValue => { ): DraftHandleValue => {
if (command === 'enter-emoji') { if (command === 'enter-emoji') {
const shortName = emojiResults[emojiResultsIndex].short_name; const { short_name: shortName } =
emojiOverride || emojiResults[emojiResultsIndex];
const content = state.getCurrentContent(); const content = state.getCurrentContent();
const selection = state.getSelection(); const selection = state.getSelection();
const word = getWordAtCaret(); const word = getWordAtCaret(state);
const emojiContent = convertShortName(shortName, skinTone); const emojiContent = convertShortName(shortName, skinTone);
const emojiEntityKey = content const emojiEntityKey = content
.createEntity('emoji', 'IMMUTABLE', { .createEntity('emoji', 'IMMUTABLE', {