Skip to content

[lexical-extension] Feature: Implement DecoratorTextExtension applying format to DecoratorTextNode#8114

Merged
etrepum merged 17 commits intofacebook:mainfrom
levensta:decorator-text-node
Feb 14, 2026
Merged

[lexical-extension] Feature: Implement DecoratorTextExtension applying format to DecoratorTextNode#8114
etrepum merged 17 commits intofacebook:mainfrom
levensta:decorator-text-node

Conversation

@levensta
Copy link
Copy Markdown
Contributor

@levensta levensta commented Feb 4, 2026

Description

Added a new extension DecoratorTextExtension which provides a new DecoratorTextNode class and the ability to apply text formatting to it. DecoratorTextNode — a new node extended from DecoratorNode. The implementation of the node is simple and is actually the same DecoratorNode, but with formatting methods exactly the same as those of TextNode.

This feature can be useful for decorator nodes that need to be rendered in the middle of text and behave like text when the user wants to convert a part in a some format. Support for specific formats and how they affect the visual component of the decorator is left up to the user. As an example, I made how this might work for DateTimeNode in lexical-playground

Test plan

Before

Screen.Recording.2026-02-04.at.05.42.50.mov

After

Screen.Recording.2026-02-04.at.05.40.45.mov

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview, Comment Feb 14, 2026 2:02pm
lexical-playground Ready Ready Preview, Comment Feb 14, 2026 2:02pm

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 Feb 4, 2026
Comment on lines +1 to +12
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/

/**
* LexicalDecoratorTextNode
*/
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was generated by the update-flowconfig command, but I didn't understand whether I needed to add the types manually. The build-types command returned an error. Perhaps it would be worth adding a brief explanation to CONTRIBUTING.md of how to correctly add new exportable things to packages 🤔

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.

pnpm run update-flowconfig only creates the files, it doesn't try and write the flow types for you.

pnpm run build-types is unrelated to flow, it only runs typescript. it does look like there's a problem with it worth fixing or filing an issue for, it tries to type check the build products in a way that doesn't make sense, so pnpm build-types will fail if you have run pnpm run build without pnpm run clean or equivalent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

it does look like there's a problem with it worth fixing or filing an issue for

I attempted to run the command on node v24.11.1. After switching to v22.22.0, pnpm run build-types executed successfully

Copy link
Copy Markdown
Collaborator

@etrepum etrepum left a comment

Choose a reason for hiding this comment

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

I haven't taken a very close look but this does seem like a lot of code for this feature, without any tests, and it appears to be failing existing tests. I suspect that it could be simpler.

@levensta
Copy link
Copy Markdown
Contributor Author

levensta commented Feb 6, 2026

I haven't taken a very close look but this does seem like a lot of code for this feature, without any tests, and it appears to be failing existing tests. I suspect that it could be simpler.

I simplified the implementation a little and fixed fundamental errors that caused the tests to fail

Comment on lines 1224 to +1228
await toggleBold(page);
await assertHTML(
page,
// After formatting the text, the selection will be reset from the decorator node,
// so it will retain its previous format when toggleBold is triggered again
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I guess this is related to the TextPointType update in selection.formatText https://github.com/facebook/lexical/blob/main/packages/lexical/src/LexicalSelection.ts#L1319-L1325

Screen.Recording.2026-02-07.at.02.03.48.mov

@levensta levensta changed the title [lexical-react] Feature: Implement DecoratorTextNode and TextWithFormattedContents component [lexical-extension] Feature: Implement DecoratorTextExtension applying format to DecoratorTextNode Feb 12, 2026
Copy link
Copy Markdown
Collaborator

@etrepum etrepum left a comment

Choose a reason for hiding this comment

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

Looks good, just some small cleanups to do!

Comment on lines +206 to +227
if ($isNodeSelection(selection)) {
selection.getNodes().forEach((node) => {
if ($isDecoratorTextNode(node)) {
node.toggleFormat(formatType);
}
});
} else if ($isRangeSelection(selection)) {
const nodes = selection.getNodes();

for (const node of nodes) {
if ($isDecoratorTextNode(node)) {
node.toggleFormat(formatType);
} else {
const decoratorText = $findMatchingParent(
node,
$isDecoratorTextNode,
);
if (decoratorText !== null) {
decoratorText.toggleFormat(formatType);
}
}
}
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.

$isDecoratorTextNode will never return true for an ElementNode so there's no need to check parents

Suggested change
if ($isNodeSelection(selection)) {
selection.getNodes().forEach((node) => {
if ($isDecoratorTextNode(node)) {
node.toggleFormat(formatType);
}
});
} else if ($isRangeSelection(selection)) {
const nodes = selection.getNodes();
for (const node of nodes) {
if ($isDecoratorTextNode(node)) {
node.toggleFormat(formatType);
} else {
const decoratorText = $findMatchingParent(
node,
$isDecoratorTextNode,
);
if (decoratorText !== null) {
decoratorText.toggleFormat(formatType);
}
}
}
if ($isNodeSelection(selection) || $isRangeSelection(selection)) {
for (const node of selection.getNodes()) {
if ($isDecoratorTextNode(node)) {
node.toggleFormat(formatType);
}
}
}

Comment on lines +205 to +206
.filter(([flag]) => format & Number(flag))
.map(([, className]) => className),
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.

These can array returning methods be fused into a flatMap, although it would probably make more sense to have formatClassMap just be an array of pairs in the first place rather than converting it to one on demand every time.

Suggested change
.filter(([flag]) => format & Number(flag))
.map(([, className]) => className),
.flatMap(([flag, className]) => format & Number(flag) ? [className] : [])

or

// This would be top-level in the module
const FORMAT_CLASSES = [
  [IS_BOLD, 'bold'],
  [IS_HIGHLIGHT, 'highlight'],
  [IS_ITALIC, 'italic'],
  [IS_STRIKETHROUGH, 'strikethrough'],
  [IS_UNDERLINE, 'underline'],
] as const;

// …
const classNames = ['dateTimePill'];
for (const [flag, className] of FORMAT_CLASSES) {
  if (format & flag) {
    classNames.push(className);
  }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

although it would probably make more sense to have formatClassMap just be an array of pairs in the first place rather than converting it to one on demand every time

Agreed, done

Copy link
Copy Markdown
Collaborator

@etrepum etrepum left a comment

Choose a reason for hiding this comment

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

Looks good!

@etrepum etrepum added this pull request to the merge queue Feb 14, 2026
Merged via the queue into facebook:main with commit d437a14 Feb 14, 2026
42 checks passed
rayterion pushed a commit to rayterion/lexical that referenced this pull request Feb 18, 2026
@etrepum etrepum mentioned this pull request Feb 25, 2026
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.

3 participants