{{ message }}
Repository navigation
[lexical-clipboard] Bug Fix: Keep dragged text when it is dropped on the edge of its own selection - #9241
Merged
Conversation
…the edge of its own selection ## Description Dragging text and dropping it on the end of its own selection deletes it. For example, in `Hello world`, select `Hello` and drop it right after itself: the paragraph becomes ` world`. Root cause: `$doDrop` splits at the drop point first. When the range starts at the beginning of a text node, a drop on its end splits at the end of that same node, so `$splitTextPointCaret` returns a caret whose origin is the dragged node. `$isDropCaretInsideSelection` compares with `< 0` on both sides, so an edge drop counts as outside the range. `removeText()` then removes the dragged node, which is also the caret origin, and the detached origin check returns without inserting anything. A fully selected node (e.g. a bold word) dropped at the start of the next text node is lost the same way, because that edge resolves to the next node and the split rewinds the caret onto the dragged node. Fix (the two changes suggested in the issue): - `$isDropCaretInsideSelection` uses `<= 0`, so a drop exactly on either edge of the range is a no-op, like a drop inside it. - If the caret origin was removed anyway (the edge resolved to the adjacent node), the data is inserted where `removeText()` left the selection, which is that same edge, instead of being dropped. Neither change alone covers both cases. Closes facebook#9232 ## Test plan Added a unit test to `HandleTextDrop.test.ts` that selects a bold `Hello` before ` world`, drops it at the end of `Hello`, then drops it at the start of ` world`, and checks the text is still `Hello world` after each drop. ### Before ``` $ npx vitest --project unit --no-watch packages/lexical/src/__tests__/unit/HandleTextDrop.test.ts × keeps the dragged text when dropped on the end edge of the source range AssertionError: expected ' world' to be 'Hello world' // Object.is equality Tests 1 failed | 11 passed (12) ``` ### After ``` $ npx vitest --project unit --no-watch packages/lexical/src/__tests__/unit/HandleTextDrop.test.ts Tests 12 passed (12) ``` The lexical-clipboard, lexical-rich-text and lexical-plain-text unit tests also pass (210 tests), as do `tsc -p tsconfig.test.json`, prettier and eslint on the changed files. The browser and e2e suites were not run locally.
kwy404
requested review from
acywatson,
etrepum,
fantactuka,
ivailop7,
potatowagon and
zurfyx
as code owners
September 26, 2026 09:23
## Description Dropping a selection on an equivalent boundary in an adjacent node still deletes and reinserts the selection in facebook#9241. In plain-text editors this converts tokens and decorators into text. The detached-caret fallback also inserts at the source when a cross-paragraph deletion removes the element that held the destination. Compare canonical text, element, and inline-wrapper boundaries before mutating the document. Drops inside the selection or on either edge now preserve node identity and selection direction. Normalize the destination to text where possible, split only text, and retain both sides of the insertion boundary so the destination survives paragraph merges. This also avoids introducing paragraph breaks when an element point is hit. Add 60 regression cases covering rich-text and plain-text handlers, forward and backward selections, token interiors and edges, adjacent text and element points, nested inline wrappers, decorators, and text/paragraph/root destinations after cross-paragraph deletion. This commit applies on top of PR facebook#9241 at 36d4dc2. Closes facebook#9232 ## Test plan ### Before The new tests with clipboard.ts from 36d4dc2: ``` $ node_modules/.bin/vitest --project unit --run packages/lexical/src/__tests__/unit/HandleTextDrop.test.ts Test Files 1 failed (1) Tests 48 failed | 24 passed (72) ``` ### After ``` $ node_modules/.bin/vitest --project unit --run packages/lexical/src/__tests__/unit/HandleTextDrop.test.ts Test Files 1 passed (1) Tests 72 passed (72) $ node_modules/.bin/vitest --project unit --run packages/lexical-clipboard packages/lexical-rich-text packages/lexical-plain-text packages/lexical/src/__tests__/unit/HandleTextDrop.test.ts Test Files 26 passed (26) Tests 270 passed (270) $ node_modules/.bin/tsc -p tsconfig.test.json --noEmit $ node_modules/.bin/eslint packages/lexical-clipboard/src/clipboard.ts packages/lexical/src/__tests__/unit/HandleTextDrop.test.ts $ node_modules/.bin/prettier --check packages/lexical-clipboard/src/clipboard.ts packages/lexical/src/__tests__/unit/HandleTextDrop.test.ts Checking formatting... All matched files use Prettier code style! ``` Browser/E2E tests were not run: Playwright's Chromium installation failed because the browser archive download was invalid or truncated.
etrepum
approved these changes
Sep 26, 2026
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Description
Dragging text and dropping it on the end of its own selection deletes it. For example, in
Hello world, selectHelloand drop it right after itself: the paragraph becomesworld.Root cause:
$isDropCaretInsideSelectioncompares with a strict< 0on both sides, so a drop exactly on an edge of the dragged range counts as outside it.$doDrophas already split the text at the drop point, and when the range starts at the beginning of a text node, the caret it will insert at is anchored to the dragged text itself.removeText()removes that text, the caret is detached, and the detached origin check returns without inserting anything. A fully selected node (e.g. a bold word) dropped at the start of the next text node is lost the same way, because the split moves that caret back onto the dragged node.Fix, following the two changes suggested in the issue:
$isDropCaretInsideSelectionuses<= 0, so a drop exactly on either edge of the range is a no-op, like a drop inside it.removeText()left the selection, which is that same edge, instead of being dropped.Neither change alone covers both cases.
Closes #9232
Test plan
Added a unit test to
HandleTextDrop.test.tsthat selects a boldHellobeforeworld, drops it at the end ofHello, then drops it at the start ofworld, and checks the text is stillHello worldafter each drop.Before
After
The lexical-clipboard, lexical-rich-text and lexical-plain-text unit tests also pass (210 tests), as do
tsc -p tsconfig.test.json, prettier and eslint on the changed files. The browser and e2e suites were not run locally.