Skip to content

[lexical-playground] Bug Fix: clear block alignment and indent with a collapsed selection but not a partial one#8666

Merged
etrepum merged 4 commits into
facebook:mainfrom
achaljhawar:fix/clear-formatting-block-alignment
Jun 11, 2026
Merged

[lexical-playground] Bug Fix: clear block alignment and indent with a collapsed selection but not a partial one#8666
etrepum merged 4 commits into
facebook:mainfrom
achaljhawar:fix/clear-formatting-block-alignment

Conversation

@achaljhawar

@achaljhawar achaljhawar commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Description

Clear formatting did nothing on a collapsed selection, so alignment set without selecting text could never be cleared (#7383). It also wiped a paragraph's alignment when you only selected one word in it.

Now a collapsed selection resets the caret block's alignment/indent, and a ranged selection only resets them when the whole block is selected. This follows the review feedback on #8237: the fully-selected check uses $isBlockFullySelected from @lexical/utils (#8532) against the selection after selection.extract(), and the empty aligned paragraph case works too.

The set of fully selected blocks is computed before any nodes are mutated, because the mutations in the loop (such as replacing a HeadingNode with a ParagraphNode) detach nodes that the selection's carets may refer to, which made $comparePointCaretNext throw an invariant violation (e.g. with a selection that starts at an empty heading).

Closes #7383

Test plan

Unit tests for the cases asked for in the #8237 review (full, partial, collapsed, empty block, cross-paragraph), plus backward selections and a regression test for the empty heading invariant violation, and an e2e test. Existing ClearFormatting e2e tests pass.

Before

Lexical.Playground.15.mp4

After

Lexical.Playground.14.mp4

@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 Jun 10, 2026
@vercel

vercel Bot commented Jun 10, 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, Comment Jun 11, 2026 8:29pm
lexical-playground Ready Ready Preview, Comment Jun 11, 2026 8:29pm

Request Review

@potatowagon potatowagon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed by Navi (Tater Thoughts Bobblehead) on behalf of @potatowagon.

LGTM — Clear block alignment/indent with collapsed selection (fixes #7383).

What I verified:

  • Uses $isBlockFullySelected() with CaretRange to only clear block format when the entire block is selected; collapsed selection resets just the caret block's format/indent.
  • Scoped to the lexical-playground toolbar utility only — no library/runtime impact.
  • 5 unit tests + 1 e2e test covering the relevant cases.
  • CI all green (unit 22.x + 24.x, browser, integrity, e2e canary chromium, CLA, Vercel).

No concerns. Safe to land.

@levensta

Copy link
Copy Markdown
Contributor

I also made $isBlockFullySelected function in my PR #8532, although my implementation depends on RangeSelection rather than CaretRange

Perhaps it makes sense for us to sync and leave someone else's implementation in lexical/utils. I'd like to hear your opinion and that of the maintainer.

@achaljhawar

Copy link
Copy Markdown
Contributor Author

I also made $isBlockFullySelected function in my PR #8532, although my implementation depends on RangeSelection rather than CaretRange

Perhaps it makes sense for us to sync and leave someone else's implementation in lexical/utils. I'd like to hear your opinion and that of the maintainer.

Sounds good, happy to consolidate. I'd lean towards the caret-based one since the caret API handles element points, decorators, and reversed selections for free, and that's where new selection logic seems to be headed anyway. It could accept a RangeSelection and convert with $caretRangeFromSelection so your call sites still work.

Mine's just a playground helper for now though, and moving it to @lexical/utils feels out of scope for this fix. I'll defer to @etrepum on where it should live.

etrepum pushed a commit to etrepum/lexical that referenced this pull request Jun 11, 2026
…kExtension review findings, add tests and docs

Addresses the issues identified in review of the SelectBlockExtension PR:

- Promote $isBlockFullySelected to @lexical/utils using the caret-based
  containment implementation from facebook#8666 (simplified with
  $getCaretRangeInDirection), which handles ancestor element points and
  empty blocks consistently; it accepts a RangeSelection or CaretRange so
  the clearFormatting use case in facebook#8666 can adopt it as well
- Bail out of the SELECT_ALL_COMMAND listener for commands bubbled from
  nested editors when cascadeSelection is off (the default), so a stale
  parent selection can no longer hijack select all from a nested editor
- Expand a selection that already spans multiple blocks directly to the
  whole document instead of shrinking it to the anchor's block
- Store a clone() snapshot of the selectAll selection; the live selection
  object can be mutated in place by later updates, aliasing
  prevSelectionAll and swallowing the next select all
- Select all even when the selection is anchored directly on the root
  (previously a no-op when prevSelectionAll was set)
- Guard against an empty NodeSelection instead of throwing
- Keep the PreventSelectAllExtension dependency's disabled state in sync
  with SelectBlockExtension so disabling the feature disables all of it
- Import PreventSelectAllExtension directly instead of via the package
  barrel (circular import), and drop redundant $setSelection() wrappers
  around ElementNode.select()
- Rename extensions to the majority '@lexical/extension/*' name convention
- Restore the PollComponent input caret workaround from facebook#3491 that was
  removed without explanation or test coverage

Tests:
- New @lexical/utils unit suite for $isBlockFullySelected covering
  containment semantics, ancestor element points, backward selections,
  CaretRange input, trailing decorators, empty and nested blocks
- New unit suite for SelectBlockExtension: block/document select all
  progression, multi-block expansion, root-anchored selections, node
  selections, disabled config and PreventSelectAll coupling, nested
  editor handling with cascadeSelection on and off (including an empty
  nested editor), and the PreventSelectAll input/textarea behavior
- New e2e test for multi-block selection expansion

Docs:
- Add SelectBlockExtension and PreventSelectAllExtension to
  included-extensions.md and fill in the other missing entries
  (@lexical/clipboard, @lexical/html, and the rest of @lexical/extension),
  omitting deprecated extensions

https://claude.ai/code/session_013LyjzFTiTDhbaNe3Tadiqo
etrepum pushed a commit to etrepum/lexical that referenced this pull request Jun 11, 2026
…kExtension review findings, add tests and docs

Addresses the issues identified in review of the SelectBlockExtension PR:

- Promote $isBlockFullySelected to @lexical/utils using the caret-based
  containment implementation from facebook#8666 (simplified with
  $getCaretRangeInDirection), which handles ancestor element points and
  empty blocks consistently; it accepts a RangeSelection or CaretRange so
  the clearFormatting use case in facebook#8666 can adopt it as well
