Repository navigation
[lexical-table][lexical-y] Bug Fix: Don't throw when a table exists before its DOM - #9501
Merged
Merged
Conversation
…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.
zurfyx
requested review from
acywatson,
ivailop7 and
potatowagon
as code owners
October 28, 2026 22:27
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
etrepum
approved these changes
Oct 29, 2026
Comment on lines
+70
to
+72
| onError: error => { | ||
| throw error; | ||
| }, |
Collaborator
There was a problem hiding this comment.
This is redundant, always the default for buildEditorFromExtensions
## 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)
```
lexical
@lexical/a11y
@lexical/clipboard
@lexical/code
@lexical/code-core
@lexical/code-prism
@lexical/code-shiki
@lexical/compiler
@lexical/devtools-core
@lexical/dragon
@lexical/eslint-plugin
@lexical/extension
@lexical/fast-check
@lexical/file
@lexical/hashtag
@lexical/headless
@lexical/history
@lexical/html
@lexical/internal
@lexical/link
@lexical/list
@lexical/mark
@lexical/markdown
@lexical/mdast
@lexical/offset
@lexical/overflow
@lexical/plain-text
@lexical/react
@lexical/rich-text
@lexical/selection
@lexical/table
@lexical/tailwind
@lexical/text
@lexical/utils
@lexical/y
@lexical/yjs
commit: |
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
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
Building an editor throws
TableObserver: Expected to find TableElement in DOM for key Nwhen the editor state already holds a table beforeTableExtensionregisters and the editor has no root element yet.registerTableSelectionObserverregisters itsTableNodemutation listener withskipInitialization: false, so it receivescreatedfor every existing table at registration, and$getTableAndElementByKeyasserts that the table's DOM exists.Any extension listed before
TableExtensionthat sets content in itsregisterhits this. For example,YCollabExtensionreads aY.Docthat already holds a table (a remount, a doc loaded from storage, or a provider that synced first):So does any earlier extension that calls
editor.setEditorState(...)with a table, or a direct call toregisterTableSelectionObserveron an unrendered editor. ListingTableExtensionfirst 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 ascreated, 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:TableExtensionsets a state holding a table in itsregister;registerTableSelectionObserveron acreateEditoreditor 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.tsBefore
After
Also run with the fix:
@lexical/tableunit tests: 23 files, 229 tests passed. The new file is 4 of them.@lexical/yunit tests: 149 files passed and 2 skipped; 4,915 tests passed, 16 expected fail, 2 skipped.Tables.spec.mjse2e on Chromium: 86 passed, 1 skipped.pnpm run ci-checkpasses.