github facebook/lexical v0.50.0

4 hours ago

v0.50.0 is a monthly release with a large number of PRs to improve correctness, accessibility, and performance. It also ships HMR support, compiled keyboard shortcut dispatch, and screen reader announcements for headings and auto-links, and build-time tree-shaking annotations (in a new @lexical/compiler package). We have upgraded to pnpm 11, so if you're working directly with the monorepo you will need to upgrade as well.

Breaking Changes

  • lexicalLexicalNode.replace() no longer re-homes a named slot value. A slot value has no parent (its up-link is __slotHost), so replacing one through the tree API now fails with an actionable invariant instead of a generic parent error; use $setSlot on the owning host. $setBlocksType and the block converters skip slot values for the same reason (#8938, #8905, #8901)
  • @lexical/clipboard — The Lexical JSON clipboard payload (application/x-lexical-editor) now asks excludeFromCopy('clone') instead of 'html', matching the destination the rest of that payload already uses. Nodes that opt out of HTML export but expect to survive a clone (e.g. MarkNode) are copied again; custom excludeFromCopy implementations that keyed on 'html' for this channel should be updated (#8995)

Behavior Changes to Watch

These were merged as bug fixes rather than breaking changes, but they change observable output — check them if you snapshot HTML, assert on the DOM, or depend on the old editing behavior.

  • HTML export emits a terminating <br>. A trailing LineBreakNode in a block now exports an extra <br data-lexical-managed-linebreak="true">, mirroring what the reconciler already puts in the live DOM, so <p>a<br></p> survives an export/import round trip. Both importers drop the marker, but exported-HTML fixtures will differ (#8765)
  • Rich text mounts an aria-live region by default. HeadingAnnounceExtension (via RichTextExtension) and AutoLinkAnnounceExtension (via AutoLinkExtension) depend on AriaLiveRegionExtension, which appends a live-region <div> to document.body — or to the configured owner element. Opt out with configExtension(HeadingAnnounceExtension, {disabled: true}) (#8908, #9071)
  • Select-all delete and cut now remove the blocks. A whole-document range is widened to the top-level blocks, so the editor is left on a fresh empty paragraph instead of an emptied heading, quote or list that went on styling the next character typed — and Cmd+X now carries those blocks, so pasting restores them (#8884)
  • A formatted paragraph copies as a block. ParagraphNode.extractWithChild now keeps a fully-selected paragraph that carries alignment, indent or inline style in the payload as a block, instead of splicing its children in and dropping that formatting. Paragraphs with none of it serialize as before (#9059)
  • importJSON applies registered node replacements. JSON deserialization builds through $create, so a replacement's class is constructed and its importJSON runs; a deprecated replace without withKlass is still applied the old way (#9061, #9090)
  • Listener return types tightened. Only RootListener and EditableListener are typed to return a cleanup function; returning one from another listener is now a TypeScript error — it was never called (#8990)
  • Collab and Markdown output shifts. @lexical/yjs no longer syncs root textFormat/textStyle (#8940); Markdown import restructures nested mixed-type lists and stores fence metadata in a new mdCodeMeta node state (#8283)
  • New invariants. Replacing a slot value (#8938), an insertText anchor that is not a text node, an empty KeyboardShortcutMatch key, and a createBindingV2 getXmlText/getXmlElement that returns an un-integrated or misnamed type (#6483) now throw instead of failing later

Deprecations

  • lexicalCAN_UNDO_COMMAND and CAN_REDO_COMMAND are deprecated in favor of the canUndo / canRedo signals on HistoryExtension. A command only reports a change, so a listener registered after initialization can't read the current value; the signal always holds it (#8918)

New APIs & Features

  • @lexical/compiler — New package that injects /* @__PURE__ */ annotations and inlines the trivial side-effect-free factories (defineExtension, configExtension, safeCast, createCommand, createState, …) at build time, as a Rollup and Vite plugin. Hand-written annotations on those calls are no longer needed (#9086)
  • @lexical/extensionHMRExtension (with HMRConfig, HMROutput, HotContext) keeps an editor alive across hot module replacement. @lexical/react gains LexicalMenuOption, LexicalAutoEmbedPluginUtils, LexicalTypeaheadMenuPluginUtils and LexicalCollaborationContextUtils entry points that split values out of component modules so Fast Refresh works — the original exports are unchanged (#8959)
  • lexical / @lexical/extension — Compiled keyboard shortcut dispatch: compileKeyboardShortcuts, CONTROL_OR_ALT, CONTROL_OR_META, CONTROL_OR_OTHER_KEY, keyboardEventMaskForPlatform and the KeyboardShortcut* types in core, plus KeyboardShortcutsExtension and formatKeyboardShortcut for declaring named shortcuts and rendering them per platform (#8876)
  • @lexical/rich-textHeadingAnnounceExtension announces heading changes to screen readers, on by default in RichTextExtension; @lexical/link's AutoLinkAnnounceExtension does the same when typing turns text into a link, on by default with AutoLinkExtension (#8908, #9071)
  • @lexical/list — Opt-in WordListImportExtension reconstructs list structure from Microsoft Word HTML (MsoListParagraph classes and mso-list metadata) on paste (#9082)
  • @lexical/rich-text — New shouldHandlePasteAsFiles config (and exported defaultShouldHandlePasteAsFiles, plus an optional third argument to registerRichText) lets an app decide that pasted files win over the incidental text/html browsers attach when an image is copied. The default is unchanged: files are handled only when the clipboard carries no text content (#8902, #8699)
  • @lexical/yjsCreateBindingV2Options__EXPERIMENTAL allows a custom Yjs XmlText, so a Lexical document can live inside a larger shared type (#6483)
  • lexical — New $getSelectionSlotFrame (#9052) and $flushSyncAfterUpdate (#9067) exports; $isBlockFullySelected moved into core and is still re-exported from @lexical/utils (#9059); @lexical/table now exports the TableCellHeaderState type (#9038); JSDoc added across the core exports (#8895)
  • Performance — Bulk inserts and removals no longer walk the parent's children to compute an offset the selection never reads, which made those operations quadratic (#9108)

Notable Fixes

Named slots & the node tree

  • Line breaks inside a slot insert and delete correctly (#8899); a managed line break is restored in wrapped slot hosts (#9066); reading the parent style of a slot value no longer throws (#8950); a slot-bearing decorator host is deleted correctly at the selection boundary (#8905)
  • replace() keeps the parent's __size correct when handed a sibling (#8917), and splice no longer creates a sibling cycle when it re-inserts a node after the range (#8916)

Node state, copy/paste & serialization

  • HTML import/export and slot frames carry the whole node state, keeping the HTML and Lexical clipboard payloads in agreement (#9052); copy and insert keep the original node's state (#9059); list operations and DOM import/export keep the node's own state (#9050); inline node transforms keep the node and its DOM in step (#9060); code blocks keep the caret and their own attributes (#9056); blocks pasted after consecutive line breaks are preserved (#8890)
  • Nodes keep their own properties across clone, split and export: EditorState.clone() keeps _parsed (#8989), AutoLinkNode.insertNewAfter keeps its properties (#8996), a split heading keeps its format and style (#8926), split lists continue numbering from the item's value (#8913), and in the playground the Excalidraw drawing and size survive a clone (#8984), a closed collapsible no longer exports as open (#8994), mention nodes keep their display text (#8999), custom nodes keep the theme classes createDOM applied (#9049) and an unparseable data-lexical-datetime no longer breaks serialization (#9047)

Selection & editing

  • Selection and caret state are correct at block and shadow root boundaries (#9058); the block cursor shows before block decorators regardless of the preceding sibling (#8888); the select-all highlight stays visible next to boundary decorators (#8931); $extendCaretToRange's focus is a real, flippable position (#8930); Shift+Arrow extends adjacent node selections (#9067); clicking inside a selected node no longer deselects it (#8910)
  • insertNodes no longer leaves an empty paragraph after a block inserted at the end of a paragraph (#9099); insertText no longer throws when the caret is inside a segmented TextNode (#9104); Backspace works at a soft line-wrap boundary in Firefox (#9101); Enter on iOS no longer moves the caret past the acceptance boundary (#8941); iPadOS desktop-class user agents are detected (#8909); dropped files are forwarded when the drop point has no caret (#9076); clear-formatting resets the cached selection format (#8889)

Tables & lists

  • Table grid indices, cell state and boundary selection fixed (#9054); alignment covers every cell a table selection touches (#9043); inserting a column steps by the visited cell's colSpan (#8911); importDOM imports the dir attribute (#8914); toggleHeaderStyle stays inside TableCellHeaderStates (#9038); the trackTable MutationObserver is disconnected in removeListeners (#9074); text cursor placement is reliable when tapping cells on touch devices (#8827)
  • The stale depth theme class is removed when a list changes depth (#9077); rapid clicks on the same checklist checkbox are no longer throttled (#9102); focus stays in the editor when a press lands on a check list item's label (#9088)

Links, Markdown & HTML

  • TOGGLE_LINK_COMMAND's object payload no longer discards the configured attributes (#8985); $toggleLink applies the title attribute for a NodeSelection (#8993); LinkNode/AutoLinkNode getters read the latest state (#9080); middle-clicked links open in a new tab (#9078); the auto embed menu only opens when a bare link is pasted (#9091); the playground's validateUrl no longer accepts a sentence that merely contains a URL (#9097)
  • Markdown round trips keep fenced code and enclosing blocks intact (#9055); nested mixed-type lists import with the right structure (#8283); a URL containing parentheses imports correctly (#9093); no dangling closing tag when a selection slices a format to whitespace (#8961); no stray leading newline when a selection starts below the first block (#8960)
  • A trailing line break survives HTML export and re-import (#8765); exportDOM returning a DocumentFragment applies its after() replacement (#8997); parseSelector rejects an empty selector group (#9083); Prism language maps are read as own properties (#8981); a function-valued style patch is applied once to an empty element (#8912); objectKlassEquals no longer throws on undefined or prototype-less objects (#9079)

React, collaboration & history

  • Decorator portals are recreated when the editor root remounts (#8921); Placeholder uses the SSR-safe useLayoutEffect (#8906); the typeahead query updates during IME composition (#8942); plugins and hooks re-derive when their inputs change (#9051); focus is restored after dragging decorator blocks (#9064); multiple clicks are normalized (#8928)
  • Collab sync gaps between the editor state and the Yjs doc are closed, and bootstrapping the initial state no longer becomes an undo entry (#9057); only one client may bootstrap a nested editor's document, so an image caption or sticky note no longer gains a spurious leading paragraph — shouldBootstrap is documented as at-most-one-client (#9107); SharedHistoryExtension forwards the parent maxDepth (#9044) and shared history stacks record the state of the editor that actually changed, alongside fixes to overlays, hit targets and shared state at the editor boundary (#9053)

Docs & tooling

  • New Decorators concept guide (#8696), a React MathType example (#8692), a clarified vanilla quick start (#8801), documented DOMImportExtension rule evaluation order (#9089), SKIP_DOM_SELECTION_TAG behavior during the initial editor state (#8636) and $reverseDfs (#8962), plus an OpenGraph social preview for the website (#8935)
  • Upgraded to pnpm 11 (#8816), cached Playwright's apt packages in CI (#9085), extracted the internal-registry OIDC exchange into a reusable action (#9084), fixed a qs dependency vulnerability (#9072), quieted stderr noise in test-unit (#9098) and moved the playground onto ownerDocument/$getDocument() for shadow DOM safety (#8878)

What's Changed

  • v0.49.0 by @etrepum in #8893
  • [lexical-playground] Chore: Use ownerDocument and $getDocument() for shadow DOM safety by @mayrang in #8878
  • [lexical] Documentation Update: Add JSDoc comments to core package exports by @mayrang in #8895
  • [lexical][lexical-rich-text] Bug Fix: Show block cursor before block decorators regardless of preceding sibling by @ochevallier in #8888
  • [lexical] Bug Fix: Line breaks in a slot can't be inserted or deleted correctly by @sahiee-dev in #8899
  • [lexical-playground] Bug Fix: Reset the cached selection format on clear formatting by @Yjason-K in #8889
  • [lexical-selection] Bug Fix: Handle bare slot values in $setBlocksType by @mayrang in #8901
  • [lexical-react] Bug Fix: Use the SSR-safe useLayoutEffect in Placeholder by @arcs- in #8906
  • [lexical][lexical-selection][lexical-list] Bug Fix: Skip named-slot values in block converters, diagnose replace() on a slot value, and delete slot-bearing decorator hosts at the selection boundary by @etrepum in #8905
  • [lexical-extension] Bug Fix: Normalize multiple clicks by @levensta in #8928
  • [lexical-rich-text] Feat: add pasteFileContentCheck config to let files win over incidental HTML on paste by @Sa-Te in #8902
  • [lexical] Bug Fix: Detect iPadOS desktop-class user agents by @corinthionia in #8909
  • [lexical-rich-text] Bug Fix: Don't deselect a selected node when clicking inside it by @alvorithm in #8910
  • [lexical-table] Bug Fix: step by the visited cell's colSpan when walking left to insert a column by @LeSingh1 in #8911
  • [lexical-selection] Bug Fix: apply a function-valued style patch only once to an empty element by @LeSingh1 in #8912
  • [lexical-table] Bug Fix: Import the dir attribute in table importDOM by @LeSingh1 in #8914
  • [lexical-react] Bug Fix: Recreate decorator portals when the editor root remounts by @alphanull in #8921
  • [lexical] Bug Fix: keep parent __size correct when replace() is passed a sibling by @LeSingh1 in #8917
  • [lexical] Bug Fix: don't create a sibling cycle when splice re-inserts the node after the range by @LeSingh1 in #8916
  • [lexical-rich-text] Bug Fix: preserve heading format and style when splitting a heading by @LeSingh1 in #8926
  • [lexical-list] Bug Fix: continue split-list numbering from the item's value, not its index by @LeSingh1 in #8913
  • [lexical] Bug Fix: make $extendCaretToRange's focus a real, flippable position by @Sa-Te in #8930
  • [lexical] Deprecate CAN_UNDO_COMMAND and CAN_REDO_COMMAND in favour of HistoryExtension's signals by @LeSingh1 in #8918
  • [lexical-website] Add opengraph social preview image by @alex-js-ltd in #8935
  • [Breaking Change][lexical][lexical-utils][lexical-selection][lexical-website] Bug Fix: LexicalNode.replace no longer re-homes named slots by @etrepum in #8938
  • [lexical-playground] Chore: Stabilize the collab dangling-text e2e tests by @etrepum in #8933
  • [scripts] Bug Fix: wait out minimumReleaseAge boundary in example installs by @etrepum in #8934
  • [lexical-yjs] Bug Fix: Don't sync root textFormat/textStyle by @patrick-atticus in #8940
  • [lexical-react] Bug Fix: Update the typeahead query during IME composition by @hamo-o in #8942
  • [lexical-selection] Bug Fix: don't throw reading the parent style of a slot value by @LeSingh1 in #8950
  • [lexical-utils] Documentation Update: describe $reverseDfs instead of $reverseDfsIterator by @luantaraschi in #8962
  • [lexical][lexical-website] Documentation Update: SKIP_DOM_SELECTION_TAG is ignored during initial editor state by @spokodev in #8636
  • [lexical] Bug Fix: keep the select-all highlight visible next to boundary decorators by @etrepum in #8931
  • [lexical-markdown] Bug Fix: don't emit a dangling closing tag when a selection slices a format to whitespace by @luantaraschi in #8961
  • [lexical-code-prism] Bug Fix: read the language maps as own properties by @luantaraschi in #8981
  • [lexical] Bug Fix: Don't move the caret past the acceptance boundary for Enter on iOS by @SujinKim1127 in #8941
  • [lexical-playground] Bug Fix: keep the Excalidraw drawing and its size across a node clone by @LeSingh1 in #8984
  • [lexical-link] Bug Fix: TOGGLE_LINK_COMMAND object payload no longer discards the configured attributes by @LeSingh1 in #8985
  • [lexical] Bug Fix: EditorState.clone() keeps the _parsed flag by @LeSingh1 in #8989
  • [lexical] Bug Fix: only root and editable listeners may return a cleanup function by @LeSingh1 in #8990
  • [lexical-link] Bug Fix: $toggleLink applies the title attribute for a NodeSelection by @LeSingh1 in #8993
  • [lexical-playground] Bug Fix: $createMentionNode keeps the display text it is given by @LeSingh1 in #8999
  • [lexical-playground] Bug Fix: a closed collapsible container no longer exports as open by @LeSingh1 in #8994
  • [Breaking Change][lexical-clipboard] Bug Fix: the Lexical clipboard payload asks excludeFromCopy for the clone destination by @LeSingh1 in #8995
  • [lexical-link] Bug Fix: AutoLinkNode.insertNewAfter keeps the node's own properties by @LeSingh1 in #8996
  • [lexical-html] Bug Fix: apply the after() replacement when exportDOM returns a DocumentFragment by @LeSingh1 in #8997
  • [lexical-table] Bug Fix: align every cell a table selection covers, not just the naive rect by @LeSingh1 in #9043
  • [lexical-playground] Bug Fix: importing an unparseable data-lexical-datetime no longer breaks serialization by @LeSingh1 in #9047
  • [lexical-history] Bug Fix: SharedHistoryExtension forwards the parent maxDepth by @LeSingh1 in #9044
  • [lexical-table] Bug Fix: toggleHeaderStyle stays inside TableCellHeaderStates by @LeSingh1 in #9038
  • [lexical] Bug fix: apply nodes replacement for importJSON by @fantactuka in #9061
  • [lexical-text][lexical-mark][lexical-link][lexical-playground] Bug Fix: inline node transforms keep the node and its DOM in step by @LeSingh1 in #9060
  • [lexical-rich-text][lexical-a11y] Feature: Announce heading changes by default in rich text by @Electro-Jam in #8908
  • [lexical-react][lexical-playground] Bug Fix: restore focus after dragging decorator blocks by @luantaraschi in #9064
  • [lexical][lexical-extension][lexical-playground] Refactor: Compiled keyboard shortcut dispatch by @mayrang in #8876
  • [lexical] Bug Fix: restore managed line break in wrapped slot hosts by @luantaraschi in #9066
  • build(deps): bump mermaid from 11.15.0 to 11.16.1 by @dependabot[bot] in #8980
  • [lexical-rich-text] Bug Fix: extend adjacent node selections with Shift+Arrow by @luantaraschi in #9067
  • [lexical] Chore: Fix qs package dependency vulnerability by @vijayojha89 in #9072
  • [lexical-website] Documentation Update: Add Decorators concept guide by @WilliamK112 in #8696
  • [lexical-markdown] Bug Fix: don't prefix a newline when a selection starts below the first block by @luantaraschi in #8960
  • [lexical-table] Bug Fix: disconnect trackTable MutationObserver in removeListeners by @kaminski-dev in #9074
  • [lexical-utils] Bug Fix: objectKlassEquals throws on undefined and prototype-less objects by @nithin42 in #9079
  • [lexical-html] Bug Fix: reject an empty selector group in parseSelector by @luantaraschi in #9083
  • [lexical-list] Feature: opt-in Word list paste overlay by @smrifat1411 in #9082
  • [lexical-link] Bug Fix: read the latest state in LinkNode and AutoLinkNode getters by @nithin42 in #9080
  • [lexical-list] Bug Fix: remove the stale depth theme class when a list changes depth by @nithin42 in #9077
  • [ci] Extract the internal-registry OIDC exchange into a reusable auth action by @zurfyx in #9084
  • [lexical-link] Feature: Tell screen reader users when typing turns text into a link by @Electro-Jam in #9071
  • [ci] Chore: cache Playwright's apt packages and skip WebKit deps on Linux by @etrepum in #9085
  • docs: clarify vanilla quick start path by @alexespinoza28 in #8801
  • [lexical-html][lexical-rich-text][lexical-website] Docs: clarify DOMImportExtension rule evaluation order by @etrepum in #9089
  • [lexical] Feature: build the node with $create when importing JSON by @etrepum in #9090
  • [lexical-code-core][lexical-code-prism][lexical-code-shiki] Bug Fix: code blocks keep the caret and their own attributes by @LeSingh1 in #9056
  • [lexical-compiler][lexical-eslint-plugin-internal] Feature: inject /* @PURE */ annotations at build time by @etrepum in #9086
  • [lexical-playground] Bug Fix: validateUrl no longer accepts a sentence that merely contains a URL by @Om-singhaI in #9097
  • [lexical-markdown] Bug Fix: import a link whose URL contains parentheses by @luantaraschi in #9093
  • [tests] Chore: silence unnecessary stderr warnings in test-unit by @etrepum in #9098
  • [lexical] Bug Fix: Backspace at a soft line-wrap boundary in Firefox by @etrepum in #9101
  • [lexical] Bug Fix: insertNodes no longer leaves an empty paragraph after a block inserted at the end of a paragraph by @Om-singhaI in #9099
  • [lexical-list] Bug Fix: stop throttling rapid mouse clicks on the same checklist checkbox by @noeschmidt in #9102
  • [lexical-react] Bug Fix: Only open the auto embed menu when a bare link is pasted by @Om-singhaI in #9091
  • [lexical-history][lexical-utils][lexical-playground] Bug Fix: overlays, hit targets and shared state at the editor boundary by @LeSingh1 in #9053
  • [lexical-playground] Bug Fix: custom nodes keep the theme classes createDOM applied by @LeSingh1 in #9049
  • [lexical] Bug Fix: insertText no longer throws when the caret is inside a segmented TextNode by @Om-singhaI in #9104
  • [lexical][lexical-rich-text][lexical-utils] Bug Fix: copy and insert keep the original node's state by @LeSingh1 in #9059
  • [lexical-list] Bug Fix: list operations and DOM import/export keep the node's own state by @LeSingh1 in #9050
  • [lexical-rich-text] Bug Fix: forward dropped files when the drop point has no caret by @nithin42 in #9076
  • [lexical-list][lexical-playground] Bug Fix: keep the focus on the editor when a press lands in a check list item's label by @luantaraschi in #9088
  • [lexical-utils] Bug Fix: Prioritize file transfer over text/html when pasting browser-copied images by @VivekJariwala50 in #8699
  • [lexical][lexical-html][lexical-clipboard] Bug Fix: HTML import/export and slot frames carry the whole state by @LeSingh1 in #9052
  • [lexical-yjs][lexical-react] Bug Fix: collab sync gaps between the editor state and the yjs doc by @LeSingh1 in #9057
  • [lexical-link] Bug Fix: open middle-clicked links in a new tab by @nithin42 in #9078
  • [lexical-table][lexical-playground][lexical-rich-text] Bug Fix: table grid indices, cell state and boundary selection by @LeSingh1 in #9054
  • [lexical][lexical-table] Bug Fix: selection and caret state at block and shadow-root boundaries by @LeSingh1 in #9058
  • [lexical-react][lexical-playground] Bug Fix: plugins and hooks re-derive when their inputs change by @LeSingh1 in #9051
  • [lexical-markdown][lexical-mdast] Bug Fix: markdown round trip keeps fenced code and enclosing blocks intact by @LeSingh1 in #9055
  • Add React MathType example by @culpen90 in #8692
  • [lexical-table] Bug Fix: Fix unreliable text cursor placement when tapping table cells on touch devices by @Arman-Luthra in #8827
  • [lexical-yjs][lexical-react] Feature: Allow custom Yjs XmlText by @flaviouk in #6483
  • Upgrade to pnpm 11 by @zurfyx in #8816

New Contributors

Full Changelog: v0.49.0...v0.50.0

Don't miss a new lexical release

NewReleases is sending notifications on new releases.