Skip to content

fix: require a collapsed selection for the forward-delete empty-block hop#3008

Merged
christianhg merged 1 commit into
mainfrom
repro-empty-block-range-delete
Jul 22, 2026
Merged

fix: require a collapsed selection for the forward-delete empty-block hop#3008
christianhg merged 1 commit into
mainfrom
repro-empty-block-range-delete

Conversation

@christianhg

@christianhg christianhg commented Jul 22, 2026

Copy link
Copy Markdown
Member

Field report: pressing Enter a few times creates empty lines, the user highlights them and presses Delete, and the lines remain. Reproduced with real gestures: keyboard-select from a text block down across three empty blocks, press Delete, and only the focus-edge block is affected while the fully covered empty blocks survive. Backspace works.

The diagnosis moved twice before landing. The editor's logical selection is verifiably expanded before the keypress, and the same gesture over non-empty lines deletes correctly, which pointed at DOM filler-node mapping. A synthetic probe then exonerated the DOM entirely: delete without a direction handles the exact range correctly, while delete with direction: 'forward', precisely what the editable's expanded-selection branch sends for the Delete key, reproduces the gesture failure byte-for-byte. The bug is in the abstract delete behaviors.

The forward-delete rule that hops a caret past an empty block guards only on the event direction, the focus block being an empty text block, and a next sibling existing. All three hold for an expanded selection ending at an empty block's start, so the rule intercepted the event and replaced the range-delete with "delete the focus block, select the next block's start". Its sibling rules (backward merge, end-of-block merge) were immune by construction because isAtTheStartOfBlock/isAtTheEndOfBlock require a collapsed selection; isEmptyTextBlock is a node predicate with no selection sensitivity. The fix gives the rule the same collapsed requirement via isSelectionCollapsed, so expanded selections fall through to the default range-delete.

Net behavior change is confined to forward delete over an expanded selection whose focus block is an empty text block. Caret behavior is untouched (the empty-block hop still fires for collapsed selections, pinned by the existing behavior suites, all green) and Backspace was never affected. Three pins land in event.delete.forward.test.tsx, red without the guard: the real-gesture scenario, the synthetic direction: 'forward' contract, and the non-empty-lines control that is green throughout.

@changeset-bot

changeset-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d881f38

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 22, 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 22, 2026 7:55pm
portable-text-example-basic Ready Ready Preview, Comment Jul 22, 2026 7:55pm
portable-text-playground Ready Ready Preview, Comment Jul 22, 2026 7:55pm

Request Review

@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Stats — @portabletext/editor

Compared against main (bec5ff08)

@portabletext/editor

Metric Value vs main (bec5ff0)
Internal (raw) 807.5 KB +78 B, +0.0%
Internal (gzip) 155.1 KB +13 B, +0.0%
Bundled (raw) 1.42 MB +79 B, +0.0%
Bundled (gzip) 319.4 KB +7 B, +0.0%
Import time 100ms -0ms, -0.4%

@portabletext/editor/behaviors

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

@portabletext/editor/plugins

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

@portabletext/editor/selectors

Metric Value vs main (bec5ff0)
Internal (raw) 82.7 KB -
Internal (gzip) 15.4 KB -
Bundled (raw) 78.4 KB -
Bundled (gzip) 14.3 KB -
Import time 8ms +0ms, +1.3%

@portabletext/editor/traversal

Metric Value vs main (bec5ff0)
Internal (raw) 28.1 KB -
Internal (gzip) 5.6 KB -
Bundled (raw) 28.1 KB -
Bundled (gzip) 5.5 KB -
Import time 6ms +0ms, +0.5%

@portabletext/editor/utils

Metric Value vs main (bec5ff0)
Internal (raw) 30.6 KB -
Internal (gzip) 6.4 KB -
Bundled (raw) 28.2 KB -
Bundled (gzip) 6.1 KB -
Import time 6ms +0ms, +2.2%

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

Metric Value vs main (bec5ff0)
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.1%

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

… hop

Selecting a range that ends in an empty block (highlighting empty
lines) and pressing Delete left the covered blocks standing: the
abstract `delete` behavior that hops an empty focus block away on
forward delete guarded only on `event.direction`, the focus block
being an empty text block, and a next sibling existing. All three hold
for an expanded selection ending at an empty block's start, so the
behavior intercepted the event and replaced the range-delete with
"delete the focus block, select the next block's start".

Its sibling rules were immune by construction: the backward merge and
the end-of-block merge guard through `isAtTheStartOfBlock`/
`isAtTheEndOfBlock`, which require a collapsed selection, while
`isEmptyTextBlock` is a node predicate with no selection sensitivity.
The fix adds the same collapsed requirement via
`isSelectionCollapsed`, so expanded selections fall through to the
default range-delete. Backspace was never affected, and the caret hop
still fires for collapsed selections (existing behavior suites pin
it).

Three pins in `event.delete.forward.test.tsx`, red without the
guard: the real-gesture scenario (`shift+ArrowDown` then Delete), the
synthetic `delete` + `direction: 'forward'` contract the editable's
expanded-selection branch sends, and a non-empty-lines control that is
green throughout.
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