[lexical][lexical-utils][lexical-extension][lexical-playground] Feature: SelectBlockExtension#8532
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
levensta
force-pushed
the
selection-block-extension
branch
from
May 28, 2026 01:20
5fa6638 to
d51b9c7
Compare
levensta
marked this pull request as ready for review
May 28, 2026 01:35
levensta
requested review from
acywatson,
fantactuka,
ivailop7 and
zurfyx
as code owners
May 28, 2026 01:35
…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
approved these changes
Jun 11, 2026
…bucket requirement and add LexicalEditor.readPending
COMMAND_PRIORITY_BEFORE_EDITOR (or any other EDITOR bucket priority)
would break cascadeSelection: listeners run for all editors in priority
bucket order with the nested editor first within each bucket, so an
EDITOR bucket listener on the parent would run after the nested editor's
own RichTextExtension SELECT_ALL_COMMAND handler. Document this
constraint at the registration site.
Build the nested test editor with RichTextExtension (wired to the parent
with NestedEditorExtension) so the cascade tests fail if the listener is
ever moved into the EDITOR bucket (previously they passed with a bare
nested editor, masking the regression). This also makes the nested
editor tests more faithful to the image caption flow: the first press is
handled by the nested editor itself and the second press cascades to the
parent, and the cascade-off test now asserts the parent's NodeSelection
survives intact.
Add LexicalEditor.readPending() which reads the pending editor state if
it exists (the committed state otherwise) without flushing, and use it
for the cascadeSelection check. getEditorState().read() would miss
queued updates in the nested editor (such as the batched initial state
from InitialStateExtension, so a SELECT_ALL_COMMAND dispatched before
the nested editor's first commit would read its stale pre-initialization
state and decline to cascade), and read() would flush them, which
corrupts the nested editor when the command is dispatched from inside
one of its updates ("Cannot call set() on a frozen Lexical node map").
Both scenarios are covered by new tests.
Import from the public @lexical/extension module instead of individual
files in the unit tests, except for deepThemeMergeInPlace which is not
exported.
https://claude.ai/code/session_013LyjzFTiTDhbaNe3Tadiqo
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
New
SelectBlockExtensionthat overrides theSELECT_ALL_COMMAND. The first call selects the nearest block element, the second call selects the entire document. A selection that already spans multiple blocks expands directly to the entire document, and select all is a no-op when the document is already fully selected (this is checked against the current state, so it still works after concurrent changes from collaborators or plugins).The extension also includes the
cascadeSelectionoption (set tofalseby default). When enabled, select all in a nested editor (such as an image caption) cascades to the parent document once all of the nested editor's content is selected. When disabled, commands bubbled up from nested editors are ignored so the nested editor's own handlers take care of them.PreventSelectAllExtensionis enabled by default in theSelectBlockExtensionextension's dependencies, and its disabled state is kept in sync withSelectBlockExtension. This prevents thekeydownevent propagating from input/textarea HTML elements to the lexical root when the Ctrl + A key combination is pressed. As a result, selecting all within input fields does not trigger theSELECT_ALL_COMMANDcommand in the editor. Partially closes #6632New supporting APIs:
$isBlockFullySelected(blockNode, selectionOrRange)in@lexical/utils: a caret-based containment check that returns true when the selection covers the entire block. It accepts aRangeSelectionorCaretRangeand handles ancestor element points, backward selections, trailing decorators, and empty blocks (theclearFormattingfix in [lexical-playground] Bug Fix: clear block alignment and indent with a collapsed selection but not a partial one #8666 can adopt it as well).LexicalEditor.readPending(callbackFn)inlexical: reads the pending editor state if it exists (the committed state otherwise) without flushing. ThecascadeSelectioncheck uses this so that aSELECT_ALL_COMMANDdispatched before the nested editor's first commit, or from inside one of its updates, observes the nested editor's current state —editor.read()would flush pending updates, which is not safe while an update is in progress.Also documents the new extensions in
included-extensions.md, along with other previously missing entries (@lexical/clipboard,@lexical/html, and the rest of@lexical/extension).Closes #4665
Playground demo
select.block.demo.mov
Test plan
SelectBlockExtensionandPreventSelectAllExtensionin@lexical/extension: block/document select all progression (including single-block documents), multi-block expansion, root-anchored and manually created full selections, select all after concurrent document changes, node selections, the disabled config andPreventSelectAllExtensioncoupling, and nested editors withcascadeSelectionon and off — including an empty nested editor, dispatch from inside a nested editor update, and the command priority bucket requirement (the nested test editor usesRichTextExtension, so moving the listener into theCOMMAND_PRIORITY_EDITORbucket fails the cascade tests)$isBlockFullySelectedin@lexical/utilsandLexicalEditor.readPending()inlexicalSelectBlock.spec.mjs), covering selection of blocks with formatted text, inline elements and decorators, node selections, shadow roots, nested editors, and multi-block expansionselectAllkeyboard helper is normalized on Linux/Firefox so existing e2e suites no longer need per-platform selection branches (E2E keyboardShortcuts/index.mjs -> selectAll(page) Linux selection should be normalized #4665)