- Remove the prevSelectionAll snapshot in favor of a stateless
  $isBlockFullySelected($getRoot(), selection) check. The stored
  selection could go stale and swallow select all (e.g. a collaborator
  appending a paragraph while the document was fully selected), and the
  live selection object could be mutated in place by later updates,
  aliasing the snapshot
- Bail out of the SELECT_ALL_COMMAND listener for commands bubbled from
  nested editors when cascadeSelection is off (the default), so a stale
  parent selection can no longer hijack select all from a nested editor
- Expand a selection that already spans multiple blocks directly to the
  whole document instead of shrinking it to the anchor's block
- Select all even when the selection is anchored directly on the root
- Guard against an empty NodeSelection instead of throwing
- Keep the PreventSelectAllExtension dependency's disabled state in sync
  with SelectBlockExtension so disabling the feature disables all of it
- Import PreventSelectAllExtension directly instead of via the package
  barrel (circular import), and drop redundant $setSelection() wrappers
  around ElementNode.select()
- Rename extensions to the majority '@lexical/extension/*' name convention
- Restore the PollComponent input caret workaround from facebook#3491 that was
  removed without explanation or test coverage

Tests:
- New @lexical/utils unit suite for $isBlockFullySelected covering
  containment semantics, ancestor element points, backward selections,
  CaretRange input, trailing decorators, empty and nested blocks
- New unit suite for SelectBlockExtension: block/document select all
  progression, multi-block expansion, root-anchored selections, select
  all after concurrent document changes, node selections, disabled config
  and PreventSelectAll coupling, nested editor handling with
  cascadeSelection on and off (including an empty nested editor), and the
  PreventSelectAll input/textarea behavior
- New e2e test for multi-block selection expansion

Docs:
- Add SelectBlockExtension and PreventSelectAllExtension to
  included-extensions.md and fill in the other missing entries
  (@lexical/clipboard, @lexical/html, and the rest of @lexical/extension),
  omitting deprecated extensions

https://claude.ai/code/session_013LyjzFTiTDhbaNe3Tadiqo
etrepum pushed a commit to etrepum/lexical that referenced this pull request Jun 11, 2026
…kExtension review findings, add tests and docs

