diff --git a/ts/quill/emoji/completion.tsx b/ts/quill/emoji/completion.tsx index ddc3a2fe7..919e4ff93 100644 --- a/ts/quill/emoji/completion.tsx +++ b/ts/quill/emoji/completion.tsx @@ -42,7 +42,7 @@ export class EmojiCompletion { quill: Quill; - outsideClickDestructor: () => void; + outsideClickDestructor?: () => void; constructor(quill: Quill, options: EmojiPickerOptions) { this.results = []; @@ -51,18 +51,6 @@ export class EmojiCompletion { this.root = document.body.appendChild(document.createElement('div')); this.quill = quill; - // Just to make sure that we don't propagate outside clicks until this - // is closed. - this.outsideClickDestructor = handleOutsideClick( - () => { - return true; - }, - { - name: 'quill.emoji.completion', - containerElements: [this.root], - } - ); - const clearResults = () => { if (this.results.length) { this.reset(); @@ -108,7 +96,8 @@ export class EmojiCompletion { } destroy(): void { - this.outsideClickDestructor(); + this.outsideClickDestructor?.(); + this.outsideClickDestructor = undefined; this.root.remove(); } @@ -277,14 +266,16 @@ export class EmojiCompletion { } onUnmount(): void { - document.body.removeChild(this.root); + this.outsideClickDestructor?.(); + this.outsideClickDestructor = undefined; + this.options.setEmojiPickerElement(null); } render(): void { const { results: emojiResults, index: emojiResultsIndex } = this; if (emojiResults.length === 0) { - this.options.setEmojiPickerElement(null); + this.onUnmount(); return; } @@ -374,7 +365,21 @@ export class EmojiCompletion { )} , - document.body + this.root + ); + + // Just to make sure that we don't propagate outside clicks until this + // is closed. + this.outsideClickDestructor?.(); + this.outsideClickDestructor = handleOutsideClick( + () => { + this.onUnmount(); + return true; + }, + { + name: 'quill.emoji.completion', + containerElements: [this.root], + } ); this.options.setEmojiPickerElement(element); diff --git a/ts/quill/mentions/completion.tsx b/ts/quill/mentions/completion.tsx index 00963d26f..b91ade091 100644 --- a/ts/quill/mentions/completion.tsx +++ b/ts/quill/mentions/completion.tsx @@ -43,7 +43,7 @@ export class MentionCompletion { suggestionListRef: RefObject; - outsideClickDestructor: () => void; + outsideClickDestructor?: () => void; constructor(quill: Quill, options: MentionCompletionOptions) { this.results = []; @@ -53,18 +53,6 @@ export class MentionCompletion { this.quill = quill; this.suggestionListRef = React.createRef(); - // Just to make sure that we don't propagate outside clicks until this - // is closed. - this.outsideClickDestructor = handleOutsideClick( - () => { - return true; - }, - { - name: 'quill.emoji.completion', - containerElements: [this.root], - } - ); - const clearResults = () => { if (this.results.length) { this.clearResults(); @@ -92,7 +80,9 @@ export class MentionCompletion { } destroy(): void { - this.outsideClickDestructor(); + this.outsideClickDestructor?.(); + this.outsideClickDestructor = undefined; + this.root.remove(); } @@ -220,7 +210,9 @@ export class MentionCompletion { } onUnmount(): void { - document.body.removeChild(this.root); + this.outsideClickDestructor?.(); + this.outsideClickDestructor = undefined; + this.options.setMentionPickerElement(null); } render(): void { @@ -228,7 +220,7 @@ export class MentionCompletion { const { getPreferredBadge, theme } = this.options; if (memberResults.length === 0) { - this.options.setMentionPickerElement(null); + this.onUnmount(); return; } @@ -293,6 +285,20 @@ export class MentionCompletion { this.root ); + // Just to make sure that we don't propagate outside clicks until this + // is closed. + this.outsideClickDestructor?.(); + this.outsideClickDestructor = handleOutsideClick( + () => { + this.onUnmount(); + return true; + }, + { + name: 'quill.mentions.completion', + containerElements: [this.root], + } + ); + this.options.setMentionPickerElement(element); } } diff --git a/ts/util/handleOutsideClick.ts b/ts/util/handleOutsideClick.ts index 35fdaea8c..2f4bf0fbb 100644 --- a/ts/util/handleOutsideClick.ts +++ b/ts/util/handleOutsideClick.ts @@ -8,11 +8,15 @@ export type ContainerElementType = Node | RefObject | null | undefined; // TODO(indutny): DESKTOP-4177 // A stack of handlers. Handlers are executed from the top to the bottom -const fakeClickHandlers = new Array<(event: MouseEvent) => boolean>(); +const fakeClickHandlers = new Array<{ + name: string; + handleEvent: (event: MouseEvent) => boolean; +}>(); function runFakeClickHandlers(event: MouseEvent): void { - for (const handler of fakeClickHandlers.slice().reverse()) { - if (handler(event)) { + for (const entry of fakeClickHandlers.slice().reverse()) { + const { handleEvent } = entry; + if (handleEvent(event)) { break; } } @@ -25,7 +29,7 @@ export type HandleOutsideClickOptionsType = Readonly<{ export const handleOutsideClick = ( handler: ClickHandlerType, - { containerElements }: HandleOutsideClickOptionsType + { name, containerElements }: HandleOutsideClickOptionsType ): (() => void) => { const handleEvent = (event: MouseEvent) => { const target = event.target as Node; @@ -49,19 +53,20 @@ export const handleOutsideClick = ( return handler(target); }; - fakeClickHandlers.push(handleEvent); + const fakeHandler = { name, handleEvent }; + fakeClickHandlers.push(fakeHandler); if (fakeClickHandlers.length === 1) { const useCapture = true; document.addEventListener('click', runFakeClickHandlers, useCapture); } return () => { - const index = fakeClickHandlers.indexOf(handleEvent); + const index = fakeClickHandlers.indexOf(fakeHandler); fakeClickHandlers.splice(index, 1); if (fakeClickHandlers.length === 0) { const useCapture = true; - document.removeEventListener('click', handleEvent, useCapture); + document.removeEventListener('click', runFakeClickHandlers, useCapture); } }; };