Skip to content

fix: sync the selection in read-only editors#2918

Merged
christianhg merged 1 commit into
mainfrom
fix-readonly-selection-sync
Jul 6, 2026
Merged

fix: sync the selection in read-only editors#2918
christianhg merged 1 commit into
mainfrom
fix-readonly-selection-sync

Conversation

@christianhg

@christianhg christianhg commented Jul 6, 2026

Copy link
Copy Markdown
Member

A field report from the table work: switch an editor to read-only while a multi-cell (selection-derived) highlight is active and the highlight is stuck forever; make a new selection in the read-only editor and no selection-derived UI appears, and copying puts nothing on the clipboard.

Both symptoms share one root. The selectionchange sync bails unless the editable is the document's active element, and a read-only editable (contenteditable="false", no tab stop) can never be the active element, so DOM-to-model selection sync is structurally dead in read-only editors. The model selection freezes at whatever it held when read-only began: stale selection-derived rendering paints forever, new selections never reach getTableSelection-style consumers, and clipboard.copy serializes the outdated selection onto a prevented default, hence the empty clipboard.

The intent of the surrounding code is clearly the opposite, three separate pieces already accommodate read-only selection: the editor machine whitelists select, clipboard.copy, and serialize in read-only mode, the selectionchange handler carries a read-only-specific deselect guard just below the gate, and hasDOMNode's editable check has an explicit clause accepting targets whose closest contenteditable="false" ancestor is the editor root, commented as existing precisely for the read-only case. The focus gate above them was the one piece that never yielded.

This is a regression from 2a3e039 (April 2026), which added a one-line early return so a toolbar click's blur could not clobber the editor's selection with the browser's collapsed one. That purpose is editable-mode-specific; read-only editors, which are never the active element, lost selection sync as collateral.

The fix makes the gate yield in read-only editors only, preserving 2a3e039's behavior for editable editors exactly. The selectable/in-editor checks below still confine the sync to selections inside the editor, and the existing deselect guard still clears the model when the selection leaves it, so the blast radius is the intended one: read-only editors gain working selection tracking (and therefore selection-derived UI and copy), while editable editors keep the gate exactly as before, all 1701 existing browser tests pass unmodified.

selection-readonly-sync.test.tsx pins both reported shapes: a selection made while read-only reaches the model with full literal points, and a selection made before switching to read-only keeps following the DOM instead of freezing. Both scenarios are red without the gate change.

@vercel

vercel Bot commented Jul 6, 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 6, 2026 10:25am
portable-text-example-basic Ready Ready Preview, Comment Jul 6, 2026 10:25am
portable-text-playground Ready Ready Preview, Comment Jul 6, 2026 10:25am

Request Review

@changeset-bot

changeset-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cd1ba48

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

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Stats — @portabletext/editor

Compared against main (cc7c1f0f)

@portabletext/editor

Metric Value vs main (cc7c1f0)
Internal (raw) 801.1 KB -3 B, -0.0%
Internal (gzip) 153.2 KB +3 B, +0.0%
Bundled (raw) 1.41 MB -3 B, -0.0%
Bundled (gzip) 317.4 KB +2 B, +0.0%
Import time 96ms +2ms, +2.1%

@portabletext/editor/behaviors

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

@portabletext/editor/plugins

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

@portabletext/editor/selectors

Metric Value vs main (cc7c1f0)
Internal (raw) 81.2 KB -
Internal (gzip) 14.9 KB -
Bundled (raw) 76.7 KB -
Bundled (gzip) 13.8 KB -
Import time 8ms -0ms, -0.2%

@portabletext/editor/traversal

Metric Value vs main (cc7c1f0)
Internal (raw) 28.0 KB -
Internal (gzip) 5.6 KB -
Bundled (raw) 27.9 KB -
Bundled (gzip) 5.5 KB -
Import time 6ms +0ms, +0.7%

@portabletext/editor/utils

Metric Value vs main (cc7c1f0)
Internal (raw) 29.7 KB -
Internal (gzip) 6.2 KB -
Bundled (raw) 27.1 KB -
Bundled (gzip) 5.8 KB -
Import time 6ms -0ms, -0.6%

🗺️ . · ./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 (cc7c1f0f)

Metric Value vs main (cc7c1f0)
Internal (raw) 53.8 KB -
Internal (gzip) 9.8 KB -
Bundled (raw) 348.9 KB -
Bundled (gzip) 96.3 KB -
Import time 37ms -1ms, -3.8%

🗺️ 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.

The `selectionchange` handler bailed unless the editable was the
document's active element, and a read-only editable
(`contenteditable="false"`, no tab stop) can never be the active
element, so DOM-to-model selection sync was structurally dead in
read-only editors. The failure surfaced two ways: a selection made
before switching to read-only froze in the model (stale
selection-derived UI stuck on screen), and selections made while
read-only never reached the model, so `clipboard.copy` serialized an
outdated selection onto a prevented default and produced an empty
clipboard.

This regressed in 2a3e039, which added the early return so a toolbar
click's blur could not clobber the editor's selection with the
browser's collapsed one. That purpose is editable-mode-specific, and
everything around the gate expects read-only selection to work: the
editor machine whitelists `select`, `clipboard.copy`, and `serialize`
in read-only mode, the handler carries a read-only-specific deselect
guard, and `hasDOMNode`'s editable check explicitly accommodates
targets inside a `contenteditable="false"` editor root.

The gate now yields in read-only editors only. Editable editors keep
the early return exactly as 2a3e039 intended; the selectable and
in-editor checks below still confine the sync to selections inside the
editor, and the deselect guard still clears the model when the
selection leaves. Red-verified: both pinned scenarios fail without the
gate change.
@christianhg christianhg force-pushed the fix-readonly-selection-sync branch from 99fdf25 to cd1ba48 Compare July 6, 2026 10:25
@christianhg christianhg marked this pull request as ready for review July 6, 2026 10:27
@christianhg christianhg enabled auto-merge (rebase) July 6, 2026 10:28
@christianhg christianhg disabled auto-merge July 6, 2026 10:30
@christianhg christianhg merged commit 2f2d6e9 into main Jul 6, 2026
17 checks passed
@christianhg christianhg deleted the fix-readonly-selection-sync branch July 6, 2026 10:30
@ecoscript ecoscript Bot mentioned this pull request Jul 6, 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