Skip to content

fix: implement editor-owned select-all#2912

Merged
christianhg merged 2 commits into
mainfrom
fix-editor-owned-select-all
Jul 4, 2026
Merged

fix: implement editor-owned select-all#2912
christianhg merged 2 commits into
mainfrom
fix-editor-owned-select-all

Conversation

@christianhg

@christianhg christianhg commented Jul 4, 2026

Copy link
Copy Markdown
Member

A field report: with a table as the document's only block, Cmd+A does nothing. The event log shows the keyboard.keydown arrive and then silence, no synthetic select, no sync, and the caret ends up parked at the table's start.

The diagnosis exonerates the engine. A synchronous capture of document.execCommand('selectAll') over the playground's table DOM shows chromium producing an already-collapsed range (div.playground-table-inner@0 -> @0); the selectionchange sync then faithfully applies the collapse and the writeback erases whatever the browser briefly rendered. Bisecting the DOM ingredients isolated the trigger, and it is broader than tables: chromium's select-all collapses whenever a non-editable element sits at either content edge of the editing host (fine in the middle, and nesting inside editable wrappers does not help; a <table> on its own, <colgroup>, and plain wrapper divs are all innocent). Block objects render contentEditable={false} and table renders carry non-editable selection chrome, so any document starting or ending with a void block, not just table-only documents, has had a native select-all no-op in chromium all along. The leafless-subtree fix (#2899) is unrelated: probes against its build behave identically, because no range ever exists for it to resolve.

The fix stops trusting the browser with the gesture. "Select everything" is a model-level statement, and round-tripping it through a DOM selection only inherits browser quirks, so the editor now owns the shortcut, the position structured-content editors converge on (ProseMirror binds Mod-a to its own selectAll command for the same reason). A selectAll entry joins defaultKeyboardShortcuts (Ctrl+A, Cmd+A on Apple platforms), and an abstract keyboard behavior beside the undo/redo shortcuts raises select over the full document range, computed with getLeaf edge descent from the root plus getBlockStartPoint/getBlockEndPoint on the enclosing blocks, following the behavior.core.insert-break.ts idiom. The end point matters: a container's block-level point edge-descends to its start during select resolution, so the behavior computes leaf-precise points rather than leaning on coercion; the table-only test pins the focus landing at the end of the last cell. Landing as an abstract behavior keeps it consumer-overridable, which leaves the door open for spreadsheet-style escalation (first Cmd+A selects the cell, then the table, then the document) as a plugin behavior later.

The regression test renders containers with real <table> DOM plus a non-editable chrome sibling, reproducing the browser's failure mode inside the harness (with a plain-div render, native select-all works and the test could never go red). A second test pins the block-objects-only document (voids at both content edges, the other real-world instance of the trigger), and a third pins plain-text documents. Net behavior for text documents is unchanged in outcome, only the mechanism shifts from DOM luck to a computed range; the existing cross-container suite, which exercises real select-all chords, passes without modification.

@changeset-bot

changeset-bot Bot commented Jul 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 92f9f37

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@portabletext/editor Patch
@portabletext/plugin-character-pair-decorator Patch
@portabletext/plugin-dnd Patch
@portabletext/plugin-emoji-picker Patch
@portabletext/plugin-input-rule Patch
@portabletext/plugin-list-index Patch
@portabletext/plugin-markdown-shortcuts Patch
@portabletext/plugin-one-line Patch
@portabletext/plugin-paste-link Patch
@portabletext/plugin-sdk-value Patch
@portabletext/plugin-table Patch
@portabletext/plugin-typeahead-picker Patch
@portabletext/plugin-typography Patch
@portabletext/toolbar Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
portable-text-editor-documentation Ready Ready Preview, Comment Jul 4, 2026 9:44am
portable-text-example-basic Ready Ready Preview, Comment Jul 4, 2026 9:44am
portable-text-playground Ready Ready Preview, Comment Jul 4, 2026 9:44am

Request Review

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Stats — @portabletext/editor

Compared against main (59d03e2e)

@portabletext/editor

Metric Value vs main (59d03e2)
Internal (raw) 800.7 KB +1.6 KB, +0.2%
Internal (gzip) 153.1 KB +445 B, +0.3%
Bundled (raw) 1.41 MB +1.7 KB, +0.1%
Bundled (gzip) 317.3 KB +442 B, +0.1%
Import time 95ms -2ms, -1.9%

@portabletext/editor/behaviors

Metric Value vs main (59d03e2)
Internal (raw) 467 B -
Internal (gzip) 207 B -
Bundled (raw) 424 B -
Bundled (gzip) 171 B -
Import time 2ms -0ms, -2.0%

@portabletext/editor/plugins

Metric Value vs main (59d03e2)
Internal (raw) 2.7 KB -
Internal (gzip) 894 B -
Bundled (raw) 2.5 KB -
Bundled (gzip) 827 B -
Import time 7ms -0ms, -0.5%

@portabletext/editor/selectors

Metric Value vs main (59d03e2)
Internal (raw) 80.1 KB -
Internal (gzip) 14.7 KB -
Bundled (raw) 75.5 KB -
Bundled (gzip) 13.5 KB -
Import time 8ms +0ms, +0.9%

@portabletext/editor/traversal

Metric Value vs main (59d03e2)
Internal (raw) 26.9 KB -
Internal (gzip) 5.3 KB -
Bundled (raw) 26.7 KB -
Bundled (gzip) 5.2 KB -
Import time 6ms -0ms, -1.1%

@portabletext/editor/utils

Metric Value vs main (59d03e2)
Internal (raw) 29.7 KB -
Internal (gzip) 6.2 KB -
Bundled (raw) 27.1 KB -
Bundled (gzip) 5.8 KB -
Import time 6ms +0ms, +1.3%

🗺️ . · ./behaviors · ./plugins · ./selectors · ./traversal · ./utils · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — @portabletext/markdown

Compared against main (59d03e2e)

Metric Value vs main (59d03e2)
Internal (raw) 53.8 KB -
Internal (gzip) 9.8 KB -
Bundled (raw) 348.9 KB -
Bundled (gzip) 96.3 KB -
Import time 39ms +1ms, +3.2%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

@christianhg christianhg force-pushed the fix-editor-owned-select-all branch from 9ffbb9c to 93c6376 Compare July 4, 2026 08:58
@christianhg christianhg force-pushed the fix-editor-owned-select-all branch from 93c6376 to f266024 Compare July 4, 2026 09:01
@christianhg christianhg force-pushed the fix-editor-owned-select-all branch from f266024 to dc95ba3 Compare July 4, 2026 09:06
@christianhg christianhg force-pushed the fix-editor-owned-select-all branch from dc95ba3 to 7888032 Compare July 4, 2026 09:14
Select-all was left entirely to the browser: the native gesture wrote a
DOM selection and the `selectionchange` sync reverse-engineered it into
the model. Chromium builds an already-collapsed range whenever a
non-editable element sits at either content edge of the editing host
(fine in the middle, and nesting inside editable wrappers does not
help). Block objects render `contentEditable={false}` and table renders
carry non-editable chrome, so any document starting or ending with a
void block, and any table-only document, had a select-all no-op: native
select-all emits the collapsed range, the sync faithfully applies it,
and the writeback erases whatever the browser briefly showed.

"Everything" is a model-level statement, so the editor now owns the
gesture: a `selectAll` shortcut joins `defaultKeyboardShortcuts`
(Ctrl+A, Cmd+A on Apple platforms) and an abstract keyboard behavior
raises `select` over the document range, computed from the model via
`getLeaf` edge descent from the root and
`getBlockStartPoint`/`getBlockEndPoint` on the enclosing blocks. This
is the position structured-content editors converge on (ProseMirror
binds Mod-a to its own `selectAll` command for the same reason). Being
an abstract behavior, consumers can override it, e.g. for
spreadsheet-style cell-first escalation.

Net behavior for plain text documents is unchanged in outcome: the
keydown is now consumed and the range computed from the model instead
of trusted from the DOM. The existing cross-container suite exercising
real select-all chords passes without modification.
…ection

`ControlOrMeta` resolves from the host OS while the shortcut guard's
`IS_APPLE` resolves from the user agent, and playwright's webkit
reports a Mac user agent on Linux, so the chord missed the guard there
and the test passed only via native select-all. Align with the
`IS_MAC` chord idiom the other shortcut tests use.
@christianhg christianhg enabled auto-merge (rebase) July 4, 2026 09:45
@christianhg christianhg merged commit a71a830 into main Jul 4, 2026
17 checks passed
@christianhg christianhg deleted the fix-editor-owned-select-all branch July 4, 2026 09:50
@ecoscript ecoscript Bot mentioned this pull request Jul 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant