Back to Plate

Single-block fragment replacement must preserve target block before full-document replace

docs/solutions/logic-errors/2026-05-04-single-block-fragment-replacement-must-preserve-target-block-before-full-document-replace.md

53.0.62.9 KB
Original Source

Single-block fragment replacement must preserve target block before full-document replace

Problem

Rich Slate fragment insertion could downgrade the receiving block when the selection covered the entire single-block document. The full-document replace fast path ran before target-block ownership had a chance to preserve the receiving block wrapper.

Symptoms

  • A paragraph fragment inserted over selected heading text produced a paragraph.
  • The post-insert selection still landed at the right text offset, which made the type regression easy to miss if tests only asserted selection.

What Didn't Work

  • Fixing only the normal empty-target insertion path was not enough. A single-block document with its full text selected is also a full-document range, so it bypassed the normal insertion path entirely.
  • Treating every full-document rich fragment paste as a full document replacement was too broad for the single text-block replacement case.

Solution

Handle the one-block text replacement case before the generic full-document replace path.

ts
if (
  editorChildren.length === 1 &&
  fragment.length === 1 &&
  isTextBlockElement(editor, onlyEditorNode) &&
  isTextBlockElement(editor, onlyFragmentNode)
) {
  replaceSnapshot(editor, {
    children: [
      {
        ...onlyEditorNode,
        children: onlyFragmentNode.children,
      },
    ],
    selection: getBlockChildrenEndSelection([0], onlyFragmentNode.children),
  })
  return
}

Then keep the normal empty-target path aligned by unwrapping a single text-block fragment into the receiving empty block instead of inserting the fragment wrapper as the new block.

Why This Works

There are two different ownership policies:

  • multi-block or structurally rich full-document paste lets the fragment own the document shape;
  • single text-block replacement lets the target block own the wrapper and the fragment own the inline/text children.

The failing case looked like the first policy because the range covered the whole document, but semantically it was the second policy. The fix checks that small target-owned shape before the broad replace path.

Prevention

  • When fragment insertion has a full-document shortcut, add a one-block selected-target test before trusting the shortcut.
  • Assert both tree shape and selection placement for clipboard/fragment fixes.
  • Add DOM clipboard round-trip coverage when the issue is paste-visible, even if the core transaction test already proves the model path.