fix: place cursor at start of item after checklist conversion

After converting a list to a checklist via the checkbox marker input rule,
position the selection at the start of the converted item so the user can
continue typing immediately.

https://claude.ai/code/session_01BGH191WyuL9SgQyjfJ3YsD
This commit is contained in:
Claude
2026-06-13 02:37:56 +00:00
parent a34d7790f9
commit e8aac7c3ec
2 changed files with 31 additions and 10 deletions
+26 -10
View File
@@ -40,7 +40,7 @@ function posAfterText(node: Node, text: string) {
/**
* Simulates typing the trigger character of an input rule, mirroring the way
* prosemirror-inputrules invokes a rule's handler, and returns the resulting
* document (or the unchanged document when the rule does not fire).
* editor state (unchanged when the rule does not fire).
*/
function typeTrigger(
rule: InputRule,
@@ -64,7 +64,7 @@ function typeTrigger(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const match = (rule as any).match.exec(textBefore) as RegExpMatchArray | null;
if (!match) {
return state.doc;
return state;
}
const startPos = from - (match[0].length - triggerChar.length);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -72,7 +72,7 @@ function typeTrigger(
if (tr) {
state = state.apply(tr);
}
return state.doc;
return state;
}
const rule = checkboxListInputRule(
@@ -89,7 +89,7 @@ describe("checkboxListInputRule", () => {
const result = typeTrigger(rule, testDoc, "[ ]", " ");
const list = result.firstChild;
const list = result.doc.firstChild;
expect(list?.type.name).toBe("checkbox_list");
expect(list?.childCount).toBe(2);
expect(list?.child(0).type.name).toBe("checkbox_item");
@@ -97,6 +97,22 @@ describe("checkboxListInputRule", () => {
expect(list?.child(1).textContent).toBe("two");
});
it("places the cursor at the start of the converted item", () => {
const testDoc = doc([
bullet_list.create(null, [li([p("[ ]")]), li([p("two")])]),
]);
const result = typeTrigger(rule, testDoc, "[ ]", " ");
// The selection should sit empty at the start of the first item's content.
const { selection } = result;
expect(selection.empty).toBe(true);
const $from = result.doc.resolve(selection.from);
expect($from.parent.type.name).toBe("paragraph");
expect($from.parentOffset).toBe(0);
expect($from.node(-1).type.name).toBe("checkbox_item");
});
it("converts a plain ordered list to a checklist", () => {
const testDoc = doc([
ordered_list.create(null, [li([p("[ ]")]), li([p("two")])]),
@@ -104,7 +120,7 @@ describe("checkboxListInputRule", () => {
const result = typeTrigger(rule, testDoc, "[ ]", " ");
expect(result.firstChild?.type.name).toBe("checkbox_list");
expect(result.doc.firstChild?.type.name).toBe("checkbox_list");
});
it("preserves nesting when converting a list with a nested list", () => {
@@ -117,7 +133,7 @@ describe("checkboxListInputRule", () => {
const result = typeTrigger(rule, testDoc, "[ ]", " ");
const list = result.firstChild;
const list = result.doc.firstChild;
expect(list?.type.name).toBe("checkbox_list");
const nested = list?.child(1).child(1);
expect(nested?.type.name).toBe("checkbox_list");
@@ -136,8 +152,8 @@ describe("checkboxListInputRule", () => {
const result = typeTrigger(rule, testDoc, "[ ]", " ");
// The rule should not fire; the marker text is left untouched.
expect(result.firstChild?.type.name).toBe("checkbox_list");
expect(result.firstChild?.child(0).textContent).toBe("[ ]");
expect(result.doc.firstChild?.type.name).toBe("checkbox_list");
expect(result.doc.firstChild?.child(0).textContent).toBe("[ ]");
});
it("does nothing when not inside a list", () => {
@@ -145,7 +161,7 @@ describe("checkboxListInputRule", () => {
const result = typeTrigger(rule, testDoc, "[ ]", " ");
expect(result.firstChild?.type.name).toBe("paragraph");
expect(result.firstChild?.textContent).toBe("[ ]");
expect(result.doc.firstChild?.type.name).toBe("paragraph");
expect(result.doc.firstChild?.textContent).toBe("[ ]");
});
});
+5
View File
@@ -4,6 +4,7 @@ import type {
Node as ProsemirrorNode,
Attrs,
} from "prosemirror-model";
import { TextSelection } from "prosemirror-state";
import toggleList from "../commands/toggleList";
import { findParentNodeClosestToPos } from "../queries/findParentNode";
import { isInHeading } from "../queries/isInHeading";
@@ -68,6 +69,10 @@ export function checkboxListInputRule(
toggleList(listType, itemType)(after, (toggleTr) => {
toggleTr.steps.forEach((step) => tr.step(step));
});
// Place the cursor at the start of the converted item, where the marker
// was, so the user can continue typing.
tr.setSelection(TextSelection.near(tr.doc.resolve(start)));
return tr;
});
}