Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Selection, TextSelection } from "prosemirror-state";
import { describe, expect, it } from "vite-plus/test";

import { BlockNoteSchema } from "../../../blocks/BlockNoteSchema.js";
Expand Down Expand Up @@ -110,6 +111,81 @@ function getTextContent(editor: BlockNoteEditor<any, any, any>) {
return text;
}

describe("KeyboardShortcutsExtension Mod-a (select all)", () => {
// BlockNote disables TipTap's core extensions, so it has no default `Mod-a`
// binding and select-all used to rely on the browser's native behaviour. That
// native select-all collapses to a cursor when the editor's first element is
// non-editable - e.g. the checkbox `<div>` of a check list item as the first
// block - so `Mod-a` is now handled explicitly. These tests exercise the
// keymap path (not native selection) and would collapse before the fix.
function createSelectAllEditor(
blocks: { type: "paragraph" | "checkListItem"; content: string }[],
) {
const editor = BlockNoteEditor.create({
schema,
initialContent: blocks.map((block, index) => ({
id: `block-${index}`,
...block,
})),
});
editor.mount(document.createElement("div"));
return editor;
}

// Dispatches a real `Mod-a` keydown through ProseMirror's `handleKeyDown`, the
// path browsers use to invoke the keymap. TipTap's `keyboardShortcut` command
// doesn't reliably simulate modifier combos in jsdom, and prosemirror-keymap
// resolves `Mod` to `Ctrl` outside of a Mac environment (jsdom reports none).
function pressSelectAll(editor: BlockNoteEditor<any, any, any>) {
const view = editor._tiptapEditor.view;
const event = new KeyboardEvent("keydown", {
key: "a",
code: "KeyA",
ctrlKey: true,
});
view.someProp("handleKeyDown", (handler) => handler(view, event));
}

function expectWholeDocSelected(editor: BlockNoteEditor<any, any, any>) {
const { selection, doc } = editor._tiptapEditor.state;
// Select-all spans all content as a `TextSelection` (from the first
// selectable position to the last), not an `AllSelection`.
expect(selection).toBeInstanceOf(TextSelection);
expect(selection.from).toBe(Selection.atStart(doc).from);
expect(selection.to).toBe(Selection.atEnd(doc).to);
}

it("selects the whole document", () => {
const editor = createSelectAllEditor([
{ type: "paragraph", content: "First" },
{ type: "paragraph", content: "Second" },
]);
editor.setTextCursorPosition("block-0", "end");

pressSelectAll(editor);

expectWholeDocSelected(editor);

editor._tiptapEditor.destroy();
});

it("selects the whole document when the first block is a check list item", () => {
const editor = createSelectAllEditor([
{ type: "checkListItem", content: "First" },
{ type: "paragraph", content: "Second" },
]);
// Place the cursor in a later block to make sure select-all still spans the
// whole document, not just the current block.
editor.setTextCursorPosition("block-1", "end");

pressSelectAll(editor);

expectWholeDocSelected(editor);

editor._tiptapEditor.destroy();
});
});

describe("KeyboardShortcutsExtension hardBreakShortcut", () => {
it("inserts a hard break on Shift-Enter by default", () => {
const editor = createEditor("paragraph");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Extension } from "@tiptap/core";
import { Fragment, Node } from "prosemirror-model";
import { TextSelection } from "prosemirror-state";
import { Selection, TextSelection } from "prosemirror-state";

import {
getBottomNestedBlockInfo,
Expand Down Expand Up @@ -997,6 +997,20 @@ export const KeyboardShortcutsExtension = Extension.create<{
"Mod-z": () => this.options.editor.undo(),
"Mod-y": () => this.options.editor.redo(),
"Shift-Mod-z": () => this.options.editor.redo(),
"Mod-a": () => {
const view = this.editor.view;
const { doc, tr } = view.state;
// Use a `TextSelection` from the document start to end as an `AllSelection` creates from/
// to positions outside a block, causing errors when calling e.g. `getBlock`.
const selection = TextSelection.between(
Selection.atStart(doc).$from,
Selection.atEnd(doc).$to,
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

view.dispatch(tr.setSelection(selection).scrollIntoView());

return true;
},
};
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -264,12 +264,15 @@ describe("Math block source popup keyboard handling", () => {
expect(isPopupOpen("math")).toBe(false);

// Single-character keys are only blocked when no Ctrl/Cmd is held, so
// shortcuts pass through - keeping copy/select-all/find working.
// shortcuts pass through - keeping copy/find working.
// (Cut/paste also pass through; that's a known limitation.)
expect(pressKey("c", { ctrlKey: true })).toBe(false);
expect(pressKey("a", { ctrlKey: true })).toBe(false);
expect(pressKey("f", { ctrlKey: true })).toBe(false);
expect(pressKey("v", { metaKey: true })).toBe(false);
// Ctrl/Cmd-a is the exception: select-all is handled explicitly by
// the global keymap (see KeyboardShortcutsExtension), not deferred to the
// browser, so it reports as handled rather than passing through.
expect(pressKey("a", { ctrlKey: true })).toBe(true);
});

it("defers deletion keys to the default while the popup is open", async () => {
Expand Down
Loading