Files
multica/packages/views/editor/extensions/submit-shortcut.test.ts
Naiyuan Qing 34a7ba9865 fix(chat): unify chat and comment send shortcut to Mod+Enter (#2398)
Chat input had `submitOnEnter` enabled while the comment editor used
`Mod+Enter`. Two consequences:

- Inconsistent muscle memory between the two inputs.
- In chat, bare Enter sending stole the only key that continues a
  TipTap bullet/ordered list. Shift+Enter falls through to HardBreak
  (a <br> inside the same list item), so bullet lists were stuck at
  one item.

Drop `submitOnEnter` from the chat input so it follows the editor
default. Mod+Enter (⌘↵ / Ctrl+Enter) sends in both places; bare Enter
now continues lists and inserts paragraphs as users expect.

Surface the shortcut on the SubmitButton via a new optional `tooltip`
prop, and route the comment input through SubmitButton instead of an
ad-hoc Button — same affordance, deduped.

Add unit coverage for the submit-shortcut extension that pins
Mod-Enter, the submitOnEnter=false case, IME, and code-block guards.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-05-11 14:39:37 +08:00

91 lines
2.6 KiB
TypeScript

import { describe, it, expect, vi } from "vitest";
import { getExtensionField } from "@tiptap/core";
import type { Editor } from "@tiptap/core";
import { createSubmitExtension } from "./submit-shortcut";
function getShortcuts(
ext: ReturnType<typeof createSubmitExtension>,
editor: Partial<Editor>,
): Record<string, () => boolean> {
const fn = getExtensionField<
() => Record<string, () => boolean>
>(ext, "addKeyboardShortcuts", {
name: "submitShortcut",
options: {},
storage: {},
editor: editor as Editor,
type: null,
});
return fn?.() ?? {};
}
describe("createSubmitExtension", () => {
const baseEditor = {
view: { composing: false } as unknown as Editor["view"],
isActive: () => false,
} as Partial<Editor>;
it("Mod-Enter always submits", () => {
const onSubmit = vi.fn(() => true);
const shortcuts = getShortcuts(
createSubmitExtension(onSubmit, { submitOnEnter: false }),
baseEditor,
);
expect(shortcuts["Mod-Enter"]).toBeDefined();
shortcuts["Mod-Enter"]!();
expect(onSubmit).toHaveBeenCalledTimes(1);
});
it("bare Enter is not bound when submitOnEnter is false", () => {
const onSubmit = vi.fn(() => true);
const shortcuts = getShortcuts(
createSubmitExtension(onSubmit, { submitOnEnter: false }),
baseEditor,
);
expect(shortcuts.Enter).toBeUndefined();
expect(onSubmit).not.toHaveBeenCalled();
});
it("bare Enter submits when submitOnEnter is true", () => {
const onSubmit = vi.fn(() => true);
const shortcuts = getShortcuts(
createSubmitExtension(onSubmit, { submitOnEnter: true }),
baseEditor,
);
expect(shortcuts.Enter).toBeDefined();
expect(shortcuts.Enter!()).toBe(true);
expect(onSubmit).toHaveBeenCalledTimes(1);
});
it("Enter is suppressed during IME composition", () => {
const onSubmit = vi.fn(() => true);
const shortcuts = getShortcuts(
createSubmitExtension(onSubmit, { submitOnEnter: true }),
{
view: { composing: true } as unknown as Editor["view"],
isActive: () => false,
},
);
expect(shortcuts.Enter!()).toBe(false);
expect(onSubmit).not.toHaveBeenCalled();
});
it("Enter is suppressed inside a code block", () => {
const onSubmit = vi.fn(() => true);
const shortcuts = getShortcuts(
createSubmitExtension(onSubmit, { submitOnEnter: true }),
{
view: { composing: false } as unknown as Editor["view"],
isActive: (name: string) => name === "codeBlock",
},
);
expect(shortcuts.Enter!()).toBe(false);
expect(onSubmit).not.toHaveBeenCalled();
});
});