Skip to content

Fix rich-text editor Mod-Enter commit dropping selected content#377

Merged
cscheid merged 2 commits into
mainfrom
feature/bd-hafs0qho-richtext-bold-content-loss
Jul 6, 2026
Merged

Fix rich-text editor Mod-Enter commit dropping selected content#377
cscheid merged 2 commits into
mainfrom
feature/bd-hafs0qho-richtext-bold-content-loss

Conversation

@cscheid

@cscheid cscheid commented Jul 6, 2026

Copy link
Copy Markdown
Member

What & why

In the format: q2-preview rich-text (tiptap) block editor, committing with Mod-Enter while text was selected deleted the selected text. Most visible path: click a block → select-all → bold → commit → the block was written to disk empty (a tight list item - banana became -; a paragraph was deleted entirely).

Found during end-to-end verification of #376 (rich-text editing for Plain blocks). It is not specific to Plain/list items and not introduced by that change — it reproduces identically on a Para, in the shared, type-agnostic RichTextEditor path.

Root cause

tiptap's HardBreak extension binds Mod-EntersetHardBreak() — the same key RichTextEditor used to commit, but via a DOM keydown listener that ran after ProseMirror's keymap plugins. So Mod-Enter:

  1. fired setHardBreak(), replacing the current selection with a hard break — over a full selection (after select-all + bold) the text is deleted, leaving paragraph[hardBreak];
  2. the DOM handler's preventDefault fired too late and committed the emptied doc.

With a collapsed caret it only appended a harmless trailing hard break — which is why plain text edits looked fine but always carried a stray break. The serializer and the Rust commit path were both verified correct in isolation; the fault was entirely in the editor's key handling.

Fix — (A)+(B)

  • (B) editorConfig.ts (new): a shared buildRichTextExtensions() disables StarterKit's built-in HardBreak and re-adds one bound to Shift-Enter only, so Mod-Enter never inserts a break. Adds @tiptap/extension-hard-break as a direct dep.
  • (A) RichTextEditor.tsx: Escape / Mod-Enter / Enter move into a high-priority tiptap keymap (q2CommitKeymap) that runs inside ProseMirror's keymap and wins deterministically; the racy DOM keydown listener is removed (the focusout-commit is kept). Preserved: Escape cancels, plain Enter is swallowed (no split), Shift-Enter is a hard break, dirty/stale guards.

Rationale for doing both (per maintainer): a hard break on Mod-Enter is never wanted here — the rich editor isn't meant for extensive multi-block editing (the preview's plain-text editor and hub-client's source editor cover that).

Tests

  • richtext/editorConfig.test.ts (fast jsdom): a real Mod-Enter over a full selection leaves the text intact; no trailing hardBreak at a collapsed caret; Shift-Enter still inserts one. Verified red with the fix reverted.
  • hub-client/e2e/q2-preview-richtext-bold-content-loss.spec.ts (Playwright, real keyboard): bolding a tight-list item and a paragraph now round-trip to **text** on disk with no content loss. Was red before the fix.

Verification

  • cargo xtask verify — all steps pass.
  • preview-renderer: tsc clean; 487 unit + 517 integration green.
  • hub-client: build green; 229 unit tests green; the e2e repro green.

Strand: bd-hafs0qho (discovered-from bd-7pxub583).

🤖 Generated with Claude Code

cscheid and others added 2 commits July 6, 2026 17:04
… rich editor

In the rich-text (tiptap) block editor, committing with `Mod-Enter` while text
was selected deleted the selected text. Most visible path: click a block →
select-all → bold → commit → the block was written to disk EMPTY (a tight list
item `- banana` became `-`; a paragraph was deleted entirely).

Root cause: tiptap's HardBreak extension binds `Mod-Enter` → setHardBreak() —
the same key RichTextEditor used to commit, but via a DOM keydown listener that
ran AFTER ProseMirror's keymap plugins. So `Mod-Enter` (1) fired setHardBreak,
replacing the current selection with a hard break (over a full selection: the
text is deleted, leaving `paragraph[hardBreak]`), then (2) the DOM handler's
preventDefault fired too late and committed the emptied doc. With a collapsed
caret it only appended a harmless trailing hard break — which is why plain text
edits looked fine but always carried a stray break.

Not specific to `Plain`/list items and not introduced by the Plain-gate change
(bd-7pxub583): it reproduces identically on a `Para`, in the shared,
type-agnostic RichTextEditor path.

Fix (A)+(B):
- (B) editorConfig.ts: a shared `buildRichTextExtensions()` disables StarterKit's
  built-in HardBreak and re-adds one bound to `Shift-Enter` ONLY, so `Mod-Enter`
  never inserts a break. Adds `@tiptap/extension-hard-break` as a direct dep.
- (A) RichTextEditor.tsx: Escape / `Mod-Enter` / `Enter` move into a
  high-priority tiptap keymap (`q2CommitKeymap`) that runs inside ProseMirror's
  keymap and wins deterministically; the racy DOM keydown listener is removed
  (the focusout-commit is kept). Preserved: Escape cancels, plain Enter is
  swallowed (no split), `Shift-Enter` is a hard break, dirty/stale guards.

Tests:
- richtext/editorConfig.test.ts (fast jsdom): real `Mod-Enter` over a full
  selection leaves the text intact; no trailing hardBreak at a collapsed caret;
  `Shift-Enter` still inserts one. Verified red with the fix reverted.
- hub-client/e2e/q2-preview-richtext-bold-content-loss.spec.ts (Playwright, real
  keyboard): bolding a tight-list item and a paragraph now round-trip to
  `**text**` on disk with no content loss. Was red before the fix.

preview-renderer: tsc clean, 487 unit + 517 integration green. hub-client build
+ 229 unit tests green.

Strand: bd-hafs0qho

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@posit-snyk-bot

posit-snyk-bot commented Jul 6, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@cscheid cscheid merged commit 4963674 into main Jul 6, 2026
8 checks passed
@cscheid cscheid deleted the feature/bd-hafs0qho-richtext-bold-content-loss branch July 6, 2026 22:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants