Skip to content

[lexical-table][lexical-y] Bug Fix: Don't throw when a table exists before its DOM - #9501

Merged
zurfyx merged 2 commits into
mainfrom
zurfyx/table-observer-no-dom
Oct 29, 2026
Merged

zurfyx merged 2 commits into
mainfrom
zurfyx/table-observer-no-dom

Conversation

@zurfyx

@zurfyx zurfyx commented Oct 28, 2026

Copy link
Copy Markdown
Member

Description

Building an editor throws TableObserver: Expected to find TableElement in DOM for key N when the editor state already holds a table before TableExtension registers and the editor has no root element yet. registerTableSelectionObserver registers its TableNode mutation listener with skipInitialization: false, so it receives created for every existing table at registration, and $getTableAndElementByKey asserts that the table's DOM exists.

Any extension listed before TableExtension that sets content in its register hits this. For example, YCollabExtension reads a Y.Doc that already holds a table (a remount, a doc loaded from storage, or a provider that synced first):

buildEditorFromExtensions(
  defineExtension({
    dependencies: [
      RichTextExtension,
      configExtension(YCollabExtension, {doc}),
      TableExtension,
    ],
    name: 'app',
  }),
);

So does any earlier extension that calls editor.setEditorState(...) with a table, or a direct call to registerTableSelectionObserver on an unrendered editor. Listing TableExtension first avoided it, but nothing documented that order.

With this change, the observer skips a table that has no DOM and drops any observer it still holds for that key. When the table is rendered, for example by setRootElement's full reconcile, the reconciler reports it as created, and the observer attaches then. A table with DOM is handled as before.

The fix is in the observer rather than in @lexical/y, which could defer its initial read until every extension has registered. A deferred read would change when the binding's content is available to the rest of the editor. It would also leave the observer throwing for every other source of pre-render content.

Changeset

.changeset/table-observer-unrendered-table.md (@lexical/table: patch, [fix]).

Test plan

New tests:

  • packages/lexical-table/src/__tests__/unit/TableSelectionObserverBeforeDOM.test.ts, with no collaboration:
    • an extension listed before TableExtension sets a state holding a table in its register;
    • the observer attaches when a root element renders the table, and again on the new DOM when the root is replaced;
    • registerTableSelectionObserver on a createEditor editor whose table is not rendered yet.
  • packages/lexical-y/src/__tests__/unit/TableOnOpen.test.ts: a document that already holds a table opens with either extension listed first.

./node_modules/.bin/vitest --run --project unit packages/lexical-table/src/__tests__/unit/TableSelectionObserverBeforeDOM.test.ts packages/lexical-y/src/__tests__/unit/TableOnOpen.test.ts

Before

× builds when an earlier extension sets a state holding a table during register 14ms
× observes the table once a root element renders it 1ms
× observes the new table DOM when the root element is replaced 1ms
× registerTableSelectionObserver accepts an unrendered table 1ms
× opens a document that holds a table with YCollabExtension listed first 26ms
✓ opens a document that holds a table with TableExtension listed first 4ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 5 ⎯⎯⎯⎯⎯⎯⎯
Error: TableObserver: Expected to find TableElement in DOM for key 19
Error: TableObserver: Expected to find TableElement in DOM for key 49
Error: TableObserver: Expected to find TableElement in DOM for key 79
Error: TableObserver: Expected to find TableElement in DOM for key 91
Error: TableObserver: Expected to find TableElement in DOM for key 15
      Tests  5 failed | 1 passed (6)

After

✓ |unit| packages/lexical-table/src/__tests__/unit/TableSelectionObserverBeforeDOM.test.ts (4 tests) 187ms
✓ |unit| packages/lexical-y/src/__tests__/unit/TableOnOpen.test.ts (2 tests) 111ms
      Tests  6 passed (6)

Also run with the fix:

  • @lexical/table unit tests: 23 files, 229 tests passed. The new file is 4 of them.
  • @lexical/y unit tests: 149 files passed and 2 skipped; 4,915 tests passed, 16 expected fail, 2 skipped.
  • Full unit suite: 540 files passed and 3 skipped; 11,754 tests passed, 16 expected fail, 4 skipped.
  • Playground Tables.spec.mjs e2e on Chromium: 86 passed, 1 skipped.
  • pnpm run ci-check passes.
  • Not run: Firefox and WebKit e2e, and browser-mode unit tests. The change only affects tables that have no DOM, which no browser test renders.

