fix: consider node selections on lists in isListActive

When a list node is the target of a NodeSelection, findParentNode resolves
to its parent list rather than the selected node. Check the selected node
directly so the toolbar reflects the selected list.

https://claude.ai/code/session_01BGH191WyuL9SgQyjfJ3YsD
This commit is contained in:
Claude
2026-06-13 16:18:45 +00:00
parent a44a725d19
commit d4185e6455
2 changed files with 71 additions and 1 deletions
@@ -1,5 +1,7 @@
import type { Node } from "prosemirror-model";
import { NodeSelection } from "prosemirror-state";
import {
createEditorState,
createEditorStateWithSelection,
doc,
p,
@@ -53,6 +55,38 @@ function stateAt(testDoc: Node, selectionText: string) {
);
}
/**
* Returns the position directly before the first node of the given type.
*
* @throws if no matching node exists in the document.
*/
function posOfNode(node: Node, typeName: string) {
let found = -1;
node.descendants((child, pos) => {
if (found === -1 && child.type.name === typeName) {
found = pos;
}
return found === -1;
});
if (found === -1) {
throw new Error(`Node "${typeName}" not found in document`);
}
return found;
}
/**
* Builds an editor state with a NodeSelection on the first node of the given
* type.
*/
function nodeSelectionStateAt(testDoc: Node, typeName: string) {
const state = createEditorState(testDoc);
const selection = NodeSelection.create(
state.doc,
posOfNode(testDoc, typeName)
);
return state.apply(state.tr.setSelection(selection));
}
describe("isListActive", () => {
it("matches the closest list type", () => {
const testDoc = doc([
@@ -95,6 +129,30 @@ describe("isListActive", () => {
expect(isListActive(ordered_list)(state)).toBe(false);
});
it("matches a list selected directly via a NodeSelection", () => {
const testDoc = doc([
bullet_list.create(null, [li([p("one")]), li([p("two")])]),
]);
const state = nodeSelectionStateAt(testDoc, "bullet_list");
expect(isListActive(bullet_list)(state)).toBe(true);
expect(isListActive(ordered_list)(state)).toBe(false);
expect(isListActive(checkbox_list)(state)).toBe(false);
});
it("matches the selected nested list, not its parent, via a NodeSelection", () => {
const testDoc = doc([
checkbox_list.create(null, [
cli([p("moo"), ordered_list.create(null, [li([p("dfsdf")])])]),
]),
]);
const state = nodeSelectionStateAt(testDoc, "ordered_list");
expect(isListActive(ordered_list)(state)).toBe(true);
expect(isListActive(checkbox_list)(state)).toBe(false);
expect(isListActive(bullet_list)(state)).toBe(false);
});
it("returns false when the selection is not in a list", () => {
const testDoc = doc([p("hello")]);
const state = stateAt(testDoc, "hello");
+13 -1
View File
@@ -1,5 +1,6 @@
import type { NodeType } from "prosemirror-model";
import type { EditorState } from "prosemirror-state";
import { NodeSelection } from "prosemirror-state";
import { findParentNode } from "./findParentNode";
import { isList } from "./isList";
@@ -15,8 +16,19 @@ import { isList } from "./isList";
export const isListActive =
(type: NodeType) =>
(state: EditorState): boolean => {
const { selection } = state;
// When the list node itself is selected via a NodeSelection, consider that
// node directly — findParentNode would otherwise report its parent list.
if (
selection instanceof NodeSelection &&
isList(selection.node, state.schema)
) {
return selection.node.type === type;
}
const closestList = findParentNode((node) => isList(node, state.schema))(
state.selection
selection
);
return closestList?.node.type === type;
};