From 2ffdd1603824b1e86b0ac41b9d96eede96023bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Thu, 8 Oct 2026 15:35:28 +0200 Subject: [PATCH] :bug: Fix typing after a triple-click in the text editor (#12218) A triple-click can leave the browser selection outside the hidden input surface (it reads as "\n"). That selection is not editable, so typed keys fired keydown but no input, and never reached the WASM editor. Move the selection back into the surface before plain keys reach the browser, and after double and triple clicks. Closes #12187 AI-assisted-by: claude-opus-5-5 --- .../ui/specs/text-editor-v3.spec.js | 77 +++++++++++++++++++ .../ui/workspace/shapes/text/v3_editor.cljs | 36 +++++++-- 2 files changed, 105 insertions(+), 8 deletions(-) diff --git a/frontend/playwright/ui/specs/text-editor-v3.spec.js b/frontend/playwright/ui/specs/text-editor-v3.spec.js index 9f0f0d7323..04efe9f73c 100644 --- a/frontend/playwright/ui/specs/text-editor-v3.spec.js +++ b/frontend/playwright/ui/specs/text-editor-v3.spec.js @@ -298,6 +298,83 @@ test.describe("BUG 10910 - Text is not replaced when there is a selection", () = }); }); +test.describe("BUG 12187 - Typing after a triple-click selection", () => { + // Creates a text and enters the editor again, so the input surface is fresh. + async function editNewText(page, text) { + const workspace = new WasmWorkspacePage(page, { textEditor: true }); + await workspace.setupEmptyFile(); + await workspace.goToWorkspace(); + await workspace.waitForFirstRender(); + + await workspace.createAutoWidthTextShape(200, 150, text); + await workspace.textEditor.stopEditing(); + await workspace.textEditor.startEditing(); + + return workspace; + } + + async function tripleClickText(page) { + await page + .getByTestId("text-editor-container") + .click({ position: { x: 10, y: 8 }, clickCount: 3 }); + } + + // Some browsers (Chrome on Linux) select the whole input surface on a + // triple-click. That selection is not editable, so typing fires no input. + async function selectWholeInputSurface(page) { + await page.evaluate(() => { + const range = document.createRange(); + range.selectNode(document.getElementById("text-editor-wasm-input")); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + }); + } + + test("Typing over a triple-click selection replaces it", async ({ page }) => { + const workspace = await editNewText(page, "Hello brave new world"); + + await tripleClickText(page); + await page.keyboard.type("ok"); + + await workspace.textEditor.stopEditing(); + + await workspace.layers.getByTestId("layer-row").first().click(); + await workspace.waitForSelectedShapeName("ok"); + }); + + test("Typing works when the browser selects the whole input surface", async ({ + page, + }) => { + const workspace = await editNewText(page, "Hello brave new world"); + + await tripleClickText(page); + await selectWholeInputSurface(page); + await page.keyboard.type("ok"); + + await workspace.textEditor.stopEditing(); + + await workspace.layers.getByTestId("layer-row").first().click(); + await workspace.waitForSelectedShapeName("ok"); + }); + + test("Typing works after deleting a whole input surface selection", async ({ + page, + }) => { + const workspace = await editNewText(page, "Hello brave new world"); + + await tripleClickText(page); + await selectWholeInputSurface(page); + await page.keyboard.press("Backspace"); + await page.keyboard.type("ok"); + + await workspace.textEditor.stopEditing(); + + await workspace.layers.getByTestId("layer-row").first().click(); + await workspace.waitForSelectedShapeName("ok"); + }); +}); + test("BUG 10531 - Entering the editor auto-selects the whole text", async ({ page, }) => { diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs index 1c95406ef5..ca486c0fe1 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs @@ -95,6 +95,27 @@ :name name))) (sync-wasm-text-editor-content!)))) +(defn- collapse-input-caret + "Replaces the browser selection with a collapsed caret at the end of `node`." + [^js node] + (when-let [sel (.getSelection js/window)] + (let [range (.createRange js/document)] + (.selectNodeContents range node) + (.collapse range false) + (.removeAllRanges sel) + (.addRange sel range)))) + +(defn- ensure-input-caret + "Collapses the browser selection into the capture surface when it leaves it: + such a selection is not editable, so typed keys would fire no `input`." + [^js node] + (when (some? node) + (let [sel (.getSelection js/window)] + (when-not (and (some? sel) + (.contains node (.-anchorNode sel)) + (.contains node (.-focusNode sel))) + (collapse-input-caret node))))) + (defn- reset-input-node "Empties the contenteditable capture surface and restores a collapsed caret inside it. @@ -110,12 +131,7 @@ (set! (.-textContent node) "") (when (not= (.-activeElement js/document) node) (.focus node)) - (when-let [sel (.getSelection js/window)] - (let [range (.createRange js/document)] - (.selectNodeContents range node) - (.collapse range true) - (.removeAllRanges sel) - (.addRange sel range))))) + (collapse-input-caret node))) (defn- keep-input-alive "Keeps the capture surface able to receive further input WITHOUT clearing it. @@ -411,8 +427,10 @@ (text-editor/text-editor-move-cursor 5 ctrl? shift?) (wasm.api/render-text-editor-overlay!)) - ;; Let contenteditable handle text input via on-input - :else nil))))) + ;; Let contenteditable handle text input via on-input, which + ;; needs an editable browser selection. + :else + (ensure-input-caret (mf/ref-val contenteditable-ref))))))) ;; Native `beforeinput` listener (see the use-effect that registers it). ;; We use the native event, not React's synthetic `onBeforeInput`, because @@ -529,6 +547,7 @@ (triple-click? native-event) (do (wasm.api/text-editor-select-paragraph off-pt) + (ensure-input-caret (mf/ref-val contenteditable-ref)) (wasm.api/render-text-editor-overlay!)) ;; `dblclick` selects the word right after. Shift+click still goes @@ -548,6 +567,7 @@ (let [native-event (dom/event->native-event event) off-pt (dom/get-offset-position native-event)] (wasm.api/text-editor-select-word-boundary off-pt) + (ensure-input-caret (mf/ref-val contenteditable-ref)) (wasm.api/render-text-editor-overlay!)))) on-context-menu