diff --git a/packages/excalidraw/element/textWysiwyg.tsx b/packages/excalidraw/element/textWysiwyg.tsx index 5dd438d66f..632759330d 100644 --- a/packages/excalidraw/element/textWysiwyg.tsx +++ b/packages/excalidraw/element/textWysiwyg.tsx @@ -493,6 +493,11 @@ export const textWysiwyg = ({ // so that we don't need to create separate a callback for event handlers let submittedViaKeyboard = false; const handleSubmit = () => { + // prevent double submit + if (isDestroyed) { + return; + } + isDestroyed = true; // cleanup must be run before onSubmit otherwise when app blurs the wysiwyg // it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the // wysiwyg on update @@ -546,10 +551,6 @@ export const textWysiwyg = ({ }; const cleanup = () => { - if (isDestroyed) { - return; - } - isDestroyed = true; // remove events to ensure they don't late-fire editable.onblur = null; editable.oninput = null; @@ -641,6 +642,22 @@ export const textWysiwyg = ({ // handle edge-case where pointerup doesn't fire e.g. due to user // alt-tabbing away window.addEventListener("blur", handleSubmit); + } else if ( + event.target instanceof HTMLElement && + !event.target.contains(editable) && + // Vitest simply ignores stopPropagation, capture-mode, or rAF + // so without introducing crazier hacks, nothing we can do + !isTestEnv() + ) { + // On mobile, blur event doesn't seem to always fire correctly, + // so we want to also submit on pointerdown outside the wysiwyg. + // Done in the next frame to prevent pointerdown from creating a new text + // immediately (if tools locked) so that users on mobile have chance + // to submit first (to hide virtual keyboard). + // Note: revisit if we want to differ this behavior on Desktop + requestAnimationFrame(() => { + handleSubmit(); + }); } }; @@ -678,7 +695,13 @@ export const textWysiwyg = ({ window.addEventListener("resize", updateWysiwygStyle); } - window.addEventListener("pointerdown", onPointerDown); + editable.onpointerdown = (event) => event.stopPropagation(); + + // rAF (+ capture to by doubly sure) so we don't catch te pointerdown that + // triggered the wysiwyg + requestAnimationFrame(() => { + window.addEventListener("pointerdown", onPointerDown, { capture: true }); + }); window.addEventListener("wheel", stopEvent, { passive: false, capture: true,