feat(extension-list-keymap): sink paragraph into previous list item on tab#7893
feat(extension-list-keymap): sink paragraph into previous list item on tab#7893iamgio wants to merge 2 commits into
Conversation
✅ Deploy Preview for tiptap-embed ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
🦋 Changeset detectedLatest commit: eff4214 The changes in this PR will be included in the next version bump. This PR includes changesets to release 73 packages
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 |
bdbch
left a comment
There was a problem hiding this comment.
LGTM - however there are some changes in here that I think were meant to be in another PR?
|
@bdbch yes, it's written in the blockquote at the beginning. These changes were dependent on those of my previous PR. It'll look correct once the other PR is merged |
|
Alright - lets get your blockquote PR merged first then. |
5e4a19b to
e678865
Compare
There was a problem hiding this comment.
Pull request overview
This PR extends @tiptap/extension-list’s ListKeymap to handle a common “continuation paragraph after a list” workflow by adding a Tab shortcut that moves a top-level textblock into the previous list’s last item. It also refines the Backspace handling so Backspace at the start of a non-first block inside the same list item (e.g. a second paragraph) falls back to ProseMirror’s default merge behavior.
Changes:
- Add
handleTaband wire aTabshortcut intoListKeymapto sink a following top-level textblock into the previous list’s last item. - Add a core helper
getPreviousBlockSibling($pos)to reliably fetch the previous block sibling around the current textblock. - Add Playwright coverage for the new Tab behavior and the Backspace regression/round-trip scenarios, plus a changeset documenting the user-facing additions.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| packages/extension-list/src/keymap/listHelpers/index.ts | Re-export the new handleTab helper from the list keymap helper barrel. |
| packages/extension-list/src/keymap/listHelpers/handleTab.ts | Implements the Tab sink behavior by moving the current top-level block into the previous list’s last item and restoring selection. |
| packages/extension-list/src/keymap/listHelpers/handleBackspace.ts | Adds a guard to only intercept Backspace at the start of the first child within a list item, preserving expected intra-item merging. |
| packages/extension-list/src/keymap/list-keymap.ts | Registers Tab alongside the existing key handlers and routes through per-list-type handling. |
| packages/core/src/helpers/index.ts | Exports the new core helper. |
| packages/core/src/helpers/getPreviousBlockSibling.ts | New helper to fetch the previous block sibling for a resolved position. |
| demos/src/Extensions/ListKeymap/index.spec.ts | Adds Playwright tests covering Tab sink behavior, precedence vs sinkListItem, and Backspace round-trips. |
| .changeset/2026-05-29-list-keymap-tab-sink-into-list.md | Documents the new Tab behavior in ListKeymap and the new @tiptap/core helper export. |
bdbch
left a comment
There was a problem hiding this comment.
Sorry for coming back again - some small nitpicks:
- Could you implement a way to check if the sank content actually fits into the schema? otherwise this could throw an error when the content won't fit the list schema
- Can you add a test for the new getPreviousBlockSibling util?
Otherwise this looks fine.
…n tab ListKeymap now registers a `Tab` shortcut that sinks a top-level textblock into the previous list's last item when pressed at the start of that textblock. The paragraph (and its inline content) is moved inside the last list item as an additional block child. The handler bails when the cursor is already inside a list item (preserving `sinkListItem`'s nesting behavior), when there is no list before the paragraph, or when the caret is mid-textblock. `handleBackspace` is also tightened: pressing Backspace at the start of a non-first child of a list item now falls through to PM's joinBackward so the textblock merges upward inside the same item, instead of lifting the whole item out. Makes the Tab sink and the subsequent Backspace round-trip cleanly. A small reusable helper `getPreviousBlockSibling($pos)` is exposed from @tiptap/core: it returns the block-level sibling immediately before the cursor's textblock, or null when the cursor is at the first child of its block parent. The same pattern lives inline in a couple of other keymap paths (blockquote backspace from ueberdosis#7891, hasListBefore / hasListItemBefore); migrating those callers can happen as follow-ups. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e678865 to
e5ae8fa
Compare
Changes Overview
Add a
Tabshortcut toListKeymapthat sinks a top-level textblock into the previous list's last item. Pressing Tab at the start of a paragraph immediately following a bullet/ordered/task list moves the paragraph (and its inline content) inside the last list item, where it becomes an additional block child. The handler stays out of the way when the cursor is already inside a list item (sosinkListItem's nesting behavior is preserved), when there is no list before the paragraph, or when the caret is mid-textblock.Use case: a user types a continuation under a list item that should belong inside it. Today the paragraph stays at the top level and
ListKeymapmakes it impossible to nest.Round-trip example
Screen.Recording.2026-05-29.at.5.10.01.PM.mov
Implementation Approach
New helper
listHelpers/handleTab.ts:insideLastItemEnd = blockStart - 2lands inside the previous list's last item at its end (one position back for the list's closing token, one more for the last item's closing token). Same PM-coordinate reasoning as the merge step in the blockquote backspace PR (#7891).handleBackspacegains a guard that checks whether the cursor's textblock is the first child of its list item ancestor; if not, returnfalseand let PM handle it.The
ListKeymapextension registersTabalongside the existing Backspace / Delete handlers.ListItem.Tab(which callssinkListItem) continues to win when the cursor is already inside a list item, becausehandleTabbails in that case.Testing Done
Added Playwright tests in
demos/src/Extensions/ListKeymap/index.spec.ts:sinkListItem(regression check forListItem.Tab).All existing list specs (BulletList, OrderedList, TaskList) continue to pass; 56 chromium tests pass overall.
pnpm check(lint + format) is clean.Verification Steps
pnpm installpnpm test:e2e --grep "ListKeymap|BulletList|OrderedList|TaskList"/src/Extensions/ListKeymap/React/: set content to<ul><li>A</li></ul><p></p>, focus the empty paragraph, press Tab. The paragraph sinks inside the last list item. Press Backspace. The state returns to<ul><li>A</li></ul>.Additional Notes
handleBackspaceis a real bug fix that would also matter onmain. I bundled it here because the Tab feature is the most obvious way for users to encounter it.hasListBefore/hasListItemBefore. Happy to follow up in a separate PR.Checklist