Skip to content

[lexical][lexical-clipboard] Bug Fix: Preserve alignment when pasting selected text#8671

Open
Rohithmatham12 wants to merge 3 commits into
facebook:mainfrom
Rohithmatham12:preserve-pasted-alignment
Open

[lexical][lexical-clipboard] Bug Fix: Preserve alignment when pasting selected text#8671
Rohithmatham12 wants to merge 3 commits into
facebook:mainfrom
Rohithmatham12:preserve-pasted-alignment

Conversation

@Rohithmatham12

@Rohithmatham12 Rohithmatham12 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Description

When copying selected text from an aligned paragraph inside the same Lexical namespace, the application/x-lexical-editor clipboard payload took priority over text/html. That JSON payload only contained the selected TextNode, so pasting into another paragraph reused the destination paragraph's default alignment and discarded the source paragraph alignment.

This updates clipboard JSON export to retain the non-inline parent element when the selected child content covers the full element text. That preserves block-level formatting such as paragraph alignment for full-paragraph text selections while partial text selections remain text-only.

Closes #8101

Test plan

Before

Copying all visible text from an aligned paragraph and pasting it elsewhere lost the source paragraph alignment.

After

Full-paragraph text selections preserve block-level alignment on paste, including paragraphs that contain inline elements such as links, while partial text selections remain text-only.

Automated tests:

  • node node_modules/vitest/vitest.mjs --project unit packages/lexical/src/__tests__/unit/HTMLCopyAndPaste.test.ts --run
  • node node_modules/vitest/vitest.mjs --project unit packages/lexical-clipboard/src/__tests__/unit/GetClipboardDataExtension.test.ts packages/lexical-clipboard/src/__tests__/unit/ClipboardImportExtension.test.ts --run
  • node node_modules/prettier/bin/prettier.cjs --check packages/lexical-clipboard/src/clipboard.ts packages/lexical/src/__tests__/unit/HTMLCopyAndPaste.test.ts
  • node node_modules/eslint/bin/eslint.js packages/lexical-clipboard/src/clipboard.ts packages/lexical/src/__tests__/unit/HTMLCopyAndPaste.test.ts

@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview, Comment Jun 23, 2026 12:22am
lexical-playground Ready Ready Preview, Comment Jun 23, 2026 12:22am

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jun 10, 2026

@potatowagon potatowagon 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.

Reviewed by Navi (Tater Thoughts Bobblehead) on behalf of @potatowagon.

Assessment: LGTM with minor note

What this does: Fixes #8101 — when copying text from an aligned paragraph within the same Lexical editor, the application/x-lexical-editor clipboard JSON only contained the TextNode, losing block-level formatting (alignment, indent). The fix adds $shouldIncludeElementWithSelectedChildren() which includes the parent element in the clipboard payload when the selected children cover the full element text content.

What I checked:

  1. Logic correctness: The function checks: (a) is range selection, (b) node is non-inline ElementNode, (c) not root, (d) selectedNodes content under this parent matches the parent's full text content. This correctly identifies "entire paragraph selected" without false positives on partial selections.
  2. Edge cases considered:
    • Partial text selection → selectedTextContent !== node.getTextContent() → returns false ✅
    • Empty paragraph → selectedTextContent !== "" guard → returns false ✅
    • Inline elements (links, marks) → node.isInline() guard → skipped ✅
    • Root node → $isRootNode(node) guard → skipped ✅
    • Multiple children in paragraph (e.g. bold+plain) → uses $sliceSelectedTextNodeContent per node → correct ✅
  3. Performance note: The function iterates selectedNodes and calls isParentOf() for each. For very large selections this is O(n×depth), but clipboard operations are user-initiated and typically small. Acceptable.
  4. Test coverage: Good test — creates a centered paragraph, copies via $getClipboardDataFromSelection, pastes into another paragraph, asserts alignment is preserved in DOM output.
  5. www compat: No API signature changes. $shouldIncludeElementWithSelectedChildren is module-private. The behavioral change (richer clipboard payload) is strictly additive — paste targets that ignore element wrappers will work as before.