…efore its DOM

## Description

Building an editor throws `TableObserver: Expected to find TableElement in DOM for key N` when the editor state already holds a table before `TableExtension` registers and the editor has no root element yet. `registerTableSelectionObserver` registers its `TableNode` mutation listener with `skipInitialization: false`, so it receives `created` for every existing table at registration, and `$getTableAndElementByKey` asserts that the table's DOM exists.

Any extension listed before `TableExtension` that sets content in its `register` hits this. For example, `YCollabExtension` reads a `Y.Doc` that already holds a table (a remount, a doc loaded from storage, or a provider that synced first):

```ts
buildEditorFromExtensions(
  defineExtension({
    dependencies: [
      RichTextExtension,
      configExtension(YCollabExtension, {doc}),
      TableExtension,
    ],
    name: 'app',
  }),
);
```

So does any earlier extension that calls `editor.setEditorState(...)` with a table, or a direct call to `registerTableSelectionObserver` on an unrendered editor. Listing `TableExtension` first avoided it, but nothing documented that order.

With this change, the observer skips a table that has no DOM and drops any observer it still holds for that key. When the table is rendered, for example by `setRootElement`'s full reconcile, the reconciler reports it as `created`, and the observer attaches then. A table with DOM is handled as before.

The fix is in the observer rather than in `@lexical/y`, which could defer its initial read until every extension has registered. A deferred read would change when the binding's content is available to the rest of the editor. It would also leave the observer throwing for every other source of pre-render content.

## Test plan

New tests:
- `packages/lexical-table/src/__tests__/unit/TableSelectionObserverBeforeDOM.test.ts`, with no collaboration:
  - an extension listed before `TableExtension` sets a state holding a table in its `register`;
  - the observer attaches when a root element renders the table, and again on the new DOM when the root is replaced;
  - `registerTableSelectionObserver` on a `createEditor` editor whose table is not rendered yet.
- `packages/lexical-y/src/__tests__/unit/TableOnOpen.test.ts`: a document that already holds a table opens with either extension listed first.

`./node_modules/.bin/vitest --run --project unit packages/lexical-table/src/__tests__/unit/TableSelectionObserverBeforeDOM.test.ts packages/lexical-y/src/__tests__/unit/TableOnOpen.test.ts`

### Before

```
× builds when an earlier extension sets a state holding a table during register 14ms
× observes the table once a root element renders it 1ms
× observes the new table DOM when the root element is replaced 1ms
× registerTableSelectionObserver accepts an unrendered table 1ms
× opens a document that holds a table with YCollabExtension listed first 26ms
✓ opens a document that holds a table with TableExtension listed first 4ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 5 ⎯⎯⎯⎯⎯⎯⎯
Error: TableObserver: Expected to find TableElement in DOM for key 19
Error: TableObserver: Expected to find TableElement in DOM for key 49
Error: TableObserver: Expected to find TableElement in DOM for key 79
Error: TableObserver: Expected to find TableElement in DOM for key 91
Error: TableObserver: Expected to find TableElement in DOM for key 15
      Tests  5 failed | 1 passed (6)
```

### After

```
✓ |unit| packages/lexical-table/src/__tests__/unit/TableSelectionObserverBeforeDOM.test.ts (4 tests) 187ms
✓ |unit| packages/lexical-y/src/__tests__/unit/TableOnOpen.test.ts (2 tests) 111ms
      Tests  6 passed (6)
```

Also run with the fix:
- `@lexical/table` unit tests: 23 files, 229 tests passed. The new file is 4 of them.
- `@lexical/y` unit tests: 149 files passed and 2 skipped; 4,915 tests passed, 16 expected fail, 2 skipped.
- Full unit suite: 540 files passed and 3 skipped; 11,754 tests passed, 16 expected fail, 4 skipped.
- Playground `Tables.spec.mjs` e2e on Chromium: 86 passed, 1 skipped.
- `pnpm run ci-check` passes.
- Not run: Firefox and WebKit e2e, and browser-mode unit tests. The change only affects tables that have no DOM, which no browser test renders.
@vercel

