NEW327674
Stale caret stays painted after Selection.collapse() when the contenteditable then moves up
https://bugs.webkit.org/show_bug.cgi?id=327674
Summary Stale caret stays painted after Selection.collapse() when the contenteditable...
Leo Wandersleb
Reported 2026-10-15 01:50:47 PDT
Steps: open the attached test case, click the box, type a word, press Enter twice quickly. Expected: one caret, on line 3. Actual: a second, steady caret stays on line 2 until the next keystroke. The box is pinned to the bottom, so each new line moves it up. The Enter handler inserts a `<br>` and places the caret with `Selection.collapse()`, the way ProseMirror does. WebKit keeps the caret position it computed before the move. Seen 3/3 in WebKitGTK 2.52.6. It doesn't happen when the box is top-anchored (`?top`, 0/3) or with `removeAllRanges()` + `addRange()` (0/4). Transform, will-change, contain, isolation, column-reverse and absolute bottom positioning still ghost. This hits every ProseMirror/Tiptap composer that grows upward. Related: * [ProseMirror's workaround for selection changes](https://github.com/ProseMirror/prosemirror/issues/1092) * [ours for the layout case](https://github.com/block/buzz/pull/8355) * possibly bug 140874. Test case (`ghost-caret.html`): ```html <!doctype html> <meta charset="utf-8"> <title>Stale caret after a bottom-anchored contenteditable grows</title> <style> body { margin: 0; font: 15px/20px sans-serif; } #outer { height: 100vh; display: flex; flex-direction: column; justify-content: flex-end; padding: 20px; box-sizing: border-box; } #ed { border: 1px solid #ccc; padding: 8px; width: 380px; outline: none; } #ed p { margin: 0; } </style> <p style="position:absolute;top:0;left:20px">Click the box, type a word, then press Enter twice quickly. A second, steady caret stays on the middle line. <a href="?top">Top-anchored control</a></p> <div id="outer"><div id="ed" contenteditable><p><br></p></div></div> <script> if (location.search === "?top") document.getElementById("outer").style.justifyContent = "flex-start"; const ed = document.getElementById("ed"); // Insert a line break and place the caret after it with Selection.collapse(), // as ProseMirror does. Using removeAllRanges() + addRange() instead does not ghost. ed.addEventListener("keydown", (e) => { if (e.key !== "Enter") return; e.preventDefault(); const p = ed.querySelector("p"); let trailing = p.querySelector("br.trailing"); if (!trailing) { trailing = document.createElement("br"); trailing.className = "trailing"; p.append(trailing); } p.insertBefore(document.createElement("br"), trailing); getSelection().collapse(p, p.childNodes.length - 1); }); </script> ```
Attachments
the example (1.41 KB, text/html)
2026-10-15 02:03 PDT, Leo Wandersleb
no flags
Leo Wandersleb
Comment 1 2026-10-15 02:03:25 PDT
Created attachment 481835 [details] the example The issue text has the html code embedded but for your convenience here it is as an html file.
Radar WebKit Bug Importer
Comment 2 2026-10-22 01:51:12 PDT
Note You need to log in before you can comment on or make changes to this bug.