Skip to content

[lexical-table] Bug Fix: Leave a cut with no end in the table to the editor's own cut - #9708

Open
therealemmanueloppong wants to merge 2 commits into
meta:mainfrom
therealemmanueloppong:fix-table-cut-outside-range
Open

therealemmanueloppong wants to merge 2 commits into
meta:mainfrom
therealemmanueloppong:fix-table-cut-outside-range

Conversation

@therealemmanueloppong

@therealemmanueloppong therealemmanueloppong commented Nov 1, 2026 •

Copy link
Copy Markdown

Description

Current behavior: once a document holds a table, the table's CUT_COMMAND listener (COMMAND_PRIORITY_HIGH) answers every range selection, wherever the range is. For a range outside the table it copies the range, $deleteCellHandler returns false, then it calls selection.removeText() and returns true. So rich text's own cut never runs (a whole-document cut is not widened to its blocks and leaves the first block's type behind, and neither update is tagged CUT_TAG), and no listener below COMMAND_PRIORITY_HIGH runs either.

This PR makes the listener return false for a range selection with neither end inside its table, so the editor's own cut and other listeners handle it as they would with no table in the document. A table selection, a range in a cell, and a range with an end in the table are unchanged.

It also fixes a document with more than one table. Every table registers its own listener, and before this change the first table answered every range, including one that ends in a second table, so that table never widened the range around itself: cutting from a cell of the second table to the text after it left a one-cell table behind. Now the table the range ends in answers, and removes the table whole, as in a single-table document.

One existing test changes: "Cut table in the middle of a range selection" relied on the table cutting a range whose ends are both outside the table. It now registers rich text and waits for the cut to finish, since rich text removes the range after its copy resolves. It gives the same <p><br></p>.

The diagnosis and the first version of the diff are from the issue.

Closes #9281

Test plan

New file: packages/lexical-table/src/__tests__/unit/CutOutsideTable.test.ts (a range outside the table reaches a LOW listener; a range in a cell stays the table's; a whole-document cut over heading, table and paragraph leaves one paragraph in an update tagged CUT_TAG; the two-table case).

Before

pnpm exec vitest run --project unit packages/lexical-table/src/__tests__/unit/CutOutsideTable.test.ts on main's LexicalTableSelectionHelpers.ts:

     × a range outside the table is left to the listeners below the table 88ms
     × a whole-document cut leaves one paragraph, in updates tagged CUT_TAG 1019ms
     × the table a range ends in cuts it whole when the document holds another table 1020ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 3 ⎯⎯⎯⎯⎯⎯⎯
AssertionError: expected +0 to be 1 // Object.is equality
AssertionError: expected [ 'heading' ] to deeply equal [ 'paragraph' ]
AssertionError: expected [ 'table', 'table', 'paragraph' ] to deeply equal [ 'table', 'paragraph' ]
      Tests  3 failed | 1 passed (4)

After

$ pnpm exec vitest run --project unit packages/lexical-table packages/lexical-rich-text packages/lexical-clipboard
$ pnpm exec vitest run --project unit packages/lexical-table/src/__tests__/unit/CutOutsideTable.test.ts
 Test Files  52 passed (52)
      Tests  454 passed (454)
      Tests  4 passed (4)

pnpm run tsc, pnpm run check-changesets, eslint and prettier on the changed files are clean.

Only the unit tests (jsdom, macOS) were run; the e2e and browser suites were not.

@vercel

vercel Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
lexical Ready Ready Preview Nov 1, 2026 10:14pm UTC
lexical-playground Ready Ready Preview Nov 1, 2026 10:14pm UTC

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 Nov 1, 2026
@meta-code-review

meta-code-review Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 1, 2026, 10:32 PM UTC

@meta-code-review meta-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the fix. This looks good to merge as is.

The early return in packages/lexical-table/src/LexicalTableSelectionHelpers.ts (lines 970-977) hands a range with neither end in the table back to rich text's cut and to lower-priority CUT_COMMAND listeners, which fixes #9281. Select All + Cut over a heading, a table and a paragraph now leaves an empty paragraph, and its updates carry CUT_TAG. Cuts the table owns (a table selection, a range in a cell, a range with one end in the table) behave as before. The new test fails without the change and passes with it.

A few optional, non-blocking suggestions:

  1. Add a Verify: paragraph to the changeset (.changeset/leave-a-cut-whose-range-has-no-end.md). CONTRIBUTING.md (Changesets) and AGENTS.md ask a fix that changes observable editing behavior to carry one. For example:

    Verify: in a document that holds a table, cut a range with neither end in the table (for example Select All over a heading, a table and a paragraph) and check that CUT_COMMAND listeners below COMMAND_PRIORITY_HIGH run, the removal arrives in an update tagged CUT_TAG once the clipboard write resolves, and an empty paragraph rather than an empty heading is left. In a document with two tables, cutting from a cell of the second table to the text after it removes that table whole.
  2. Assert the outcome the issue describes (packages/lexical-table/src/__tests__/unit/CutOutsideTable.test.ts, lines 74-85). The test checks that a LOW listener ran. Registering rich text and asserting that a whole-document cut over heading/table/paragraph leaves a single paragraph, with an update tagged CUT_TAG, would pin the behavior people actually hit.

  3. Mention and test the two-table case. Every table registers its own HIGH cut listener, and before this change the first table answered every range. That included a range with one end in a second table, which then never got that table's widening: cutting from a cell of a second 2×2 table to the text after it left a one-cell table behind. With this change the owning table answers and removes the table whole, as in a single-table document. The description says ranges with an end in the table are unchanged, so this improvement deserves a sentence there and a test.

  4. Paste the Test plan output. AGENTS.md (Commits and Pull Requests) asks the ### Before / ### After sections to include the real output of the commands that were run, not a description of it.

@pkg-pr-new

pkg-pr-new Bot commented Nov 1, 2026

Copy link
Copy Markdown
lexical

npm i https://pkg.pr.new/lexical@9708

@lexical/a11y

npm i https://pkg.pr.new/@lexical/a11y@9708

@lexical/clipboard

npm i https://pkg.pr.new/@lexical/clipboard@9708

@lexical/code

npm i https://pkg.pr.new/@lexical/code@9708

@lexical/code-core

npm i https://pkg.pr.new/@lexical/code-core@9708

@lexical/code-prism

npm i https://pkg.pr.new/@lexical/code-prism@9708

@lexical/code-shiki

npm i https://pkg.pr.new/@lexical/code-shiki@9708

@lexical/compiler

npm i https://pkg.pr.new/@lexical/compiler@9708

@lexical/devtools-core

npm i https://pkg.pr.new/@lexical/devtools-core@9708

@lexical/dragon

npm i https://pkg.pr.new/@lexical/dragon@9708

@lexical/eslint-plugin

npm i https://pkg.pr.new/@lexical/eslint-plugin@9708

@lexical/extension

npm i https://pkg.pr.new/@lexical/extension@9708

@lexical/fast-check

npm i https://pkg.pr.new/@lexical/fast-check@9708

@lexical/file

npm i https://pkg.pr.new/@lexical/file@9708

@lexical/hashtag

npm i https://pkg.pr.new/@lexical/hashtag@9708

@lexical/headless

npm i https://pkg.pr.new/@lexical/headless@9708

@lexical/history

npm i https://pkg.pr.new/@lexical/history@9708

@lexical/html

npm i https://pkg.pr.new/@lexical/html@9708

@lexical/internal

npm i https://pkg.pr.new/@lexical/internal@9708

@lexical/link

npm i https://pkg.pr.new/@lexical/link@9708

@lexical/list

npm i https://pkg.pr.new/@lexical/list@9708

@lexical/mark

npm i https://pkg.pr.new/@lexical/mark@9708

@lexical/markdown

npm i https://pkg.pr.new/@lexical/markdown@9708

@lexical/mdast

npm i https://pkg.pr.new/@lexical/mdast@9708

@lexical/offset

npm i https://pkg.pr.new/@lexical/offset@9708

@lexical/overflow

npm i https://pkg.pr.new/@lexical/overflow@9708

@lexical/plain-text

npm i https://pkg.pr.new/@lexical/plain-text@9708

@lexical/react

npm i https://pkg.pr.new/@lexical/react@9708

@lexical/rich-text

npm i https://pkg.pr.new/@lexical/rich-text@9708

@lexical/selection

npm i https://pkg.pr.new/@lexical/selection@9708

@lexical/table

npm i https://pkg.pr.new/@lexical/table@9708

@lexical/tailwind

npm i https://pkg.pr.new/@lexical/tailwind@9708

@lexical/text

npm i https://pkg.pr.new/@lexical/text@9708

@lexical/utils

npm i https://pkg.pr.new/@lexical/utils@9708

@lexical/y

npm i https://pkg.pr.new/@lexical/y@9708

@lexical/yjs

npm i https://pkg.pr.new/@lexical/yjs@9708

commit: d1301be

@meta-code-review meta-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for the fix. This looks good to me.

The early return in the table's CUT_COMMAND listener (packages/lexical-table/src/LexicalTableSelectionHelpers.ts, lines 970-977) hands a range with neither end in the table to rich text's cut and to lower-priority listeners. That fixes #9281. Table selections, ranges in a cell and ranges with one end in the table stay on the table's path.

What I checked:

  • Against the base implementation, three of the four new CutOutsideTable.test.ts cases fail with the errors in your Before section, and all four pass on this head. The lexical-table, lexical-rich-text and lexical-clipboard unit suites also pass here, as do pnpm run tsc and pnpm run check-changesets.
  • In the playground in Chromium, Select All followed by Ctrl+X over a heading, a table and a paragraph now leaves one empty paragraph, with CUT_TAG updates and the lower-priority cut listeners running. On main it leaves an empty Heading 1. Cutting text inside a cell behaves the same as before.
  • The two-table case from the description behaves as the new test pins it: the table the range ends in removes itself whole.
  • Undo after these cuts restores the document, as it does on main.
  • The changeset follows CONTRIBUTING.md (Changesets), including the Verify: paragraph a behavior fix needs.

No changes requested.

@therealemmanueloppong

Copy link
Copy Markdown
Author

Heads up, and apologies for the noise: after opening this I noticed #9285 by @rinarakaki, opened on Sept 30, which fixes the same issue (#9281). I missed it when I checked for existing PRs.

The two take the same approach in LexicalTableSelectionHelpers.ts. This one also adds a test for a document with two tables (cutting from a cell of the second table to the text after it) and an outcome test for a whole-document cut, in case either is useful there.

Please merge whichever you prefer. I am happy to close this one in favor of #9285, or to rebase it onto it, just say which.

This branch was successfully deployed

2 active deployments
Preview – lexical — fcf6b8bf Deployed Nov 1, 2026 by vercel[bot]
Preview – lexical-playground — fcf6b8bf Deployed Nov 1, 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.

1 participant