Skip to content

[lexical][lexical-rich-text] Bug Fix: Fix formatText toggle direction and add SET_TEXT_FORMAT_COMMAND#8807

Open
mayrang wants to merge 4 commits into
facebook:mainfrom
mayrang:fix/6935-format-text-toggle
Open

[lexical][lexical-rich-text] Bug Fix: Fix formatText toggle direction and add SET_TEXT_FORMAT_COMMAND#8807
mayrang wants to merge 4 commits into
facebook:mainfrom
mayrang:fix/6935-format-text-toggle

Conversation

@mayrang

@mayrang mayrang commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Description

$formatText used the first TextNode's format as the toggle reference when alignWithFormat was null. This disagreed with hasFormat() and the toolbar, which use selection.format — the AND-intersection of all selected TextNodes' formats. The mismatch caused the bold button to remove bold from a mixed-format selection even though the toolbar showed bold as inactive.

This PR changes $formatText to use selection.format as the toggle reference for RangeSelection when no explicit alignment is given, so the toggle direction now matches what the toolbar displays.

It also adds SET_TEXT_FORMAT_COMMAND and $setTextFormat() for explicitly setting or unsetting individual text formats without toggling. SET_TEXT_FORMAT_COMMAND accepts a Partial<Record<TextFormatType, boolean>> payload, and $setTextFormat() iterates the map and delegates to $formatText with an explicit alignWithFormat per format type.

Other changes:

  • The NodeSelection path in $formatText now passes alignWithFormat through to getFormatFlags() instead of hardcoding null.
  • hasFormat JSDoc corrected from "any" to "all" text nodes.

Closes #6935

Test plan

  • pnpm vitest run --project unit — 211 files, 3684 tests pass.
  • pnpm playwright test --project=chromium — 768 pass, 19 skipped.
    • TextFormatting.spec.mjs expectations updated to reflect the corrected toggle direction.
  • DecoratorTextExtension.test.ts — set selection.format manually (jsdom doesn't run onSelectionChange).
  • 6 new unit tests for $formatText toggle direction and $setTextFormat.
  • Manual playground (Chrome, Firefox, Safari):
    • Type "Hello world", select "world" (mixed bold), click Bold → bold added to all.
    • All-bold selection → click Bold → bold removed from all.
    • Explicit $setTextFormat(selection, {bold: true, italic: false}) sets/unsets independently.
  • Flow / tsc / prettier / eslint clean.

@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview Jul 7, 2026 3:07pm
lexical-playground Ready Ready Preview Jul 7, 2026 3:07pm

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 Jul 7, 2026
@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label Jul 7, 2026
mayrang added 3 commits July 7, 2026 21:51
… for mixed formatting selections

Use selection.format (AND intersection) as toggle reference instead of
first node when alignWithFormat is null. Add SET_TEXT_FORMAT_COMMAND and
$setTextFormat for explicit format set/unset.

Closes facebook#6935
…ion tests for jsdom

onSelectionChange does not run in jsdom, so selection.format defaults
to 0.  The $formatText fix (using selection.format as toggle reference)
needs the AND-intersection to be set manually in tests where all
TextNodes share a format.
… selection.format toggle

Two e2e tests encoded the old buggy toggle direction where the first
TextNode determined whether format was added or removed.  With the fix
(using selection.format AND-intersection as reference), mixed-format
selections now toggle ON when the toolbar shows the format as OFF.
Comment thread packages/lexical-rich-text/src/index.ts Outdated
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand<Partial<Record<TextFormatType, boolean>>>(

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.

The generic here should be inferred from the phantom type in the command

Suggested change
editor.registerCommand<Partial<Record<TextFormatType, boolean>>>(
editor.registerCommand(

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.

Done in 60ffebd.

Comment on lines +2200 to +2205
for (const type of Object.keys(formats) as TextFormatType[]) {
const value = formats[type];
if (value !== undefined) {
$formatText(selection, type, value ? TEXT_TYPE_TO_FORMAT[type] : 0);
}
}

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 could use entries instead:

Suggested change
for (const type of Object.keys(formats) as TextFormatType[]) {
const value = formats[type];
if (value !== undefined) {
$formatText(selection, type, value ? TEXT_TYPE_TO_FORMAT[type] : 0);
}
}
for (const [type, value] of Object.entries(formats) as [TextFormatType, undefined | boolean][]) {
if (typeof value === 'boolean') {
$formatText(selection, type, value ? TEXT_TYPE_TO_FORMAT[type] : 0);
}
}

Wouldn't it be better to compose a format/alignment bitmask and call something equivalent to $formatText once though?

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.

Done — went with the bitmask approach in 60ffebd.

… $setTextFormat, infer registerCommand generic

Rewrite $setTextFormat to compose a setBits/clearBits bitmask upfront and
apply it in a single node traversal, instead of calling $formatText N times.
Remove the unnecessary explicit generic from SET_TEXT_FORMAT_COMMAND registration.
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.

Bug: hasFormat in RangeSelection is not consistent with dispatchCommand

2 participants