Refactor the emoji-picker somewhat

Trigger an `emojiSelected` event instead of manually calling `insertIntoTextArea` on the `converse-message-form` a component.
This loosens the coupling between the emoji picker and `converse-message-form`.

Call `disableArrowNavigation` when the emoji-picker is disconnected from
the DOM or when escape is pressed. See #2754
This commit is contained in:
JC Brand 2022-03-09 12:24:26 +01:00
parent 1c0ce25f12
commit e52056bb33
5 changed files with 52 additions and 36 deletions

View File

@ -15,9 +15,22 @@ export default class MessageForm extends ElementView {
await this.model.initialized;
this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting);
this.listenTo(this.model, 'change:composing_spoiler', () => this.render());
this.handleEmojiSelection = ({ detail }) => this.insertIntoTextArea(
detail.value,
detail.autocompleting,
false,
detail.ac_position
);
document.addEventListener("emojiSelected", this.handleEmojiSelection);
this.render();
}
disconnectedCallback () {
super.disconnectedCallback();
document.removeEventListener("emojiSelected", this.handleEmojiSelection);
}
toHTML () {
return tpl_message_form(
Object.assign(this.model.toJSON(), {

View File

@ -44,7 +44,7 @@ describe("Emojis", function () {
expect(input.value).toBe(':grimacing:');
// Check that ENTER now inserts the match
const enter_event = Object.assign({}, tab_event, {'keyCode': 13, 'key': 'Enter', 'target': input});
const enter_event = Object.assign({}, tab_event, {'keyCode': 13, 'key': 'Enter', 'target': input, 'bubbles': true});
input.dispatchEvent(new KeyboardEvent('keydown', enter_event));
await u.waitUntil(() => input.value === '');
@ -148,7 +148,7 @@ describe("Emojis", function () {
const input = picker.querySelector('.emoji-search');
input.dispatchEvent(new KeyboardEvent('keydown', tab_event));
await u.waitUntil(() => input.value === ':100:');
const enter_event = Object.assign({}, tab_event, {'keyCode': 13, 'key': 'Enter', 'target': input});
const enter_event = Object.assign({}, tab_event, {'keyCode': 13, 'key': 'Enter', 'target': input, 'bubbles': true});
input.dispatchEvent(new KeyboardEvent('keydown', enter_event));
expect(textarea.value).toBe(':100: ');
@ -193,7 +193,7 @@ describe("Emojis", function () {
expect(visible_emojis[1].getAttribute('data-emoji')).toBe(':smiley_cat:');
// Check that pressing enter without an unambiguous match does nothing
const enter_event = Object.assign({}, event, {'keyCode': 13});
const enter_event = Object.assign({}, event, {'keyCode': 13, 'bubbles': true});
input.dispatchEvent(new KeyboardEvent('keydown', enter_event));
expect(input.value).toBe('smiley');

View File

@ -58,6 +58,7 @@ export default class EmojiDropdown extends DropdownBase {
<converse-emoji-picker
.chatview=${this.chatview}
.model=${this.model}
@emojiSelected=${() => this.hideMenu()}
?render_emojis=${this.render_emojis}
current_category="${this.model.get('current_category') || ''}"
current_skintone="${this.model.get('current_skintone') || ''}"
@ -95,12 +96,6 @@ export default class EmojiDropdown extends DropdownBase {
super.showMenu();
setTimeout(() => this.querySelector('.emoji-search')?.focus());
}
hideMenu () {
this.chatview.querySelector('converse-emoji-picker')?.disableArrowNavigation();
super.hideMenu();
setTimeout(() => this.chatview.querySelector('.chat-textarea')?.focus());
}
}
api.elements.define('converse-emoji-dropdown', EmojiDropdown);

View File

@ -3,6 +3,7 @@ import './emoji-dropdown.js';
import DOMNavigator from "shared/dom-navigator";
import debounce from 'lodash-es/debounce';
import { CustomElement } from 'shared/components/element.js';
import { KEYCODES } from '@converse/headless/shared/constants.js';
import { _converse, api, converse } from "@converse/headless/core";
import { tpl_emoji_picker } from "./templates/emoji-picker.js";
@ -56,7 +57,7 @@ export default class EmojiPicker extends CustomElement {
'onCategoryPicked': ev => this.chooseCategory(ev),
'onSearchInputBlurred': ev => this.chatview.emitFocused(ev),
'onSearchInputFocus': ev => this.onSearchInputFocus(ev),
'onSearchInputKeyDown': ev => this.onKeyDown(ev),
'onSearchInputKeyDown': ev => this.onSearchInputKeyDown(ev),
'onSkintonePicked': ev => this.chooseSkinTone(ev),
'query': this.query,
'search_results': this.search_results,
@ -120,6 +121,7 @@ export default class EmojiPicker extends CustomElement {
disconnectedCallback() {
const body = document.querySelector('body');
body.removeEventListener('keydown', this.onGlobalKeyDown);
this.disableArrowNavigation();
super.disconnectedCallback();
}
@ -127,14 +129,15 @@ export default class EmojiPicker extends CustomElement {
if (!this.navigator) {
return;
}
if (ev.keyCode === converse.keycodes.ENTER &&
this.navigator.selected &&
u.isVisible(this)) {
if (ev.keyCode === KEYCODES.ENTER && u.isVisible(this)) {
this.onEnterPressed(ev);
} else if (ev.keyCode === converse.keycodes.DOWN_ARROW &&
} else if (ev.keyCode === KEYCODES.DOWN_ARROW &&
!this.navigator.enabled &&
u.isVisible(this)) {
this.enableArrowNavigation(ev);
} else if (ev.keyCode === KEYCODES.ESCAPE) {
this.disableArrowNavigation();
setTimeout(() => this.chatview.querySelector('.chat-textarea').focus(), 50);
}
}
@ -149,8 +152,13 @@ export default class EmojiPicker extends CustomElement {
insertIntoTextArea (value) {
const autocompleting = this.model.get('autocompleting');
const ac_position = this.model.get('ac_position');
this.chatview.getMessageForm().insertIntoTextArea(value, autocompleting, false, ac_position);
this.model.set({'autocompleting': null, 'query': '', 'ac_position': null});
this.disableArrowNavigation();
const options = {
'bubbles': true,
'detail': { value, autocompleting, ac_position }
};
this.dispatchEvent(new CustomEvent("emojiSelected", options));
}
chooseSkinTone (ev) {
@ -174,8 +182,8 @@ export default class EmojiPicker extends CustomElement {
!this.navigator.enabled && this.navigator.enable();
}
onKeyDown (ev) {
if (ev.keyCode === converse.keycodes.TAB) {
onSearchInputKeyDown (ev) {
if (ev.keyCode === KEYCODES.TAB) {
if (ev.target.value) {
ev.preventDefault();
const match = converse.emojis.shortnames.find(sn => _converse.FILTER_CONTAINS(sn, ev.target.value));
@ -183,31 +191,19 @@ export default class EmojiPicker extends CustomElement {
} else if (!this.navigator.enabled) {
this.enableArrowNavigation(ev);
}
} else if (ev.keyCode === converse.keycodes.DOWN_ARROW && !this.navigator.enabled) {
} else if (ev.keyCode === KEYCODES.DOWN_ARROW && !this.navigator.enabled) {
this.enableArrowNavigation(ev);
} else if (ev.keyCode === converse.keycodes.ENTER) {
this.onEnterPressed(ev);
} else if (ev.keyCode === converse.keycodes.ESCAPE) {
u.ancestor(this, 'converse-emoji-dropdown').hideMenu();
ev.stopPropagation();
ev.preventDefault();
} else if (
ev.keyCode !== converse.keycodes.ENTER &&
ev.keyCode !== converse.keycodes.DOWN_ARROW
ev.keyCode !== KEYCODES.ENTER &&
ev.keyCode !== KEYCODES.DOWN_ARROW
) {
this.debouncedFilter(ev.target);
}
}
onEnterPressed (ev) {
if (ev.emoji_keypress_handled) {
// Prevent the emoji from being inserted a 2nd time due to this
// method being called by two event handlers: onKeyDown and _onGlobalKeyDown
return;
}
ev.preventDefault();
ev.stopPropagation();
ev.emoji_keypress_handled = true;
if (converse.emojis.shortnames.includes(ev.target.value)) {
this.insertIntoTextArea(ev.target.value);
} else if (this.search_results.length === 1) {
@ -259,7 +255,7 @@ export default class EmojiPicker extends CustomElement {
}
disableArrowNavigation () {
this.navigator.disable();
this.navigator?.disable();
}
enableArrowNavigation (ev) {

View File

@ -1,9 +1,10 @@
import 'shared/components/icons.js';
import DOMNavigator from "shared/dom-navigator.js";
import { converse, api } from "@converse/headless/core";
import DropdownBase from 'shared/components/dropdownbase.js';
import { KEYCODES } from '@converse/headless/shared/constants.js';
import { api } from "@converse/headless/core";
import { html } from 'lit';
import { until } from 'lit/directives/until.js';
import DropdownBase from 'shared/components/dropdownbase.js';
import './styles/dropdown.scss';
@ -40,6 +41,17 @@ export default class Dropdown extends DropdownBase {
this.initArrowNavigation();
}
connectedCallback () {
super.connectedCallback();
this.hideOnEscape = ev => (ev.keyCode === KEYCODES.ESCAPE && this.hideMenu());
document.addEventListener('keydown', this.hideOnEscape);
}
disconnectedCallback() {
document.removeEventListener('keydown', this.hideOnEscape);
super.disconnectedCallback();
}
hideMenu () {
super.hideMenu();
this.navigator?.disable();
@ -66,7 +78,7 @@ export default class Dropdown extends DropdownBase {
handleKeyUp (ev) {
super.handleKeyUp(ev);
if (ev.keyCode === converse.keycodes.DOWN_ARROW && !this.navigator.enabled) {
if (ev.keyCode === KEYCODES.DOWN_ARROW && !this.navigator.enabled) {
this.enableArrowNavigation(ev);
}
}