🐛 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
This commit is contained in:
Belén Albeza 2026-10-08 15:35:28 +02:00 committed by GitHub
parent c2a1d70476
commit 2ffdd16038
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 105 additions and 8 deletions

View File

@ -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,
}) => {

View File

@ -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