vercel Bot commented Oct 28, 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 2:56pm UTC
lexical-playground Ready Ready Preview Oct 29, 2026 2:56pm UTC

Request Review

Comment on lines +70 to +72
onError: error => {
throw error;
},

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.

This is redundant, always the default for buildEditorFromExtensions

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Removed in c884a71.

@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label Oct 29, 2026
## Description

Review feedback on this PR: `buildEditorFromExtensions` already throws from
`onError` by default, so the test's own `onError: error => { throw error; }`
is redundant. Removed it. The `createEditor` test in
`TableSelectionObserverBeforeDOM.test.ts` keeps its `onError`, since
`createEditor`'s default only logs.

## Test plan

### After

```
$ npx vitest run --project unit packages/lexical-y/src/__tests__/unit/TableOnOpen.test.ts packages/lexical-table/src/__tests__/unit/TableSelectionObserverBeforeDOM.test.ts
 Test Files  2 passed (2)
      Tests  6 passed (6)
```
@pkg-pr-new

pkg-pr-new Bot commented Oct 29, 2026

Copy link
Copy Markdown
lexical

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

@lexical/a11y

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

@lexical/clipboard

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

@lexical/code

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

@lexical/code-core

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

@lexical/code-prism

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

@lexical/code-shiki

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

@lexical/compiler

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

@lexical/devtools-core

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

@lexical/dragon

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

@lexical/eslint-plugin

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

@lexical/extension

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

@lexical/fast-check

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

@lexical/file

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

@lexical/hashtag

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

@lexical/headless

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

@lexical/history

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

@lexical/html

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

@lexical/internal

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

@lexical/link

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

@lexical/list

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

@lexical/mark

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

@lexical/markdown

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

@lexical/mdast

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

@lexical/offset

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

@lexical/overflow

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

@lexical/plain-text

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

@lexical/react

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

@lexical/rich-text

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

@lexical/selection

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

@lexical/table

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

@lexical/tailwind

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

@lexical/text

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

@lexical/utils

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

@lexical/y

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

@lexical/yjs

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

commit: c884a71

@zurfyx
zurfyx added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit 8cc9a96 Oct 29, 2026
57 checks passed
etrepum pushed a commit to Om-singhaI/lexical that referenced this pull request Oct 29, 2026
…stream main into code block word wrap and line numbers

## Description

Brings the PR branch up to date with upstream main (8cc9a96, meta#9501). The merge applies without conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zurfyx added a commit that referenced this pull request Oct 29, 2026
…maVersion

## Description

Rebasing this PR onto `main` brings in two tests that landed after it was
written and bind a plain `Y.Doc` without a `schemaVersion`, which this PR's
`YCollabExtension` and `@lexical/y/headless` config no longer accept:

- `TableOnOpen.test.ts` (#9501): the document is made with `createYDoc` from
  the seeded state, and the seed and the extension get `schemaVersion: 1`.
- `MarkStateBlockType.test.ts` (#9503): both documents are made with
  `createYDoc`, and the extension gets `schemaVersion: 1`.

## Test plan

### Before

```
$ npx tsc --noEmit -p tsconfig.json
packages/lexical-y/src/__tests__/unit/MarkStateBlockType.test.ts(54,44): error TS2741: Property '[createdByCreateYDoc]' is missing in type 'Doc' but required in type 'YDoc'.
packages/lexical-y/src/__tests__/unit/TableOnOpen.test.ts(58,9): error TS2345: Argument of type '{ extensions: ... }' is not assignable to parameter of type 'HeadlessYConfig'.
packages/lexical-y/src/__tests__/unit/TableOnOpen.test.ts(62,55): error TS2741: Property '[createdByCreateYDoc]' is missing in type 'Doc' but required in type 'YDoc'.
```

### After

```
$ pnpm run ci-check
Found 0 errors
$ npx vitest run --project unit
 Test Files  563 passed | 3 skipped (566)
      Tests  14641 passed | 46 expected fail | 6 skipped (14693)
```

This branch was successfully deployed

2 active deployments
Preview – lexical — c884a715 Deployed Oct 29, 2026 by vercel[bot]
Preview – lexical-playground — c884a715 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. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants