Skip to content

bugfix: normalize ragged tables left by updates that skip transforms - #9459

Open
madsysharma wants to merge 1 commit into
meta:mainfrom
madsysharma:bug/ragged-tables
Open

madsysharma wants to merge 1 commit into
meta:mainfrom
madsysharma:bug/ragged-tables

Conversation

@madsysharma

Copy link
Copy Markdown

[lexical-table] Bug Fix: Normalize ragged tables left by updates that skip transforms

Description

Current behavior

The TableNode transform pads ragged tables via $padTableRows, but only for tables that an update marks dirty. While checking #9453, I tested each load path the issue lists:

  • JSON loaded through setEditorState(parseEditorState(json)) is already padded: a parsed editor state marks every node dirty, so the transform runs. Tables that exist when the plugin registers are also covered, because registerNodeTransform marks existing nodes dirty.
  • Updates applied with skipTransforms stay ragged, even with TablePlugin/TableExtension registered: this is how collaboration updates arrive. The table then stays ragged indefinitely, because later updates don't mark it dirty again.
  • Editors without the table transform stay ragged: (for example, headless editors that only register the nodes). Their HTML export is ragged too.

Option 1 in the issue, padding in importJSON/updateFromJSON, isn't feasible. $parseSerializedNodeImpl calls those methods before it appends the table's rows, so there's nothing to pad at that point.

Changes

  • registerTablePlugin now registers an update listener: i.e., registerRaggedTableNormalization. When a committed update leaves a dirty TableNode ragged, the listener pads it with $padTableRows in a follow-up update tagged HISTORY_MERGE_TAG, so the padding is never an undo step of its own. Because TableExtension calls registerTablePlugin, both entry points get the listener.
  • The listener deliberately skips COLLABORATION_TAG updates: each peer receives the same remote change. If each peer padded it, the CRDT would keep all of the appended cells, making the row wider than the others, and the next transform would then pad the remaining rows, so the table would keep growing. Remote ragged tables thus stay ragged in the model until a local table command pads them (the existing behavior from issue [lexical-table][lexical-website] Bug Fix: Table commands throw or misbehave on a ragged table #9375).
  • The listener also skips HISTORIC_TAG updates, so that padding never rewrites the state being restored by undo or redo.
  • TableNode.exportDOM now pads the exported HTML of a ragged table to a rectangle without touching editor state. This makes export correct in headless editors and during collaboration. Padding cells mirror $padTableRows: a cell becomes a <th> when the row's last cell has the row-header bit, and it uses the same inline styles TableCellNode.exportDOM writes for a default cell. Partial exports of a selection are left unpadded, because the exported rows no longer line up with the table map.
  • The raggedness check is cheap for the common case: for tables without spans, it compares row sizes directly, so the table map is only computed when a colSpan or rowSpan is present. Typing inside an ordinary table therefore doesn't compute the table map on every update.

No public API was added; both new functions are module-private.

Closes #9453

Test plan

Before

  • A ragged table inserted in an update with skipTransforms: true stayed ragged ([2, 1] cells per row) with TableExtension registered, and it stayed ragged after subsequent updates.
  • Exporting a ragged model to HTML with $generateHtmlFromNodes produced rows of different lengths (3, 1, 2 cells).

After

New file: packages/lexical-table/src/__tests__/unit/RaggedTableNormalization.test.ts (11 tests).

  • JSON loading: a table loaded with setEditorState(parseEditorState(...)) is rectangular. This locks in the existing behavior.
  • Updates that skip transforms: a ragged table from a skipTransforms update is padded, including rows whose width comes from a colSpan.
  • History: with HistoryExtension, a single undo removes both the table and its padding. This test fails if the padding update is not tagged HISTORY_MERGE_TAG.
  • No unnecessary work: a rectangular table triggers no follow-up padding update.
  • Exclusions: updates tagged COLLABORATION_TAG or HISTORIC_TAG are not padded in the model.
  • HTML export:
    • A ragged model exports as a rectangular table.
    • Padding in a header row exports as <th>.
    • A ragged collaboration update still exports rectangular HTML.
    • A rectangular table exports with no extra cells.

The three model-padding tests fail with the listener disabled, and the undo test fails without HISTORY_MERGE_TAG.

Existing suites pass: lexical core, lexical-react (including the collaboration tests), lexical-yjs, lexical-history, lexical-clipboard, lexical-markdown, lexical-table, lexical-html, the playground unit tests, and every other unit test file that uses tables. tsc --noEmit and ESLint are clean.

@vercel

vercel Bot commented Oct 24, 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 Oct 29, 2026 10:36pm UTC
lexical-playground Ready Ready Preview Oct 29, 2026 10:36pm 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 Oct 24, 2026
@madsysharma

Copy link
Copy Markdown
Author

Hi @acywatson @etrepum @fantactuka @ivailop7 @potatowagon @zurfyx - please review this PR and let me know if everything is good to go. Thank you.

@pkg-pr-new

pkg-pr-new Bot commented Oct 25, 2026

Copy link
Copy Markdown
lexical

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

@lexical/a11y

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

@lexical/clipboard

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

@lexical/code

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

@lexical/code-core

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

@lexical/code-prism

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

@lexical/code-shiki

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

@lexical/compiler

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

@lexical/devtools-core

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

@lexical/dragon

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

@lexical/eslint-plugin

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

@lexical/extension

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

@lexical/fast-check

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

@lexical/file

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

@lexical/hashtag

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

@lexical/headless

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

@lexical/history

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

@lexical/html

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

@lexical/internal

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

@lexical/link

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

@lexical/list

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

@lexical/mark

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

@lexical/markdown

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

@lexical/mdast

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

@lexical/offset

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

@lexical/overflow

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

@lexical/plain-text

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

@lexical/react

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

@lexical/rich-text

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

@lexical/selection

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

@lexical/table

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

@lexical/tailwind

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

@lexical/text

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

@lexical/utils

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

@lexical/yjs

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

commit: bc04043

@madsysharma

Copy link
Copy Markdown
Author

Hi @acywatson @etrepum @fantactuka @ivailop7 @potatowagon @zurfyx - please review this PR and let me know if everything is good to go. Thank you.

This branch was successfully deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[lexical-table] Ragged tables stay ragged until the table is next touched

1 participant