Files
learnharness/tests/enter-submits.test.ts
amosblomqvist 26bc305d66 ui: plan canvas ink extent + the keyboard-first round
Plan canvas (owner report: arrows clipped at the viewBox edge in dist —
measured 14px/20px cuts in the owner's real plans): blockLayout now
unions node boxes with every edge path's control-point hull (a bezier
never leaves it; the layout calls edgePathD with the renderer's exact
inputs). Dagre's node extent stays the floor, so ordinary plans render
byte-identical; the viewBox origin shifts only when ink bulges past
zero. Verified on both real chats; pins use the owner's real
linear-algebra tree (golden-real-data).

Keyboard-first (owner's law: Enter always submits, arrows always cycle;
text fields the only exception):
- Enter guard split (enter-submits): a focused BUTTON keeps native
  Enter only when keyboard-walked (:focus-visible) or inside an open
  overlay (menus/dialogs); pointer-clicked transcript buttons submit,
  preventDefault suppressing the re-activation underneath.
- Focus hygiene (focus-hygiene): pointer clicks on transient controls
  (agent/user thinking toggles, copy, mermaid expand, plan chip) blur
  to body — the state clicking empty space produces.
- Session search owns the walk: Up/Down wrap-cycle the filtered rows,
  Enter opens the walked row or the first match (quick-open), then
  blurs and keeps the query. The Alt-arrow session cycle is RETIRED.
- Registry: model-menu walk/pick entries removed (obvious in-field
  navigation, not settings material); question-dock keys stay
  displayOnly per owner ruling.

Gates: 1489/1489, typecheck, lint. Verified live end-to-end.
2026-10-10 09:43:12 +03:00

83 lines
3.1 KiB
TypeScript

/**
* enter-submits + focus-hygiene — the pointer/keyboard Enter split and the
* transient-control blur (owner, keyboard round 2026-10-10).
*
* The regression this pins: clicking a transcript control (a thinking
* toggle) left focus on its <button>; the learner's next Enter re-activated
* the toggle instead of submitting. The law now: Enter from a
* POINTER-focused transcript button submits (the window listener also
* preventDefaults, suppressing the default activation underneath);
* keyboard-focused buttons and buttons inside open overlays (menus walk
* focus with arrows, dialogs are their own layer) keep native Enter.
*/
import assert from "node:assert/strict";
import { parseHTML } from "linkedom";
import { describe, it } from "node:test";
import { buttonKeepsNativeEnter } from "../components/enter-submits.ts";
import { blurAfterPointerClick } from "../components/focus-hygiene.ts";
const { document } = parseHTML("<html><body></body></html>");
function buttonIn(attrs: Record<string, string>): HTMLElement {
const host = document.createElement("div");
for (const [k, v] of Object.entries(attrs)) host.setAttribute(k, v);
const b = document.createElement("button");
b.type = "button";
host.appendChild(b);
document.body.appendChild(host);
return b;
}
describe("buttonKeepsNativeEnter (the pointer/keyboard Enter split)", () => {
it("a transcript button (pointer-focused) does NOT keep native Enter — Enter submits", () => {
const b = buttonIn({});
assert.equal(buttonKeepsNativeEnter(b), false);
});
it("a keyboard-focused button keeps native Enter (:focus-visible)", () => {
const b = buttonIn({});
// linkedom lacks the pseudo — stub the one call the helper makes.
(b as unknown as { matches: (sel: string) => boolean }).matches = (sel: string) =>
sel === ":focus-visible";
assert.equal(buttonKeepsNativeEnter(b), true);
});
it("buttons inside overlays keep native Enter: hand-rolled menu, Radix popper, dialog, menu role", () => {
assert.equal(buttonKeepsNativeEnter(buttonIn({ "data-lh-menu": "" })), true);
assert.equal(
buttonKeepsNativeEnter(buttonIn({ "data-radix-popper-content-wrapper": "" })),
true,
);
assert.equal(buttonKeepsNativeEnter(buttonIn({ role: "dialog" })), true);
assert.equal(buttonKeepsNativeEnter(buttonIn({ role: "menu" })), true);
});
it("a selector engine without :focus-visible degrades to pointer-focus, never throws", () => {
const b = buttonIn({});
assert.doesNotThrow(() => buttonKeepsNativeEnter(b));
assert.equal(buttonKeepsNativeEnter(b), false);
});
});
describe("blurAfterPointerClick (focus hygiene)", () => {
it("a POINTER click (detail > 0) blurs the transient control", () => {
const b = buttonIn({});
let blurred = false;
b.blur = () => {
blurred = true;
};
blurAfterPointerClick({ detail: 1, currentTarget: b });
assert.equal(blurred, true);
});
it("a KEYBOARD-activated click (detail 0) keeps focus", () => {
const b = buttonIn({});
let blurred = false;
b.blur = () => {
blurred = true;
};
blurAfterPointerClick({ detail: 0, currentTarget: b });
assert.equal(blurred, false);
});
});