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

View file

@ -1,6 +1,6 @@
/** @babel */
import {ExtensionRegistry} from 'nylas-exports';
import EmojisComposerExtension from './emojis-composer-extension'
import EmojisComposerExtension from './emojis-composer-extension';
export function activate() {
ExtensionRegistry.Composer.register(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
});
})
})
})