[lexical-playground] Bug Fix: clear block alignment and indent with a collapsed selection but not a partial one#8666
Conversation
… collapsed selection but not a partial one
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
potatowagon
left a comment
There was a problem hiding this comment.
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()withCaretRangeto 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.
|
I also made 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 Mine's just a playground helper for now though, and moving it to |
…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
…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
…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
…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
…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
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
$isBlockFullySelectedfrom@lexical/utils(#8532) against the selection afterselection.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
HeadingNodewith aParagraphNode) detach nodes that the selection's carets may refer to, which made$comparePointCaretNextthrow 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