Skip to content

[lexical-react][lexical-playground] Feature: Draggable individual list items - #9705

Draft
playlikeboy wants to merge 1 commit into
meta:mainfrom
playlikeboy:feat/draggable-list-items
Draft

playlikeboy wants to merge 1 commit into
meta:mainfrom
playlikeboy:feat/draggable-list-items

Conversation

@playlikeboy

Copy link
Copy Markdown

Description

DraggableBlockPlugin_EXPERIMENTAL only ever targets top-level blocks, so hovering a list shows one handle for the whole <ul>/<ol> and individual items cannot be reordered by drag (#7630). Two earlier attempts (#7640, #8178) stalled; the review feedback on them was that nested lists should be reorderable at any depth, that the drop line should only appear where the dragged node can actually land, and that there is no API surface to preserve, so the targeting code may change as needed.

This PR changes the targeting in two places:

  • Handle targeting walks down. After the existing top-level search finds the block under the pointer, $getNestedBlockElement descends into the child block that contains the pointer, repeatedly, so a list item at any depth gets its own handle. Descent stops at shadow roots (tables, layout containers, collapsibles), which stay one draggable unit as before, and at blocks with only inline content. If no child block is under the pointer (the margin band of a list), the block itself is the target, so a whole list can still be dragged from its edge.
  • Drop targeting is aware of the dragged node. $resolveDropTarget starts at the hovered block and walks up to the nearest ancestor that can take the dragged node as a sibling ($canDropAsSibling): anything under the root; a node that requires a parent (a list item) only beside nodes whose parent has the same type as its own, so items move between lists of any type and depth; any other block never beside such nodes. A paragraph dragged over a list item therefore targets the enclosing list and the drop line is drawn above or below the list, not between its items. On drop, a list item that lands outside a list is wrapped in $copyNode(parent), so a numbered item stays numbered (the ListItemNode transform alone would make it a bullet). The drop line starts at the target's left edge so the nesting depth of the target is visible.

The rules are expressed with isParentRequired() / isShadowRoot() / isInline() rather than list node classes, so the plugin does not depend on @lexical/list. Playground containers that are shadow roots (collapsible, layout) are deliberately not descended into in this PR; a predicate prop could opt them in later (this answers the suggestion in #8178 to generalize, without touching table or layout structure here). Horizontal drag-to-indent and multi-block drag are out of scope.

Closes #7630

Changeset

.changeset/draggable-block-nested-items.md (@lexical/react: patch, [feature], with a Verify: paragraph).

Test plan

Commands run locally (Node 22.22.0, pnpm 11.24.0, Chromium via Playwright):

  • pnpm run tsc and pnpm run tsc-test: clean.
  • pnpm run lint and pnpm run prettier: clean.
  • pnpm run check-changesets: passed.
  • npx vitest --project unit --no-watch packages/lexical-react/src/__tests__/unit/LexicalDraggableBlockPlugin.test.tsx: 2 passed (existing tests).
  • npx vitest --project browser --no-watch packages/lexical-react/src/__tests__/browser/LexicalDraggableBlockPlugin.test.tsx: 7 passed (new; needs a real layout engine, jsdom reports zero rects).
  • pnpm run start + E2E_BROWSER=chromium npx playwright test --project=chromium packages/lexical-playground/__tests__/e2e/DraggableBlock.spec.mjs: 9 passed (5 existing, 4 new). Firefox and WebKit were not run here (the existing drag tests in this spec are already skipped on Firefox).

Before

Hovering any list item shows the handle for the whole list; the only drag unit is the <ul>/<ol>.

After

New browser-mode tests (packages/lexical-react/src/__tests__/browser/LexicalDraggableBlockPlugin.test.tsx):

  • the handle targets the list item under the pointer, at any depth
  • reorders items within a list
  • reorders items within a nested list
  • moves an item between lists of different depth and type
  • an item dropped outside a list gets a list of the same type
  • a paragraph dropped on a list item lands next to the whole list
  • the drop line is indented to the target item

New e2e tests in packages/lexical-playground/__tests__/e2e/DraggableBlock.spec.mjs, driven through the real handle in the playground:

  • List item one can be dragged below list item two
  • A nested list item can be dragged below its sibling
  • A list item dragged below a paragraph gets its own list
  • A paragraph dragged onto a list item lands below the list

🤖 Generated with Claude Code

@vercel

vercel Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
lexical Ready Ready Preview Nov 1, 2026 12:59pm UTC
lexical-playground Ready Ready Preview Nov 1, 2026 12:59pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Nov 1, 2026
…t items

The drag handle of DraggableBlockPlugin_EXPERIMENTAL now targets the block
under the pointer at any depth instead of only top-level blocks, so a list
item (in a nested list too) gets its own handle. Descent stops at shadow
roots (tables, layout containers, collapsibles), which stay one draggable
unit, and the margin band of a list still drags the whole list.

Drop targeting is aware of the dragged node: a list item can land between
the items of any list (any type, any depth) or outside a list, where it is
wrapped in a copy of the list it came from so its type is kept; a block that
is not a list item dragged over a list targets the enclosing list, so the
drop line only appears where the drop will happen. The drop line starts at
the target's left edge to show the nesting depth.

Browser-mode tests cover handle targeting, reordering in flat and nested
lists, moves between lists, moves out of a list and the escalation of a
paragraph to the whole list; the playground e2e spec covers the same flows
through the real handle.

Closes meta#7630

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@meta-code-review

meta-code-review Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
📝 Draft — not reviewed automatically (for maintainers only) Nov 3, 2026, 5:41 AM UTC

This branch was successfully deployed

2 active deployments
Preview – lexical — 3e4c7e5f Deployed Nov 1, 2026 by vercel[bot]
Preview – lexical-playground — 3e4c7e5f Deployed Nov 1, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

1 participant