From 1c058b72d83da3be1068961305577b0ad2ca1a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Theys?= Date: Fri, 26 Jun 2020 09:32:32 +0000 Subject: [PATCH] [FIX] mail: fix (inbox reply) composer event handlers (and discard) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit task-2092950 closes odoo/odoo#53731 X-original-commit: c29f5776594c4d2278ce163a78d8eb917fb672c0 Signed-off-by: Sébastien Theys (seb) Signed-off-by: Alexandre Kühn (aku) --- .../src/components/chat_window/chat_window.js | 11 + .../chat_window_manager_tests.js | 74 ++++- .../src/components/composer/composer.js | 72 ++++- .../src/components/composer/composer.xml | 6 +- .../src/components/composer/composer_tests.js | 2 +- .../composer_text_input.js | 26 +- .../static/src/components/discuss/discuss.xml | 1 - .../discuss/tests/discuss_inbox_tests.js | 300 ++++++++++++++++++ .../emojis_popover/emojis_popover.js | 23 +- .../static/src/components/message/message.js | 16 +- .../components/thread_viewer/thread_viewer.js | 2 +- .../src/models/chat_window/chat_window.js | 2 +- .../static/src/models/composer/composer.js | 23 +- .../mail/static/src/models/discuss/discuss.js | 42 ++- .../mail/static/src/models/message/message.js | 13 +- addons/mail/static/src/utils/utils.js | 32 ++ addons/mail/views/assets.xml | 1 + 17 files changed, 578 insertions(+), 68 deletions(-) create mode 100644 addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js diff --git a/addons/mail/static/src/components/chat_window/chat_window.js b/addons/mail/static/src/components/chat_window/chat_window.js index 8d1851c78fc..24801e5f301 100644 --- a/addons/mail/static/src/components/chat_window/chat_window.js +++ b/addons/mail/static/src/components/chat_window/chat_window.js @@ -7,6 +7,7 @@ const components = { ThreadViewer: require('mail/static/src/components/thread_viewer/thread_viewer.js'), }; const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); +const { isEventHandled } = require('mail/static/src/utils/utils.js'); const { Component } = owl; const { useRef } = owl.hooks; @@ -250,6 +251,10 @@ class ChatWindow extends Component { */ _onFocusinThread(ev) { ev.stopPropagation(); + if (!this.chatWindow) { + // prevent crash on destroy + return; + } this.chatWindow.update({ isFocused: true }); } @@ -293,6 +298,12 @@ class ChatWindow extends Component { } break; case 'Escape': + if (isEventHandled(ev, 'ComposerTextInput.closeMentionSuggestions')) { + break; + } + if (isEventHandled(ev, 'Composer.closeEmojisPopover')) { + break; + } ev.preventDefault(); this.chatWindow.close(); break; diff --git a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js index 1c51a669312..5bd516c5214 100644 --- a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js +++ b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js @@ -527,8 +527,13 @@ QUnit.test('chat window: open / close', async function (assert) { }); QUnit.test('chat window: close on ESCAPE', async function (assert) { - assert.expect(4); + assert.expect(12); + this.data['res.partner'].records = [{ + email: "testpartnert@odoo.com", + id: 11, + name: "TestPartner", + }]; Object.assign(this.data.initMessaging, { channel_slots: { channel_channel: [{ @@ -569,12 +574,73 @@ QUnit.test('chat window: close on ESCAPE', async function (assert) { document.querySelector(`.o_MessagingMenu_dropdownMenu .o_NotificationList_preview`).click() ); assert.verifySteps(['rpc:channel_fold/open']); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "chat window should be opened after click on preview" + ); + + await afterNextRender(() => + document.querySelector(`.o_Composer_buttonEmojis`).click() + ); + assert.containsOnce( + document.body, + '.o_EmojisPopover', + "emojis popover should be opened after click on emojis button" + ); await afterNextRender(() => { - document.querySelector(`.o_ChatWindow`).focus(); - const kevt = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); - document.querySelector(`.o_ChatWindow`).dispatchEvent(kevt); + const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); + document.querySelector(`.o_Composer_buttonEmojis`).dispatchEvent(ev); }); + assert.containsNone( + document.body, + '.o_EmojisPopover', + "emojis popover should be closed after pressing escape on emojis button" + ); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "chat window should still be opened after pressing escape on emojis button" + ); + + await afterNextRender(() => { + document.execCommand('insertText', false, "@"); + document.querySelector(`.o_ComposerTextInput_textarea`) + .dispatchEvent(new window.KeyboardEvent('keydown')); + document.querySelector(`.o_ComposerTextInput_textarea`) + .dispatchEvent(new window.KeyboardEvent('keyup')); + }); + assert.containsOnce( + document.body, + '.o_ComposerTextInput_mentionDropdownPropositionList', + "mention suggestion should be opened after typing @" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerTextInput_textarea`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + '.o_ComposerTextInput_mentionDropdownPropositionList', + "mention suggestion should be closed after pressing escape on mention suggestion" + ); + assert.containsOnce( + document.body, + '.o_ChatWindow', + "chat window should still be opened after pressing escape on mention suggestion" + ); + + await afterNextRender(() => { + const ev = new window.KeyboardEvent('keydown', { bubbles: true, key: "Escape" }); + document.querySelector(`.o_ComposerTextInput_textarea`).dispatchEvent(ev); + }); + assert.containsNone( + document.body, + '.o_ChatWindow', + "chat window should be closed after pressing escape if there was no other priority escape handler" + ); assert.verifySteps(['rpc:channel_fold/closed']); }); diff --git a/addons/mail/static/src/components/composer/composer.js b/addons/mail/static/src/components/composer/composer.js index 3fd82216b05..a0fe1490bc7 100644 --- a/addons/mail/static/src/components/composer/composer.js +++ b/addons/mail/static/src/components/composer/composer.js @@ -11,6 +11,10 @@ const components = { }; const useDragVisibleDropZone = require('mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js'); const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); +const { + isEventHandled, + markEventHandled, +} = require('mail/static/src/utils/utils.js'); const mailUtils = require('mail.utils'); const { Component } = owl; @@ -35,8 +39,8 @@ class Composer extends Component { }; }); /** - * Reference of the emoji button. Useful to include emoji popover as - * click "inside" the composer for the prop `isDiscardOnClickAway`. + * Reference of the emoji popover. Useful to include emoji popover as + * contained "inside" the composer. */ this._emojisPopoverRef = useRef('emojisPopover'); /** @@ -48,6 +52,7 @@ class Composer extends Component { * Reference of the text input component. */ this._textInputRef = useRef('textInput'); + this._onClickCaptureGlobal = this._onClickCaptureGlobal.bind(this); } @@ -70,6 +75,22 @@ class Composer extends Component { return this.env.models['mail.composer'].get(this.props.composerLocalId); } + /** + * Returns whether the given node is self or a children of self, including + * the emoji popover. + * + * @param {Node} node + * @returns {boolean} + */ + contains(node) { + // emoji popover is outside but should be considered inside + const emojisPopover = this._emojisPopoverRef.comp; + if (emojisPopover && emojisPopover.contains(node)) { + return true; + } + return this.el.contains(node); + } + /** * Get the current partner image URL. * @@ -172,23 +193,13 @@ class Composer extends Component { } /** - * Discards the composer when clicking away from the Inbox reply in discuss. - * TODO maybe move this in discuss. task-2092950 + * Discards the composer when clicking away. * * @private * @param {MouseEvent} ev */ _onClickCaptureGlobal(ev) { - // in discuss app: prevents discarding the composer when clicking away - if (!this.props.isDiscardOnClickAway) { - return; - } - // prevents discarding when clicking on self (on discuss and not on discuss) - if (this.el.contains(ev.target)) { - return; - } - // emoji popover is outside but should be considered inside - if (this._emojisPopoverRef.comp.isInsideEventTarget(ev.target)) { + if (this.contains(ev.target)) { return; } this.composer.discard(); @@ -280,6 +291,37 @@ class Composer extends Component { this.composer.focus(); } + /** + * @private + * @param {KeyboardEvent} ev + */ + _onKeydown(ev) { + if (ev.key === 'Escape') { + if (isEventHandled(ev, 'ComposerTextInput.closeMentionSuggestions')) { + return; + } + if (isEventHandled(ev, 'Composer.closeEmojisPopover')) { + return; + } + ev.preventDefault(); + this.composer.discard(); + } + } + + /** + * @private + * @param {KeyboardEvent} ev + */ + _onKeydownEmojiButton(ev) { + if (ev.key === 'Escape') { + if (this._emojisPopoverRef.comp) { + this._emojisPopoverRef.comp.close(); + this.composer.focus(); + markEventHandled(ev, 'Composer.closeEmojisPopover'); + } + } + } + /** * @private * @param {CustomEvent} ev @@ -315,7 +357,6 @@ Object.assign(Composer, { hasThreadName: false, hasThreadTyping: false, isCompact: true, - isDiscardOnClickAway: false, isExpandable: false, }, props: { @@ -357,7 +398,6 @@ Object.assign(Composer, { optional: true, }, isCompact: Boolean, - isDiscardOnClickAway: Boolean, isExpandable: Boolean, }, template: 'mail.Composer', diff --git a/addons/mail/static/src/components/composer/composer.xml b/addons/mail/static/src/components/composer/composer.xml index bd14aed503a..4a1c20ca9f2 100644 --- a/addons/mail/static/src/components/composer/composer.xml +++ b/addons/mail/static/src/components/composer/composer.xml @@ -10,6 +10,7 @@ 'o-has-header': hasHeader, 'o-is-compact': props.isCompact, }" + t-on-keydown="_onKeydown" > @@ -77,18 +78,19 @@
- + - +