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