diff --git a/packages/editor/src/extensions/text-direction/__tests__/text-direction.test.ts b/packages/editor/src/extensions/text-direction/__tests__/text-direction.test.ts
new file mode 100644
index 000000000..d0de5a1b0
--- /dev/null
+++ b/packages/editor/src/extensions/text-direction/__tests__/text-direction.test.ts
@@ -0,0 +1,96 @@
+/*
+This file is part of the Notesnook project (https://notesnook.com/)
+
+Copyright (C) 2023 Streetwriters (Private) Limited
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+*/
+
+import { describe, expect, test } from "vitest";
+import { createEditor } from "../../../../test-utils/index.js";
+import { TaskListNode } from "../../task-list/task-list.js";
+import { TaskItemNode } from "../../task-item/task-item.js";
+import { BulletList } from "../../bullet-list/bullet-list.js";
+import { ListItem } from "../../list-item/list-item.js";
+import { Paragraph } from "../../paragraph/paragraph.js";
+import { TextDirection } from "../text-direction.js";
+
+function directions(editor: {
+ state: { doc: { descendants: (fn: (node: any) => void) => void } };
+}) {
+ const found: Record = {};
+ editor.state.doc.descendants((node) => {
+ const dir = node.attrs.textDirection;
+ if (dir !== undefined) (found[node.type.name] ??= []).push(dir);
+ });
+ return found;
+}
+
+/** cursor into the first paragraph of the document */
+function cursorInFirstParagraph(editor: any) {
+ let pos = -1;
+ editor.state.doc.descendants((node: any, at: number) => {
+ if (pos === -1 && node.type.name === "paragraph") pos = at + 1;
+ });
+ editor.commands.setTextSelection(pos);
+}
+
+describe("text direction on lists", () => {
+ const cases = [
+ {
+ name: "task list",
+ extensions: {
+ taskList: TaskListNode,
+ taskListItem: TaskItemNode.configure({ nested: true }),
+ paragraph: Paragraph
+ },
+ content: ``
+ },
+ {
+ name: "bullet list",
+ extensions: {
+ bulletList: BulletList,
+ listItem: ListItem,
+ paragraph: Paragraph
+ },
+ content: ``
+ }
+ ];
+
+ for (const { name, extensions, content } of cases) {
+ test(`switching a ${name} to ltr clears the direction of every item`, () => {
+ const { editor } = createEditor({
+ initialContent: content,
+ extensions: {
+ ...extensions,
+ textDirection: TextDirection.configure({
+ types: ["paragraph", "taskList", "bulletList"]
+ })
+ }
+ });
+
+ // every paragraph starts rtl, matching the list
+ const before = directions(editor);
+ expect(Object.values(before).flat()).toContain("rtl");
+
+ cursorInFirstParagraph(editor);
+ editor.commands.setTextDirection(undefined);
+
+ // ...and nothing is left rtl — not the list, not any item, cursor or
+ // not, so the checkboxes/markers and the text no longer disagree
+ const after = directions(editor);
+ expect(Object.values(after).flat()).not.toContain("rtl");
+ });
+ }
+});
diff --git a/packages/editor/src/extensions/text-direction/text-direction.ts b/packages/editor/src/extensions/text-direction/text-direction.ts
index bbc67a1eb..2fda1f74d 100644
--- a/packages/editor/src/extensions/text-direction/text-direction.ts
+++ b/packages/editor/src/extensions/text-direction/text-direction.ts
@@ -114,10 +114,40 @@ export const TextDirection = Extension.create({
return {
setTextDirection:
(direction) =>
- ({ commands }) => {
- return this.options.types.every((type) =>
- commands.updateAttributes(type, { textDirection: direction })
- );
+ ({ state, tr, dispatch }) => {
+ const value = direction || "";
+ const { $from, from, to } = state.selection;
+
+ // Expand to the outermost block that carries a direction, so a
+ // whole task list turns together — every item's paragraph and
+ // all — instead of only the row the cursor is in. Otherwise the
+ // list's own direction flips while its items keep theirs, and the
+ // checkboxes and text end up on opposite sides.
+ let start = from;
+ let end = to;
+ for (let depth = $from.depth; depth > 0; depth--) {
+ if (this.options.types.includes($from.node(depth).type.name)) {
+ start = Math.min(start, $from.before(depth));
+ end = Math.max(end, $from.after(depth));
+ }
+ }
+
+ let changed = false;
+ state.doc.nodesBetween(start, end, (node, pos) => {
+ if (
+ !this.options.types.includes(node.type.name) ||
+ node.attrs.textDirection === value
+ )
+ return;
+ tr.setNodeMarkup(pos, undefined, {
+ ...node.attrs,
+ textDirection: value
+ });
+ changed = true;
+ });
+
+ if (changed) dispatch?.(tr);
+ return changed;
}
};
}