Skip to content

DraggableList: no way to opt out of cross-list drops #1554

@misama-ct

Description

@misama-ct

Summary

DraggableList.Root accepts drops from every other DraggableList in the page, with no public API to opt out. Consumers rendering two (or more) lists that should be independent have no way to disable cross-list dropping.

cc @ByronDWall

Where it's hardcoded

In packages/nimbus/src/components/draggable-list/components/draggable-list.root.tsx:

  • A single module-level constant DRAGGABLE_LIST_DATA_FORMAT = "nimbus-draggable-list-item" is used as both the drag payload type and acceptedDragTypes — so every Nimbus DraggableList implicitly accepts every other Nimbus DraggableList.
  • getDropOperation: () => "move" is hardcoded — there's no path to return "cancel" for foreign sources.
  • The component constructs its own dragAndDropHooks internally and assigns it after spreading consumer props, so a consumer-supplied dragAndDropHooks is silently overridden.
  • DraggableListRootProps exposes none of these knobs.

The CrossListDragAndDrop story confirms cross-list movement is the intended default — but there's no way to opt out.

Repro

<>
  <DraggableList.Root aria-label="A" items={a} onUpdateItems={setA} />
  <DraggableList.Root aria-label="B" items={b} onUpdateItems={setB} />
</>

Items can be dragged from A into B and vice versa, with no consumer-controlled way to prevent it.

Proposed API

Any one of the following would close the gap; my preference is the first:

  1. dragNamespace?: string prop — when set, the list uses \nimbus-draggable-list-item:${dragNamespace}`` as its drag type and only accepts that exact type. Lists with different namespaces won't accept each other's items. Backward-compatible: unset preserves today's behavior.
  2. acceptedDragTypes?: string[] + a way to set the outgoing drag type — direct passthrough to useDragAndDrop. More flexible, more surface area.
  3. getDropOperation?: (...) => DropOperation override — lets consumers return "cancel" based on source/target inspection.

(1) covers the "two independent lists on one page" case with one prop and zero new concepts for consumers. (2)/(3) are useful for more advanced cases (custom drag sources, conditional acceptance) and could come later.

Workaround today

There isn't a clean one. App-level revert in onUpdateItems (detect foreign items, restore prior state) works but flashes the wrong state briefly. Forking the component is the only way to get correct UX.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions