Skip to content

[lexical][lexical-utils][lexical-extension][lexical-playground] Feature: SelectBlockExtension#8532

Merged
etrepum merged 38 commits into
facebook:mainfrom
levensta:selection-block-extension
Jun 11, 2026
Merged

[lexical][lexical-utils][lexical-extension][lexical-playground] Feature: SelectBlockExtension#8532
etrepum merged 38 commits into
facebook:mainfrom
levensta:selection-block-extension

Conversation

@levensta

@levensta levensta commented May 21, 2026

Copy link
Copy Markdown
Contributor

Description

New SelectBlockExtension that overrides the SELECT_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 cascadeSelection option (set to false by 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.

PreventSelectAllExtension is enabled by default in the SelectBlockExtension extension's dependencies, and its disabled state is kept in sync with SelectBlockExtension. This prevents the keydown event 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 the SELECT_ALL_COMMAND command in the editor. Partially closes #6632

New 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 a RangeSelection or CaretRange and handles ancestor element points, backward selections, trailing decorators, and empty blocks (the clearFormatting fix 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) in lexical: reads the pending editor state if it exists (the committed state otherwise) without flushing. The cascadeSelection check uses this so that a SELECT_ALL_COMMAND dispatched 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

  • Unit tests for SelectBlockExtension and PreventSelectAllExtension in @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 and PreventSelectAllExtension coupling, and nested editors with cascadeSelection on and off — including an empty nested editor, dispatch from inside a nested editor update, and the command priority bucket requirement (the nested test editor uses RichTextExtension, so moving the listener into the COMMAND_PRIORITY_EDITOR bucket fails the cascade tests)
  • Unit tests for $isBlockFullySelected in @lexical/utils and LexicalEditor.readPending() in lexical
  • E2E tests in the playground (SelectBlock.spec.mjs), covering selection of blocks with formatted text, inline elements and decorators, node selections, shadow roots, nested editors, and multi-block expansion
  • The selectAll keyboard 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)

@vercel

vercel Bot commented May 21, 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 6:20pm
lexical-playground Ready Ready Preview, Comment Jun 11, 2026 6:20pm

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 May 21, 2026
@levensta levensta changed the title [lexical-extension][lexical-playgrpund] Feature: SelectBlockExtension [WIP][lexical-extension][lexical-playgrpund] Feature: SelectBlockExtension May 21, 2026
@etrepum etrepum changed the title [WIP][lexical-extension][lexical-playgrpund] Feature: SelectBlockExtension [WIP][lexical-extension][lexical-playground] Feature: SelectBlockExtension May 21, 2026
@levensta
levensta force-pushed the selection-block-extension branch from 5fa6638 to d51b9c7 Compare May 28, 2026 01:20
@levensta levensta changed the title [WIP][lexical-extension][lexical-playground] Feature: SelectBlockExtension [lexical-extension][lexical-playground] Feature: SelectBlockExtension May 28, 2026
@levensta
levensta marked this pull request as ready for review May 28, 2026 01:35
claude and others added 2 commits June 11, 2026 09:22
…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 etrepum changed the title [lexical-extension][lexical-playground] Feature: SelectBlockExtension [lexical-utils][lexical-extension][lexical-playground] Feature: SelectBlockExtension Jun 11, 2026
etrepum and others added 2 commits June 11, 2026 10:34
…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
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

4 participants