v0.49.0
pascalorg/editorv0.49.0Jul 30, 2026by etrepum
AI Summary
This monthly release completes the migration of Lexical's built-in node classes to the `$config()` protocol, replacing boilerplate methods with runtime-synthesized behavior. It also introduces a sticky horizontal scrollbar for tables and includes various fixes for tables, Markdown, and selection.
Key Highlights
- Completion of the `$config()` protocol migration for Lexical's built-in node classes.
- Opt-in sticky horizontal scrollbar for overflowing tables.
- Follow-up fixes for synthesized behavior, preventing stack overflows in compiled builds.
- Improvements to Markdown, HTML import, code highlighting, and selection.
Breaking Changes
- Built-in node classes ported to the `$config()` protocol; static `importJSON()`, `importDOM()`, `clone()`, and `transform()` methods are removed.
- `LexicalCommand<T>`'s payload type is now invariant; unsafe `LexicalCommand<unknown>` usage must switch to the `AnyLexicalCommand` alias.
New Features
- Opt-in sticky horizontal scrollbar for tables (`hasStickyScrollbar: true`).
- Dynamic `<colgroup>` element handling for table column widths.
- Editor operation benchmark suite for tracking core editing performance.
Full Release Notes
v0.49.0 is a monthly release headlined by the completion of the `$config()` protocol migration for Lexical's built-in node classes ([#8640](https://github.com/facebook/lexical/pull/8640)), a breaking change that replaces per-node `getType()`/`clone()`/`importJSON()`/`importDOM()`/`transform()` boilerplate with runtime-synthesized behavior. It ships with a cluster of follow-up fixes hardening the synthesized `getType()` and `clone()` (including a stack overflow in compiled builds), an opt-in sticky horizontal scrollbar for overflowing tables, and a broad batch of fixes across tables, Markdown, HTML, code highlighting, and selection. ## Breaking Changes * [`lexical`](https://lexical.dev/docs/api/modules/lexical) — Built-in node classes are ported to the [`$config()`](https://lexical.dev/docs/concepts/nodes) protocol. The static `importJSON()`, `importDOM()`, `clone()`, and `transform()` methods are no longer present on ported nodes; use the higher-level equivalents instead (`LexicalEditor.parseEditorState`/`$generateNodesFromDOM`, the `$cloneWithProperties` helper, and extensions or `$transform` in `$config`). `getType()` is unchanged and safe to keep using, `__type` is now `readonly`, and node constructors require zero-argument defaults. Custom nodes may keep their static methods, but adopting `$config()` is recommended ([#8640](https://github.com/facebook/lexical/pull/8640)) * [`lexical`](https://lexical.dev/docs/api/modules/lexical) — `LexicalCommand<T>`'s payload type is now invariant to prevent unsafe structural compatibility between commands. Previously correct code (including explicit generics) needs no changes; only unsafe `LexicalCommand<unknown>` usage must switch to the newly exported `AnyLexicalCommand` alias. `dispatchCommand`'s payload argument is now optional when a command's payload type is `undefined` or `void`, and redundant explicit type parameters were dropped from the `registerCommand`/`registerNodeTransform` call sites ([#8877](https://github.com/facebook/lexical/pull/8877)) ## New APIs & Features * [`@lexical/table`](https://lexical.dev/docs/api/modules/lexical_table) — Opt-in sticky horizontal scrollbar for tables wider than their container, so the scrollbar stays anchored at the viewport bottom instead of only being reachable at the end of the table. Enable it with `hasStickyScrollbar: true` in the table config; style it via the new `tableStickyScrollbar` theme key ([#8790](https://github.com/facebook/lexical/pull/8790)) * [`@lexical/table`](https://lexical.dev/docs/api/modules/lexical_table) — The `<colgroup>` element is now omitted from the table DOM when column widths are undefined, producing cleaner markup and letting the browser auto-size columns; it is created or removed dynamically as widths change ([#8850](https://github.com/facebook/lexical/pull/8850)) * [`lexical`](https://lexical.dev/docs/api/modules/lexical) — Added an editor operation benchmark suite (paragraph split, bold formatting, range deletion, paste, select-all format) measured through full DOM reconciliation, to help contributors track core editing performance ([#8856](https://github.com/facebook/lexical/pull/8856)) ## Deprecations * [`@lexical/table`](https://lexical.dev/docs/api/modules/lexical_table) — `$createTableSelection` is deprecated in favor of `$createTableSelectionFrom`, which takes the table node plus anchor/focus cells and validates them, instead of returning a blank selection with placeholder `'root'` keys that must be overwritten ([#8855](https://github.com/facebook/lexical/pull/8855)) * [`@lexical/table`](https://lexical.dev/docs/api/modules/lexical_table) — `getShape()` is deprecated because it computes incorrect bounds for merged cells; use the now-exported `$computeTableCellRectBoundary` (with `$computeTableMap`) instead ([#8853](https://github.com/facebook/lexical/pull/8853)) ## Notable Fixes **`$config()` follow-ups (#8640)** * The synthesized `clone()` now applies `afterCloneFrom()` when called directly (e.g. `NodeClass.clone(node)`), fixing silent property loss across 25+ core nodes ([#8864](https://github.com/facebook/lexical/pull/8864)) * The synthesized `getType()` no longer returns the superclass type when inherited, fixing node-type collisions during editor registration ([#8867](https://github.com/facebook/lexical/pull/8867)) * The synthesized `getType()` no longer recurses infinitely when a bundler copies it onto a subclass as an own static, fixing stack overflows in compiled/minified builds ([#8869](https://github.com/facebook/lexical/pull/8869)) **Tables** * `DELETE_LINE_COMMAND` (Cmd/Ctrl-based delete-line shortcuts) now works inside table cells instead of being silently swallowed ([#8851](https://github.com/facebook/lexical/pull/8851)) * Table alignment now works when cells are selected in any direction, not just top-left→bottom-right ([#8883](https://github.com/facebook/lexical/pull/8883)) **Selection & editing** * Editor updates dispatched from a read-only context (e.g. inside `editor.read()`) now run in a fresh writable update instead of being silently dropped, with a dev warning ([#8863](https://github.com/facebook/lexical/pull/8863)) * Firefox now creates a selection and shows the block cursor when clicking in the gap between block decorators ([#8862](https://github.com/facebook/lexical/pull/8862)) * Pressing ArrowUp before a leading non-inline decorator no longer moves the selection out of the editor ([#8887](https://github.com/facebook/lexical/pull/8887)) **Markdown, HTML & code** * Typing a list marker at the start of a heading no longer converts the heading into a list ([#8879](https://github.com/facebook/lexical/pull/8879)) * HTML import now evaluates unrestricted CSS selector groups (e.g. `p, .foo`) against all elements instead of only tag-matched ones ([#8873](https://github.com/facebook/lexical/pull/8873)) * Concurrent async language/theme loads in the Shiki highlighter are deduplicated and merged into a single history entry, avoiding spurious undo states ([#8854](https://github.com/facebook/lexical/pull/8854)) * `QuoteNode.updateDOM` now accepts the `EditorConfig` argument the reconciler passes to every other node, for API consistency ([#8882](https://github.com/facebook/lexical/pull/8882)) **Playground & website** * Playground scroll padding now accounts for the sticky toolbar so selections scrolled into view from above aren't hidden behind it ([#8849](https://github.com/facebook/lexical/pull/8849)) * Fixed a homepage crash caused by a minified dev build, and isolated each embedded example in its own error boundary so one failure no longer takes down the page ([#8861](https://github.com/facebook/lexical/pull/8861)) ## What's Changed * \[lexical\] Bug Fix: Scope bench vitest projects to exclude regular test files by [@mayrang](https://github.com/mayrang) in [#8852](https://github.com/facebook/lexical/pull/8852) * \[lexical-table\] Bug Fix: Enable DELETE_LINE_COMMAND in table cells by [@mayrang](https://github.com/mayrang) in [#8851](https://github.com/facebook/lexical/pull/8851) * \[lexical-table\] Chore: Deprecate $createTableSelection in favor of $createTableSelectionFrom by [@mayrang](https://github.com/mayrang) in [#8855](https://github.com/facebook/lexical/pull/8855) * \[Breaking Changes\]\[lexical\] Refactor: Port node classes to the $config() protocol by [@etrepum](https://github.com/etrepum) in [#8640](https://github.com/facebook/lexical/pull/8640) * \[lexical-playground\] Bug Fix: account for sticky toolbar in scroll padding by [@etrepum](https://github.com/etrepum) in [#8849](https://github.com/facebook/lexical/pull/8849) * \[lexical-list\] Chore: Rename isNestedListNode to $isNestedListNode by [@etrepum](https://github.com/etrepum) in [#8843](https://github.com/facebook/lexical/pull/8843) * \[lexical-table\]\[lexical-playground\] Chore: Deprecate getShape() and migrate playground by [@mayrang](https://github.com/mayrang) in [#8853](https://github.com/facebook/lexical/pull/8853) * \[lexical-table\] Chore: Remove dead code in table command handlers by [@mayrang](https://github.com/mayrang) in [#8857](https://github.com/facebook/lexical/pull/8857) * \[lexical-code-shiki\] Bug Fix: Deduplicate async loads and merge into history by [@Alxs39](https://github.com/Alxs39) in [#8854](https://github.com/facebook/lexical/pull/8854) * \[lexical-table\]\[lexical\] Feature: Sticky scrollbar for overflowing tables by [@mayrang](https://github.com/mayrang) in [#8790](https://github.com/facebook/lexical/pull/8790) * fix: $config() auto-synthesized clone() loses properties when called directly (#8640) by [@potatowagon](https://github.com/potatowagon) in [#8864](https://github.com/facebook/lexical/pull/8864) * fix: $config() synthesized getType() inherited by subclasses causes node-type collision (#8640) by [@potatowagon](https://github.com/potatowagon) in [#8867](https://github.com/facebook/lexical/pull/8867) * \[lexical\] Chore: Migrate LexicalSelection tests to buildEditorFromExtensions by [@mayrang](https://github.com/mayrang) in [#8865](https://github.com/facebook/lexical/pull/8865) * \[lexical\] Feature: Editor operation benchmarks by [@mayrang](https://github.com/mayrang) in [#8856](https://github.com/facebook/lexical/pull/8856) * \[lexical\] Bug Fix: run editor updates dispatched from a read-only context in a fresh writable update by [@potatowagon](https://github.com/potatowagon) in [#8863](https://github.com/facebook/lexical/pull/8863) * \[lexical\]\[lexical-website\] Bug Fix: Homepage crash from optimized dev build by [@etrepum](https://github.com/etrepum) in [#8861](https://github.com/facebook/lexical/pull/8861) * fix: $config() synthesized getType() recurses infinitely when inherited as own static (#8867 follow-up) by [@potatowagon](https://github.com/potatowagon) in [#8869](https://github.com/facebook/lexical/pull/8869) * \[lexical\] Bug Fix: Create selection when clicking between block decorators on Firefox by [@mayrang](https://github.com/mayrang) in [#8862](https://github.com/facebook/lexical/pull/8862) * \[lexical-table\] Feature: Skip colgroup element in table DOM with undefined col widths by [@zacharyg-qualtrics](https://github.com/zacharyg-qualtrics) in [#8850](https://github.com/facebook/lexical/pull/8850) * \[lexical-list\] Chore: Remove redundant decorator-adjacent backspace handler by [@mayrang](https://github.com/mayrang) in [#8871](https://github.com/facebook/lexical/pull/8871) * \[lexical\] Refactor: Simplify RangeSelection.insertText via removeText decomposition by [@mayrang](https://github.com/mayrang) in [#8870](https://github.com/facebook/lexical/pull/8870) * \[lexical\] Performance: Skip redundant selection restoration in $removeTextFromCaretRange by [@mayrang](https://github.com/mayrang) in [#8872](https://github.com/facebook/lexical/pull/8872) * \[lexical-playground\] Bug Fix: Deterministic history coalescing for flaky webkit undo tests by [@etrepum](https://github.com/etrepum) in [#8874](https://github.com/facebook/lexical/pull/8874) * \[lexical-html\] Bug Fix: Dispatch unrestricted CSS selector groups by [@winklemad](https://github.com/winklemad) in [#8873](https://github.com/facebook/lexical/pull/8873) * \[Breaking Change\]\[lexical\] Chore: Remove redundant registerCommand/registerNodeTransform generics by [@mayrang](https://github.com/mayrang) in [#8877](https://github.com/facebook/lexical/pull/8877) * \[lexical-table\] Bug Fix: Table alignment fails when selecting in non-TL->BR direction by [@sahiee-dev](https://github.com/sahiee-dev) in [#8883](https://github.com/facebook/lexical/pull/8883) * \[lexical-rich-text\] Bug Fix: Forward editorConfig to QuoteNode.updateDOM by [@werterhalimi](https://github.com/werterhalimi) in [#8882](https://github.com/facebook/lexical/pull/8882) * \[lexical-markdown\] Bug Fix: Preserve headings when typing list shortcuts by [@StevenPDang](https://github.com/StevenPDang) in [#8879](https://github.com/facebook/lexical/pull/8879) * \[lexical-rich-text\] Bug Fix: Keep block cursor in editor when ArrowUp at start of root (#8886) by [@etrepum](https://github.com/etrepum) in [#8887](https://github.com/facebook/lexical/pull/8887) ## New Contributors * [@Alxs39](https://github.com/Alxs39) made their first contribution in [#8854](https://github.com/facebook/lexical/pull/8854) * [@zacharyg-qualtrics](https://github.com/zacharyg-qualtrics) made their first contribution in [#8850](https://github.com/facebook/lexical/pull/8850) * [@winklemad](https://github.com/winklemad) made their first contribution in [#8873](https://github.com/facebook/lexical/pull/8873) * [@werterhalimi](https://github.com/werterhalimi) made their first contribution in [#8882](https://github.com/facebook/lexical/pull/8882) * [@StevenPDang](https://github.com/StevenPDang) made their first contribution in [#8879](https://github.com/facebook/lexical/pull/8879) **Full Changelog**: [v0.48.0...v0.49.0](https://github.com/facebook/lexical/compare/v0.48.0...v0.49.0)