(help needed) `Shift+Enter` for hard_break is creating invalidated transactions

TL;DR: Shift+Enter hard_break in list items causes content deletion on subsequent typing

I have a requirement where i am wrapping everything into a custom block which can have one or multiple nodes.

  • Custom schema with customBlock nodes containing block+ content

  • List items inside these blocks with schema: list_item: { content: 'paragraph block*' }

  • hard_break defined as: { inline: true, group: 'inline', toDOM: () => ['br'], parseDOM: [{ tag: 'br' }] }

Desired behavior:

  • Enter in normal paragraph should create new <custom-block><p></p></custom-block>

  • Enter in list item → split list item (works fine using splitListItem)

  • Shift+Enter in list item → insert hard_break (line break within same paragraph)

Implementation:

// Shift+Enter handler: Insert hard_break in tables/lists, new paragraph within custom block
// NOTE: hard_break in list items has known DOM observation issues in some browsers
// where typing after the break can cause content deletion. This is a ProseMirror limitation.
function handleShiftEnter(schema: Schema) {
  return (state: EditorState, dispatch?: (tr: Transaction) => void) => {
    const { $from, empty } = state.selection
    if (!empty) return false

    if (!dispatch) return true
    // Tables/Lists → insert hard_break (line break within the same paragraph)
    if (isInTable($from) || isInList($from)) {
      const br = schema.nodes.hard_break.create()
      const tr = state.tr.replaceSelectionWith(br).scrollIntoView()
      dispatch(tr)
      return true
    }
    // custom block/answer → new paragraph within SAME block
    const customBlockDepth = findAncestorOfType($from, ['customBlock'])
    if (blockDepth === -1) return false

    const tr = state.tr
    const depthToSplit = $from.depth - customBlockDepth

    // Use tr.split with explicit attrs to prevent copying data-id from parent
    if (depthToSplit > 0) {
      tr.split($from.pos, depthToSplit, [
        { type: schema.nodes.paragraph, attrs: {} },
      ])
    } else {
      const newParagraph = schema.nodes.paragraph.create(editorOrigin)
      tr.insert($from.pos, newParagraph)
    }

    dispatch(tr)
    return true
  }
}

Problem:

  1. Shift+Enter inserts the hard_break correctly

  2. Typing any character immediately after causes content deletion

Demo:

CleanShot 2026-01-06 at 20.25.01

I have tried to use a simpler version of shift + enter

function handleShiftEnter(schema: Schema) {
  const br = schema.nodes.hard_break
  return chainCommands(
    exitCode,
    (state: EditorState, dispatch?: (tr: Transaction) => void) => {
      if (dispatch)
        dispatch(state.tr.replaceSelectionWith(br.create()).scrollIntoView())
      return true
    }
  )
}

I still face the same issue.

CleanShot 2026-01-06 at 20.55.41

If you mean by ‘invalid transaction’ that applying the step crashes, replaceSelectionWith should not be capable of that. If you are seeing that, could you create a minimal schema and script that reproduces it?