Skip to content

Pasting into a blank list item inside a blockquote crashes with Lexical error #66#1108

Open
jorgemanrubia wants to merge 1 commit into
mainfrom
sentry-lexical-error-66
Open

Pasting into a blank list item inside a blockquote crashes with Lexical error #66#1108
jorgemanrubia wants to merge 1 commit into
mainfrom
sentry-lexical-error-66

Conversation

@jorgemanrubia

@jorgemanrubia jorgemanrubia commented Jun 10, 2026

Copy link
Copy Markdown
Member

Fixes BC3-JS-N7CJ — 228 events / 186 users in 3 days. Card: https://app.basecamp.com/2914079/buckets/41746046/card_tables/cards/9982981354

What it fixes

Pasting block content with the cursor on a blank list item inside a blockquote crashed with Lexical error #66 ("Expected node %s to have a parent"), and the pasted content was lost. Every Sentry event goes through the same path: Contents#insertDOMinsertAtCursorRangeSelection.insertNodes.

Reproduction: in a blockquote, Shift+Enter on an empty list item (leaving a list item whose only child is a line break), then paste rich text.

How

Lexical's RangeSelection.insertNodes captures firstBlock (the list item), calls insertParagraphblock.insertNewAfter(...), then inserts the pasted blocks with insertRangeAfter(firstBlock, ...). EarlyEscapeListItemNode#insertNewAfter treated this internal call as an escape gesture: #escapeFromList removed the list item, so insertRangeAfter called insertAfter on a detached node and getParentOrThrow threw #66.

The fix guards #shouldEscape with $hasUpdateTag(PASTE_TAG), mirroring the guard EarlyEscapeCodeNode already has. Pasting is not an escape gesture; during paste the node defers to the default list item behavior, so the pasted content lands in the list and the escape UX on Enter is unchanged.

Includes a Playwright regression test at test/browser/tests/paste/paste_into_quoted_list.test.js that recreates the production scenario with a real collapsed caret (Enter, then Shift+Enter on the quoted list, then paste). Red/green verified: it fails on main's early_escape_list_item_node.js, passes with the fix.

Overlap notes

Copilot AI review requested due to automatic review settings June 10, 2026 16:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a Lexical paste crash when the caret is on a blank (but not empty) list item inside a blockquote by preventing EarlyEscapeListItemNode from treating paste-driven insertNewAfter calls as an “escape” gesture, and adds a regression test to ensure both “no crash” and “content preserved” behavior.

Changes:

  • Guard EarlyEscapeListItemNode#shouldEscape with $hasUpdateTag(PASTE_TAG) so paste flows defer to default list-item behavior.
  • Add a vitest regression test covering the crash path and verifying pasted content remains in the document.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
test/javascript/unit/editor/paste_into_quoted_list.test.js Adds regression coverage for pasting block content into a blank quoted list item (crash + content preservation).
src/nodes/early_escape_list_item_node.js Prevents list-item “escape” behavior during paste updates to avoid detached-node errors.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +22 to +26
const quote = $getRoot().getChildren().find($isQuoteNode)
const list = quote.getChildren().find($isListNode)
const blankListItem = list.getChildren().findLast($isListItemNode)
blankListItem.append($createLineBreakNode()) // As after Shift+Enter on an empty list item
blankListItem.select()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Addressed — the regression test now lives in the browser suite (test/browser/tests/paste/paste_into_quoted_list.test.js) and drives a real collapsed caret through actual editor interaction (Enter then Shift+Enter on the quoted list), so the selection shape matches the production scenario exactly. The unit test file this comment was on has been deleted.

Pasting block content with the cursor on a blank list item inside a
blockquote crashed with Lexical error #66 ("Expected node to have a
parent"). Lexical's RangeSelection.insertNodes calls insertParagraph,
which triggered EarlyEscapeListItemNode's early-escape behavior: it
removed the list item that insertNodes still references, so the
follow-up insertRangeAfter called insertAfter on a detached node and
threw. The pasted content was lost too.

Guard the escape with PASTE_TAG, mirroring EarlyEscapeCodeNode: pasting
is not an escape gesture, so defer to the default list item behavior.
@jorgemanrubia jorgemanrubia force-pushed the sentry-lexical-error-66 branch from 207c259 to 3f36003 Compare June 10, 2026 18:54
@jorgemanrubia jorgemanrubia marked this pull request as draft June 10, 2026 18:55
@jorgemanrubia jorgemanrubia marked this pull request as ready for review June 11, 2026 05:21
@romansklenar

Copy link
Copy Markdown

Another lexxy consumer (Rails, lexxy 0.9.12.beta) hitting #66 at the same RangeSelection.insertNodesinsertAftergetParentOrThrow spot in production — flagging because our trigger differs, so it may be a sibling path your EarlyEscapeListItemNode guard doesn't cover.

We insert a saved-signature HTML snippet by dispatching a synthetic paste ClipboardEvent into the contenteditable (to reuse Lexical's paste pipeline). That goes Contents#insertAtCursorinsertNodesinsertAftergetParentOrThrow#66. Chrome 148 / Windows, real user.

We can't tell from the Sentry event whether the caret was on a blank list item inside a blockquote (your exact repro) — if it was, treat this as a +1; if not, there may be a second paste-driven getParentOrThrow trigger worth a look. Either way, your guarding #shouldEscape with $hasUpdateTag(PASTE_TAG) looks right.

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.

3 participants