fix(composer-emojis): Add spec and allow adjacent emojis

Summary:
Adds tests to the emoji picker.

The emoji picker should also now be able to add emojis consecutively (without spaces).

Test Plan: Tests included in diff.

Reviewers: evan, bengotow

Differential Revision: https://phab.nylas.com/D2551
This commit is contained in:
Jackie Luo 2016-02-09 13:27:41 -08:00
parent c4749f592e
commit e53b481a25
3 changed files with 150 additions and 35 deletions

View file

@ -14,7 +14,7 @@ class EmojisComposerExtension extends ContenteditableExtension {
const offset = sel.anchorOffset; const offset = sel.anchorOffset;
if (!DOMUtils.closest(sel.anchorNode, "n1-emoji-autocomplete")) { if (!DOMUtils.closest(sel.anchorNode, "n1-emoji-autocomplete")) {
editor.select(sel.anchorNode, editor.select(sel.anchorNode,
sel.anchorOffset - triggerWord.length, sel.anchorOffset - triggerWord.length - 1,
sel.focusNode, sel.focusNode,
sel.focusOffset).wrapSelection("n1-emoji-autocomplete"); sel.focusOffset).wrapSelection("n1-emoji-autocomplete");
editor.select(sel.anchorNode, editor.select(sel.anchorNode,
@ -26,9 +26,9 @@ class EmojisComposerExtension extends ContenteditableExtension {
if (DOMUtils.closest(sel.anchorNode, "n1-emoji-autocomplete")) { if (DOMUtils.closest(sel.anchorNode, "n1-emoji-autocomplete")) {
editor.unwrapNodeAndSelectAll(DOMUtils.closest(sel.anchorNode, "n1-emoji-autocomplete")); editor.unwrapNodeAndSelectAll(DOMUtils.closest(sel.anchorNode, "n1-emoji-autocomplete"));
editor.select(sel.anchorNode, editor.select(sel.anchorNode,
sel.anchorOffset + triggerWord.length, sel.anchorOffset + triggerWord.length + 1,
sel.focusNode, sel.focusNode,
sel.focusOffset + triggerWord.length); sel.focusOffset + triggerWord.length + 1);
} }
} }
} else { } else {
@ -49,9 +49,8 @@ class EmojisComposerExtension extends ContenteditableExtension {
if (emojiOptions.length > 0 && !toolbarState.dragging && !toolbarState.doubleDown) { if (emojiOptions.length > 0 && !toolbarState.dragging && !toolbarState.doubleDown) {
const locationRefNode = DOMUtils.closest(sel.anchorNode, const locationRefNode = DOMUtils.closest(sel.anchorNode,
"n1-emoji-autocomplete"); "n1-emoji-autocomplete");
const emojiNameNode = DOMUtils.closest(sel.anchorNode, if (!locationRefNode) return null;
"n1-emoji-autocomplete"); const selectedEmoji = locationRefNode.getAttribute("selectedEmoji");
const selectedEmoji = emojiNameNode.getAttribute("selectedEmoji");
return { return {
component: EmojiPicker, component: EmojiPicker,
props: {emojiOptions, props: {emojiOptions,
@ -111,22 +110,22 @@ class EmojisComposerExtension extends ContenteditableExtension {
sel.anchorNode.nodeValue && sel.anchorNode.nodeValue &&
sel.anchorNode.nodeValue.length > 0 && sel.anchorNode.nodeValue.length > 0 &&
sel.isCollapsed) { sel.isCollapsed) {
const words = sel.anchorNode.nodeValue.substring(0, sel.anchorOffset).split(" "); const words = sel.anchorNode.nodeValue.substring(0, sel.anchorOffset);
let lastWord = words[words.length - 1].trim(); let index = words.lastIndexOf(":");
if (words.length === 1 && let lastWord = "";
lastWord.indexOf(" ") === -1 && if (index !== -1 && words.lastIndexOf(" ") < index) {
lastWord.indexOf(":") === -1) { lastWord = words.substring(index + 1, sel.anchorOffset);
} else {
const {text} = EmojisComposerExtension._getTextUntilSpace(sel.anchorNode, sel.anchorOffset); const {text} = EmojisComposerExtension._getTextUntilSpace(sel.anchorNode, sel.anchorOffset);
lastWord = text; index = text.lastIndexOf(":");
} if (index !== -1 && text.lastIndexOf(" ") < index) {
if (lastWord.length > 1 && lastWord = text.substring(index + 1);
lastWord.charAt(0) === ":" & } else {
lastWord.charAt(lastWord.length - 1) !== " ") { return {triggerWord: "", emojiOptions: []};
let word = lastWord.substring(1);
if (lastWord.charAt(lastWord.length - 1) === ":") {
word = word.substring(0, word.length - 1);
} }
return {triggerWord: lastWord, emojiOptions: EmojisComposerExtension._findMatches(word)}; }
if (lastWord.length > 0) {
return {triggerWord: lastWord, emojiOptions: EmojisComposerExtension._findMatches(lastWord)};
} }
return {triggerWord: lastWord, emojiOptions: []}; return {triggerWord: lastWord, emojiOptions: []};
} }
@ -140,22 +139,22 @@ class EmojisComposerExtension extends ContenteditableExtension {
if (sel.anchorNode && if (sel.anchorNode &&
sel.anchorNode.nodeValue && sel.anchorNode.nodeValue &&
sel.anchorNode.nodeValue.length > 0 && sel.anchorNode.nodeValue.length > 0 &&
sel.isCollapsed) { sel.isCollapsed) {
const words = sel.anchorNode.nodeValue.substring(0, sel.anchorOffset).split(" "); const words = sel.anchorNode.nodeValue.substring(0, sel.anchorOffset);
let lastWord = words[words.length - 1].trim(); let index = words.lastIndexOf(":");
if (words.length === 1 && let lastWord = words.substring(index + 1, sel.anchorOffset);
lastWord.indexOf(" ") === -1 && if (index !== -1 && words.lastIndexOf(" ") < index) {
lastWord.indexOf(":") === -1) { editor.select(sel.anchorNode,
const {text, textNode} = EmojisComposerExtension._getTextUntilSpace(sel.anchorNode, sel.anchorOffset); sel.anchorOffset - lastWord.length - 1,
lastWord = text;
const offset = textNode.nodeValue.lastIndexOf(":");
editor.select(textNode,
offset,
sel.focusNode, sel.focusNode,
sel.focusOffset); sel.focusOffset);
} else { } else {
editor.select(sel.anchorNode, const {text, textNode} = EmojisComposerExtension._getTextUntilSpace(sel.anchorNode, sel.anchorOffset);
sel.anchorOffset - lastWord.length, index = text.lastIndexOf(":");
lastWord = text.substring(index + 1);
const offset = textNode.nodeValue.lastIndexOf(":");
editor.select(textNode,
offset,
sel.focusNode, sel.focusNode,
sel.focusOffset); sel.focusOffset);
} }

View file

@ -1,6 +1,6 @@
/** @babel */ /** @babel */
import {ExtensionRegistry} from 'nylas-exports'; import {ExtensionRegistry} from 'nylas-exports';
import EmojisComposerExtension from './emojis-composer-extension' import EmojisComposerExtension from './emojis-composer-extension';
export function activate() { export function activate() {
ExtensionRegistry.Composer.register(EmojisComposerExtension); ExtensionRegistry.Composer.register(EmojisComposerExtension);
@ -8,4 +8,4 @@ export function activate() {
export function deactivate() { export function deactivate() {
ExtensionRegistry.Composer.unregister(EmojisComposerExtension); ExtensionRegistry.Composer.unregister(EmojisComposerExtension);
} }

View file

@ -0,0 +1,116 @@
import React, {addons} from 'react/addons';
import {renderIntoDocument} from '../../../spec/nylas-test-utils';
import Contenteditable from '../../../src/components/contenteditable/contenteditable';
import EmojisComposerExtension from '../lib/emojis-composer-extension';
const ReactTestUtils = addons.TestUtils;
describe('EmojisComposerExtension', ()=> {
beforeEach(()=> {
spyOn(EmojisComposerExtension, 'onContentChanged').andCallThrough()
spyOn(EmojisComposerExtension, '_onSelectEmoji').andCallThrough()
const html = 'Testing!'
const onChange = jasmine.createSpy('onChange')
this.component = renderIntoDocument(
<Contenteditable html={html} onChange={onChange} extensions={[EmojisComposerExtension]}/>
)
this.editableNode = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithAttr(this.component, 'contentEditable'));
})
describe('when emoji trigger is typed', ()=> {
beforeEach(()=> {
this._performEdit = (newHTML) => {
this.editableNode.innerHTML = newHTML;
const sel = document.getSelection()
const textNode = this.editableNode.childNodes[0];
sel.setBaseAndExtent(textNode, textNode.nodeValue.length, textNode, textNode.nodeValue.length);
}
})
it('should show the emoji picker', ()=> {
runs(()=> {
this._performEdit('Testing! :h');
});
waitsFor(()=> {
return ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'emoji-picker').length > 0
});
})
it('should be focused on the first emoji in the list', ()=> {
runs(()=> {
this._performEdit('Testing! :h');
});
waitsFor(()=> {
return ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'emoji-option').length > 0
});
runs(()=> {
expect(React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(this.component, 'emoji-option')).textContent === "💇 :haircut:").toBe(true);
});
})
it('should insert an emoji on enter', ()=> {
runs(()=> {
this._performEdit('Testing! :h');
});
waitsFor(()=> {
return ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'emoji-picker').length > 0
});
runs(()=> {
ReactTestUtils.Simulate.keyDown(this.editableNode, {key: "Enter", keyCode: 13, which: 13});
});
waitsFor(()=> {
return EmojisComposerExtension._onSelectEmoji.calls.length > 0
})
runs(()=> {
expect(this.editableNode.textContent === "Testing! 💇").toBe(true);
});
})
it('should insert an emoji on click', ()=> {
runs(()=> {
this._performEdit('Testing! :h');
});
waitsFor(()=> {
return ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'emoji-picker').length > 0
});
runs(()=> {
const button = React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(this.component, 'emoji-option'))
ReactTestUtils.Simulate.mouseDown(button);
expect(EmojisComposerExtension._onSelectEmoji).toHaveBeenCalled()
});
waitsFor(()=> {
return EmojisComposerExtension._onSelectEmoji.calls.length > 0
})
runs(()=> {
expect(this.editableNode.textContent).toEqual("Testing! 💇");
});
})
it('should move to the next emoji on arrow down', ()=> {
runs(()=> {
this._performEdit('Testing! :h');
});
waitsFor(()=> {
return ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'emoji-option').length > 0
});
runs(()=> {
ReactTestUtils.Simulate.keyDown(this.editableNode, {key: "ArrowDown", keyCode: 40, which: 40});
});
waitsFor(()=> {
return EmojisComposerExtension.onContentChanged.calls.length > 1
});
runs(()=> {
expect(React.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(this.component, 'emoji-option')).textContent).toEqual("🍔 :hamburger:");
});
})
it('should be able to insert two emojis next to each other', ()=> {
runs(()=> {
this._performEdit('Testing! 🍔 :h');
});
waitsFor(()=> {
return ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'emoji-picker').length > 0
});
})
})
})