#8321·tiptap

ListKeymap Delete can cross isolating: true node boundaries and join content from a different frame

Author: GamewiseCreated Sep 7, 2026Updated Sep 13, 2026
Labelsstatus: triagearea: editorcomplexity: hardimpact: medium

Affected Packages

listKeymap

Tiptap Version

3.30.5 (but I believe all versions)

Browser Used

Chrome

What happened?

The ListKeymap Delete handler crosses an ancestor with isolating: true when deleting at the end of a list item.

I have a custom frame node with: { content: 'block+', isolating: true, } and multiple frame siblings inside a page.

Given this document: page ├── frame (isolating: true) │ ├── orderedList │ │ └── listItem │ │ └── paragraph("hello |") │ └── paragraph("world") │ └── frame (isolating: true) └── paragraph("whoops") where the cursor is at the end of "hello".

Pressing Delete should operate within the first frame only since frames have isolating:true. In particular, the paragraph immediately following the list should be the relevant forward content, and when delete is pressed it should result in a list item with hello world; the second frame should never be modified.

With ListKeymap enabled, however, Delete crosses the isolating frame boundary and produces: frame 1 ├── orderedList │ └── listItem("hello") └── paragraph("worldwhoops") with the second frame removed entirely.

Expected Behavior

The isolating: true boundary should prevent the Delete operation from crossing from the first frame into the second frame.

For normal editor behaviour, when the cursor is at the end of the final list item and there is a paragraph immediately following the list in the same frame, that paragraph should be merged into the final list item rather than content from a later frame being used.

At an absolute minimum, the operation should not modify or remove the second frame.

Reproducible Example URL (Optional)

No response

Additional Context (Optional)

The important part of my schema is simply: `const Frame = Node.create({ name: 'frame', content: 'block+', isolating: true, })

const Page = Node.create({ name: 'page', content: 'frame*', })`

Minimal reproduction

The smallest conceptual document is: page ├── frame [isolating] │ ├── orderedList │ │ └── listItem │ │ └── paragraph("A|") │ └── paragraph("B") │ └── frame [isolating] └── paragraph("C")

Press Delete at the end of A. Expected: frame 1 remains intact frame 2 remains untouched B is joined to A

Actual: frame 2 is removed "C" is joined into "B"

The problem appears to come from the list-specific Delete path.

The ListKeymap Delete handling eventually calls joinItemForward(). That command uses ProseMirror's joinPoint() to find a join point in the ancestor hierarchy.

The important issue is that this ancestor search does not appear to treat NodeSpec.isolating as a boundary in the same way that ProseMirror's normal structural delete commands do.

In contrast, ProseMirror's isolating documentation says:

“the sides of nodes of this type count as boundaries that regular editing operations, like backspacing or lifting, won't cross.”