One observation: The text-content comparison (selectedTextContent === node.getTextContent()) is a heuristic — it could theoretically match if a partial selection happens to produce the same string as the full element (e.g., repeated characters). This is an extremely unlikely edge case and the consequence (including alignment where it shouldn't) is benign.

CI: CLA ✅, Vercel ✅. Full test matrix pending (PR is ~30min old). Recommend waiting for unit green.

Ready to approve once full CI passes.

@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label Jun 10, 2026
@etrepum

etrepum commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Title and description doesn't match the pull request template

@Rohithmatham12 Rohithmatham12 changed the title Preserve alignment when pasting selected text [lexical][lexical-clipboard] Bug Fix: Preserve alignment when pasting selected text Jun 11, 2026
@mayrang

mayrang commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

The approach makes sense, but $shouldIncludeElementWithSelectedChildren breaks on paragraphs containing inline elements (links, marks, etc.). selection.getNodes() includes intermediate element nodes, so their getTextContent() gets double-counted in the join — e.g. "Hello worldworld" vs "Hello world". Tested locally and confirmed the alignment isn't preserved when a link is present.

Details and a suggested fix inline. Also noted how HeadingNode handles the same problem via extractWithChild for reference.

if (selectedNodes.length === 0) {
return false;
}
const selectedTextContent = selectedNodes

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.

selection.getNodes() returns intermediate element nodes too — e.g. for "Hello " + <a>world</a>, it returns [TextNode, LinkNode, TextNode]. The join then produces "Hello worldworld" (LinkNode's getTextContent() + child TextNode), which never matches the paragraph's "Hello world". So this function returns false for any paragraph containing links, marks, or other inline elements.

Filtering to leaf nodes (e.g. .filter(n => !$isElementNode(n))) before the comparison would fix it.


if (shouldInclude && !shouldExclude) {
targetArray.push(serializedNode);
} else if (

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.

For context — HeadingNode already overrides extractWithChild to return true, so heading alignment is preserved on copy without needing this new function. ParagraphNode doesn't override it (defaults to false), which is why paragraph formatting gets stripped.

The full-text check makes sense if you only want full-paragraph selections to carry alignment. Just noting the existing mechanism since it covers a related case.


target.select(0, target.getChildrenSize());

const selection = $getSelection();

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.

target.select() returns RangeSelection directly — the $getSelection() + invariant($isRangeSelection) round-trip can be replaced with const selection = target.select(0, target.getChildrenSize()).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

const selection = target.select(0) is also sufficient, the default for both select arguments is the end (target.select() and target.selectEnd() are equivalent)

@Rohithmatham12

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback in 752a47ecc:

  • filtered selected intermediate ElementNodes out of $shouldIncludeElementWithSelectedChildren() so inline elements like links are not double-counted
  • added regression coverage for preserving centered paragraph alignment when the selected paragraph contains a link
  • simplified the paste target selection to use the RangeSelection returned by target.select()

Re-ran:

node node_modules/vitest/vitest.mjs --project unit packages/lexical/src/__tests__/unit/HTMLCopyAndPaste.test.ts --run
node node_modules/vitest/vitest.mjs --project unit packages/lexical-clipboard/src/__tests__/unit/GetClipboardDataExtension.test.ts packages/lexical-clipboard/src/__tests__/unit/ClipboardImportExtension.test.ts --run
node node_modules/prettier/bin/prettier.cjs --check packages/lexical-clipboard/src/clipboard.ts packages/lexical/src/__tests__/unit/HTMLCopyAndPaste.test.ts
node node_modules/eslint/bin/eslint.js packages/lexical-clipboard/src/clipboard.ts packages/lexical/src/__tests__/unit/HTMLCopyAndPaste.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Text alignment is lost on copy-paste

4 participants