[lexical-clipboard] Bug Fix: Keep dragged text when it is dropped on the edge of its own selection by kwy404 · Pull Request #9241 · facebook/lexical · GitHub
Skip to content

[lexical-clipboard] Bug Fix: Keep dragged text when it is dropped on the edge of its own selection - #9241

Merged
etrepum merged 2 commits into
facebook:mainfrom
kwy404:fix-drop-on-own-selection-edge
Sep 26, 2026
Merged

etrepum merged 2 commits into
facebook:mainfrom
kwy404:fix-drop-on-own-selection-edge

Conversation

@kwy404

@kwy404 kwy404 commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

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: $isDropCaretInsideSelection compares with a strict < 0 on both sides, so a drop exactly on an edge of the dragged range counts as outside it. $doDrop has 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:

  • $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 #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.

…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.
@vercel

vercel Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

@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 Sep 26, 2026
@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label Sep 26, 2026
## 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
etrepum added this pull request to the merge queue Sep 26, 2026
Merged via the queue into facebook:main with commit c58ded4 Sep 26, 2026
42 checks passed
kwy404 added a commit to kwy404/kwy404 that referenced this pull request Sep 26, 2026
@etrepum etrepum mentioned this pull request Sep 28, 2026

This branch was successfully deployed

2 active deployments
Preview – lexical — 08188f76 Deployed Sep 26, 2026 by vercel[bot]
Preview – lexical-playground — 08188f76 Deployed Sep 26, 2026 by vercel[bot]
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: [lexical-clipboard] Dropping dragged text on the end of its own selection deletes it

3 participants