mirror of
https://github.com/outline/outline.git
synced 2026-08-03 13:27:25 +03:00
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:
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user