Skip to content

Commit 82e4af2

Browse files
fix: disable calling splitRunToParagraph when unavailable (#2557)
* fix: disable calling splitRunToParagraph when unavailable * fix: add tr.setMeta check helper * fix: remove extra setMeta check * test: add behavior test for Enter in comment editor (SD-2092) Verifies that pressing Enter inside the comment input creates a paragraph break instead of throwing an error. --------- Co-authored-by: Caio Pizzol <caio@harbourshare.com>
1 parent 8839899 commit 82e4af2

3 files changed

Lines changed: 91 additions & 4 deletions

File tree

packages/super-editor/src/core/extensions/keymap.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,22 @@ import { Extension } from '../Extension.js';
33
import { isIOS } from '../utilities/isIOS.js';
44
import { isMacOS } from '../utilities/isMacOS.js';
55

6+
const dispatchHistoryBoundary = (view) => {
7+
const tr = view?.state?.tr;
8+
if (!tr) return;
9+
view.dispatch?.(closeHistory(tr));
10+
};
11+
612
export const handleEnter = (editor) => {
713
const { view } = editor;
814
// Close the current undo group so this structural action becomes its own undo step.
915
// Note: this fires before the command chain, so if no command succeeds (rare — e.g.
1016
// Enter with no valid split target) an empty undo boundary is created. Acceptable
1117
// trade-off vs. the complexity of post-hoc closeHistory after commands.first.
12-
view?.dispatch?.(closeHistory(view?.state?.tr));
18+
dispatchHistoryBoundary(view);
1319

1420
return editor.commands.first(({ commands }) => [
15-
() => commands.splitRunToParagraph(),
21+
() => commands.splitRunToParagraph?.() ?? false,
1622
() => commands.newlineInCode(),
1723
() => commands.createParagraphNear(),
1824
() => commands.liftEmptyBlock(),
@@ -23,7 +29,7 @@ export const handleEnter = (editor) => {
2329
export const handleBackspace = (editor) => {
2430
const { view } = editor;
2531
// Close undo group — see comment in handleEnter.
26-
view?.dispatch?.(closeHistory(view?.state?.tr));
32+
dispatchHistoryBoundary(view);
2733

2834
return editor.commands.first(({ commands, tr }) => [
2935
() => commands.undoInputRule(),
@@ -45,7 +51,7 @@ export const handleBackspace = (editor) => {
4551
export const handleDelete = (editor) => {
4652
const { view } = editor;
4753
// Close undo group — see comment in handleEnter.
48-
view?.dispatch?.(closeHistory(view?.state?.tr));
54+
dispatchHistoryBoundary(view);
4955

5056
return editor.commands.first(({ commands }) => [
5157
() => commands.deleteSkipEmptyRun(),

packages/super-editor/src/core/extensions/keymap.test.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, it, expect, vi } from 'vitest';
2+
import { handleEnter } from './keymap.js';
23

34
const setupKeymap = async ({ isMacOS, isIOS }) => {
45
vi.resetModules();
@@ -25,6 +26,37 @@ const setupKeymap = async ({ isMacOS, isIOS }) => {
2526
};
2627

2728
describe('Keymap extension', () => {
29+
it('falls back when splitRunToParagraph is unavailable', () => {
30+
const splitBlock = vi.fn(() => true);
31+
const first = vi.fn((resolver) => {
32+
const chain = resolver({
33+
commands: {
34+
newlineInCode: vi.fn(() => false),
35+
createParagraphNear: vi.fn(() => false),
36+
liftEmptyBlock: vi.fn(() => false),
37+
splitBlock,
38+
},
39+
});
40+
41+
for (const command of chain) {
42+
if (command()) return true;
43+
}
44+
45+
return false;
46+
});
47+
48+
const editor = {
49+
view: {
50+
state: { tr: { setMeta: vi.fn(() => ({})) } },
51+
dispatch: vi.fn(),
52+
},
53+
commands: { first },
54+
};
55+
56+
expect(handleEnter(editor)).toBe(true);
57+
expect(splitBlock).toHaveBeenCalledTimes(1);
58+
});
59+
2860
it('maps Ctrl-a to selectAll on macOS', async () => {
2961
const { bindings, editor } = await setupKeymap({ isMacOS: true, isIOS: false });
3062

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { test, expect } from '../../fixtures/superdoc.js';
2+
import { assertDocumentApiReady } from '../../helpers/document-api.js';
3+
import { addCommentViaUI, activateCommentDialog } from '../../helpers/comments.js';
4+
5+
test.use({ config: { toolbar: 'full', comments: 'on' } });
6+
7+
test('comment with multi-paragraph text renders correctly (SD-2092)', async ({ superdoc }) => {
8+
await assertDocumentApiReady(superdoc.page);
9+
10+
await superdoc.type('hello world');
11+
await superdoc.waitForStable();
12+
13+
// Add a comment, then edit it with multi-paragraph content
14+
await addCommentViaUI(superdoc, { textToSelect: 'world', commentText: 'placeholder' });
15+
await superdoc.waitForStable();
16+
17+
// Activate the comment and enter edit mode
18+
const dialog = await activateCommentDialog(superdoc, 'world');
19+
await dialog.locator('.overflow-icon').click();
20+
await superdoc.waitForStable();
21+
22+
const editOption = superdoc.page.locator('.comments-dropdown__option-label', { hasText: 'Edit' });
23+
await expect(editOption.first()).toBeVisible({ timeout: 5_000 });
24+
await editOption.first().click();
25+
await superdoc.waitForStable();
26+
27+
// Set multi-paragraph content in the edit input.
28+
// This simulates what ProseMirror's splitBlock produces when Enter is pressed.
29+
const editInput = dialog.locator('.reply-expanded .superdoc-field .ProseMirror').first();
30+
await expect(editInput).toBeVisible({ timeout: 5_000 });
31+
await editInput.evaluate((el) => {
32+
el.innerHTML = '<p>first line</p><p>second line</p>';
33+
el.dispatchEvent(new Event('input', { bubbles: true }));
34+
});
35+
await superdoc.waitForStable();
36+
37+
// Save the edited comment
38+
await dialog.locator('.reply-expanded .sd-button.primary', { hasText: 'Update' }).click();
39+
await superdoc.waitForStable();
40+
41+
// Verify the comment renders with two paragraphs
42+
const updatedDialog = superdoc.page.locator('.comment-placeholder .comments-dialog');
43+
const commentBody = updatedDialog.locator('.comment-body .comment').first();
44+
await expect(commentBody.locator('p')).toHaveCount(2, { timeout: 10_000 });
45+
await expect(commentBody.locator('p').first()).toContainText('first line');
46+
await expect(commentBody.locator('p').last()).toContainText('second line');
47+
48+
await superdoc.snapshot('comment with paragraph break');
49+
});

0 commit comments

Comments
 (0)