Addresses the issues identified in review of the SelectBlockExtension PR:

- Promote $isBlockFullySelected to @lexical/utils using the caret-based
  containment implementation from facebook#8666 (simplified with
  $getCaretRangeInDirection), which handles ancestor element points and
  empty blocks consistently; it accepts a RangeSelection or CaretRange so
  the clearFormatting use case in facebook#8666 can adopt it as well
- Remove the prevSelectionAll snapshot in favor of a stateless
  $isBlockFullySelected($getRoot(), selection) check. The stored
  selection could go stale and swallow select all (e.g. a collaborator
  appending a paragraph while the document was fully selected), and the
  live selection object could be mutated in place by later updates,
  aliasing the snapshot
- Bail out of the SELECT_ALL_COMMAND listener for commands bubbled from
  nested editors when cascadeSelection is off (the default), so a stale
  parent selection can no longer hijack select all from a nested editor
- Expand a selection that already spans multiple blocks directly to the
  whole document instead of shrinking it to the anchor's block
- Select all even when the selection is anchored directly on the root
- Guard against an empty NodeSelection instead of throwing
- Keep the PreventSelectAllExtension dependency's disabled state in sync
  with SelectBlockExtension so disabling the feature disables all of it
- Import PreventSelectAllExtension directly instead of via the package
  barrel (circular import), and drop redundant $setSelection() wrappers
  around ElementNode.select()
- Rename extensions to the majority '@lexical/extension/*' name convention
- Restore the PollComponent input caret workaround from facebook#3491 that was
  removed without explanation or test coverage

Tests:
- New @lexical/utils unit suite for $isBlockFullySelected covering
  containment semantics, ancestor element points, backward selections,
  CaretRange input, trailing decorators, empty and nested blocks
- New unit suite for SelectBlockExtension: block/document select all
  progression (including single-block documents), multi-block expansion,
  root-anchored selections, select all after concurrent document changes,
  preservation of manually created full selections, node selections,
  disabled config and PreventSelectAll coupling, nested editor handling
  with cascadeSelection on and off (including an empty nested editor),
  and the PreventSelectAll input/textarea behavior
- New e2e test for multi-block selection expansion

Docs:
- Add SelectBlockExtension and PreventSelectAllExtension to
  included-extensions.md and fill in the other missing entries
  (@lexical/clipboard, @lexical/html, and the rest of @lexical/extension),
  omitting deprecated extensions

https://claude.ai/code/session_013LyjzFTiTDhbaNe3Tadiqo
etrepum pushed a commit to levensta/lexical that referenced this pull request Jun 11, 2026
…kExtension review findings, add tests and docs

Addresses the issues identified in review of the SelectBlockExtension PR:

- Promote $isBlockFullySelected to @lexical/utils using the caret-based
  containment implementation from facebook#8666 (simplified with
  $getCaretRangeInDirection), which handles ancestor element points and
  empty blocks consistently; it accepts a RangeSelection or CaretRange so
  the clearFormatting use case in facebook#8666 can adopt it as well
- Remove the prevSelectionAll snapshot in favor of a stateless
  $isBlockFullySelected($getRoot(), selection) check. The stored
  selection could go stale and swallow select all (e.g. a collaborator
  appending a paragraph while the document was fully selected), and the
  live selection object could be mutated in place by later updates,
  aliasing the snapshot
- Bail out of the SELECT_ALL_COMMAND listener for commands bubbled from
  nested editors when cascadeSelection is off (the default), so a stale
  parent selection can no longer hijack select all from a nested editor
- Expand a selection that already spans multiple blocks directly to the
  whole document instead of shrinking it to the anchor's block
- Select all even when the selection is anchored directly on the root
- Guard against an empty NodeSelection instead of throwing
- Keep the PreventSelectAllExtension dependency's disabled state in sync
  with SelectBlockExtension so disabling the feature disables all of it
- Import PreventSelectAllExtension directly instead of via the package
  barrel (circular import), and drop redundant $setSelection() wrappers
  around ElementNode.select()
- Rename extensions to the majority '@lexical/extension/*' name convention
- Restore the PollComponent input caret workaround from facebook#3491 that was
  removed without explanation or test coverage

Tests:
- New @lexical/utils unit suite for $isBlockFullySelected covering
  containment semantics, ancestor element points, backward selections,
  CaretRange input, trailing decorators, empty and nested blocks
