fix(ui): +Dev inline-namer kept getting blurred by default mousedown

Root cause traced by sherlock with Playwright (docs/sherlock-+dev-bug.md
on the sherlock branch). The previous routing fix at 94869f3 was
necessary but not sufficient: placeDeviceAt() now reaches promptInline()
correctly, but the synchronous input.focus() is undone ~6ms later by
the browser's compatibility-mousedown default — which blurs the active
element when the mousedown landed on a non-focusable target (SVG rect /
SVG root). The blur listener then ran done(null) and ripped the
<foreignObject> out before m could type a name.

Primary fix: e.preventDefault() at the top of both armed-tool branches
in onCanvasPointerDown. Suppresses the focus-shifting default so the
input keeps focus.

+Frame is also wrapped for symmetry. It wasn't strictly affected (its
namer runs from pointerup, not pointerdown) but preventDefault avoids
a subtle text-selection side effect during rubber-band drag.

Secondary fix in promptInline.done(): clear activeNamer *before*
fo.remove(). Enter-key triggers a synchronous blur listener which
re-enters done() — if remove() ran first, the re-entry hit a
"node no longer a child" pageerror. Reordering makes the re-entry a
no-op (activeNamer is already null).

Verified locally: served /main.js shows e.preventDefault() inside both
tool branches and the reordered done() body. go test -race ./... still
green.
This commit is contained in:
mAi
2026-05-15 23:17:44 +02:00
parent e12b449169
commit 12804619b2

View File

@@ -465,11 +465,21 @@ function onCanvasPointerDown(e) {
// of an existing frame or device — fires the tool. The +Dev tool needs
// this so m can drop a device inside a frame; without it the frame's
// own pointerdown handler would steal the click and start a drag.
//
// e.preventDefault() suppresses the compatibility mousedown's default
// focus-shift. Without it, the freshly-focused inline-namer input gets
// blurred ~6ms later by the browser's "focus nearest focusable ancestor
// or blur active" behaviour (SVG rects are not focusable), and the
// blur handler tears the namer down before m can type. Root cause +
// verified fix from sherlock's Playwright shift; see docs/sherlock-+dev-bug.md
// for the full trace.
if (state.tool === "frame") {
e.preventDefault();
startFrameRubberBand(e, p);
return;
}
if (state.tool === "device") {
e.preventDefault();
placeDeviceAt(p);
return;
}
@@ -582,7 +592,14 @@ function promptInline(placeholder, cx, cy) {
const input = fo.querySelector("input");
input.focus();
const done = (val) => {
if (activeNamer === fo) { fo.remove(); activeNamer = null; }
// Clear the flag *before* removing the node. Enter-key triggers a
// synchronous blur on the input, which re-enters done() — and if
// fo.remove() ran first, the second call hits a
// "node no longer a child" pageerror. Reordering makes the second
// re-entry a no-op (activeNamer is already null).
if (activeNamer !== fo) return;
activeNamer = null;
fo.remove();
resolve(val);
};
input.addEventListener("keydown", (e) => {