- New unit suite for SelectBlockExtension: block/document select all
  progression (including single-block documents), multi-block expansion,
  root-anchored selections, select all after concurrent document changes,
  preservation of manually created full selections, node selections,
  disabled config and PreventSelectAll coupling, nested editor handling
  with cascadeSelection on and off (including an empty nested editor),
  and the PreventSelectAll input/textarea behavior
- New e2e test for multi-block selection expansion

Docs:
- Add SelectBlockExtension and PreventSelectAllExtension to
  included-extensions.md and fill in the other missing entries
  (@lexical/clipboard, @lexical/html, and the rest of @lexical/extension),
  omitting deprecated extensions

https://claude.ai/code/session_013LyjzFTiTDhbaNe3Tadiqo
etrepum pushed a commit to etrepum/lexical that referenced this pull request Jun 11, 2026
…kExtension review findings, add tests and docs

Addresses the issues identified in review of the SelectBlockExtension PR:

- Promote $isBlockFullySelected to @lexical/utils using the caret-based
  containment implementation from facebook#8666 (simplified with
  $getCaretRangeInDirection), which handles ancestor element points and
  empty blocks consistently; it accepts a RangeSelection or CaretRange so
  the clearFormatting use case in facebook#8666 can adopt it as well
- Remove the prevSelectionAll snapshot in favor of a stateless
  $isBlockFullySelected($getRoot(), selection) check. The stored
  selection could go stale and swallow select all (e.g. a collaborator
  appending a paragraph while the document was fully selected), and the
  live selection object could be mutated in place by later updates,
  aliasing the snapshot
- Bail out of the SELECT_ALL_COMMAND listener for commands bubbled from
  nested editors when cascadeSelection is off (the default), so a stale
  parent selection can no longer hijack select all from a nested editor
- Expand a selection that already spans multiple blocks directly to the
  whole document instead of shrinking it to the anchor's block
- Select all even when the selection is anchored directly on the root
- Guard against an empty NodeSelection instead of throwing
- Keep the PreventSelectAllExtension dependency's disabled state in sync
  with SelectBlockExtension so disabling the feature disables all of it
- Import PreventSelectAllExtension directly instead of via the package
  barrel (circular import), and drop redundant $setSelection() wrappers
  around ElementNode.select()
- Rename extensions to the majority '@lexical/extension/*' name convention
- Restore the PollComponent input caret workaround from facebook#3491 that was
  removed without explanation or test coverage

Tests:
- New @lexical/utils unit suite for $isBlockFullySelected covering
  containment semantics, ancestor element points, backward selections,
  CaretRange input, trailing decorators, empty and nested blocks
- New unit suite for SelectBlockExtension: block/document select all
  progression (including single-block documents), multi-block expansion,
  root-anchored selections, select all after concurrent document changes,
  preservation of manually created full selections, node selections,
  disabled config and PreventSelectAll coupling, nested editor handling
  with cascadeSelection on and off (including an empty nested editor),
  and the PreventSelectAll input/textarea behavior
- New e2e test for multi-block selection expansion

Docs:
- Add SelectBlockExtension and PreventSelectAllExtension to
  included-extensions.md and fill in the other missing entries
  (@lexical/clipboard, @lexical/html, and the rest of @lexical/extension),
  omitting deprecated extensions

https://claude.ai/code/session_013LyjzFTiTDhbaNe3Tadiqo
…/utils in clearFormatting

Replace the hand-rolled $isBlockFullySelected with the equivalent
$isBlockFullySelected from @lexical/utils (added in facebook#8532), and compute
the set of fully selected blocks before making any changes. The
mutations in the extracted nodes loop (such as replacing a HeadingNode
with a ParagraphNode) detach nodes that the selection's carets may
refer to, causing $comparePointCaretNext to throw an invariant
violation (e.g. when the selection starts at an empty heading).

https://claude.ai/code/session_01FbyreCoAxDey1yx837GUTK
@etrepum
etrepum added this pull request to the merge queue Jun 11, 2026
Merged via the queue into facebook:main with commit e3feb66 Jun 11, 2026
46 checks passed
@etrepum etrepum mentioned this pull request Jun 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.

Bug: [lexical-playground] clear-formatting doesn't clear text-left/right/center alignment when no formatting to text

5 participants