dnd-kit icon indicating copy to clipboard operation
dnd-kit copied to clipboard

@dnd-kit/react source and target in sortable list always the same in onDragEnd handler

Open rossyman opened this issue 1 year ago • 22 comments

When composing a multiple sortable list and handling the onDragEnd event, both operation.source and operation.target are always set to the currently dragged item when re-arranging the order of a parent list (i.e.: moving the position of a kanban column).

When dragging the column, the target is only ever the correct one when you are dragging directly over the top of the target element itself.

When @dnd-kit/react automatically re-positions other columns in the sortable list to "preview" where the dragged column will be dropped, it causes the target to be subsequently set to the currently dragged column when there's no column "beneath" it besides the "ghost column".

This makes it near enough impossible to trigger a change such as updating a column's position in an external DB:

  const handleDragEnd = (cancelled: boolean, operation: KanbanDragEvent<TGroup, TItem>) => {
    if (!cancelled && operation.source && operation.target) {
      const { source, target } = operation
      switch (source.type) {
        case 'column':
          props.onGroupReorder(source.data, target.data)
          break
        case 'item':
          props.onItemMove(source.data, target.data)
          break
      }
    }
  }

Reproduction

I've created a reproduction of this issue by forking a minimal version of the multiple sortable lists example on CodeSandbox. View the reproduction on CodeSandbox

rossyman avatar Dec 12 '24 11:12 rossyman

I'm facing the same issue even in a Sortable list.

RubenZx avatar Dec 12 '24 15:12 RubenZx

This is because the DOM gets optimistically updated by @dnd-kit by default. If you would like to manage DOM and state manually, you can call event.preventDefault() in onDragOver

<DragDropProvider
  onDragOver={(event) => {
    event.preventDefault();
  }}

clauderic avatar Dec 12 '24 15:12 clauderic

Even if dnd-kit automatically handles the transition optimistically, how do we access the resulting index within onDragEnd @clauderic? My use-case is essentially that we need to retrieve the new index and send it to the B/E for persistence.

One of the issues I continue to run into is that if I track the last dragged over item via onDragOver, and move an item in position N, to N+1 and then back to N, all without dropping the item, the last dragged over item will still be N+1, and therefore I cannot accurately determine what position the resulting drop occurred at.

rossyman avatar Dec 12 '24 16:12 rossyman

There could potentially be a solution to this. If I clone the columns and items arrays, then have dnd-kit optimistically update the cloned arrays within onDragOver. Then inside of onDragEnd I can determine the new position based on the cloned array, or roll back if event.cancelled.

rossyman avatar Dec 12 '24 16:12 rossyman

You can read the index and group on start and end:

event.source.sortable.index and event.source.sortable.group

clauderic avatar Dec 12 '24 16:12 clauderic

Makes sense @clauderic, thanks for clarifying, I'll post my updated implementation here shortly incase anyone else experiences something similar.

One last thing though, what would the correct generics be for DragOperation<T, U> then when using Sortable? From what I can see Sortable doesn't actually extend Draggable which is what the generics expect, rather it's a container that houses both a Draggable and a Droppable element. However, source and target appear to both expect Draggable or Droppable as their value.

rossyman avatar Dec 12 '24 16:12 rossyman

You can import the isSortable type guard from @dnd-kit/dom/sortable

clauderic avatar Dec 12 '24 16:12 clauderic

The isSortable type-guard is not publicly exposed by the package, but using a type-guard would be ideal.

rossyman avatar Dec 12 '24 16:12 rossyman

It is exported: https://github.com/clauderic/dnd-kit/blob/experimental/packages/dom/src/sortable/index.ts#L3

You have to import it from @dnd-kit/dom/sortable but I will make a note to re-export it from @dnd-kit/react/sortable

clauderic avatar Dec 12 '24 16:12 clauderic

Maybe I'm just being obtuse here, but TS doesn't seem to think it's exported, and when looking inside of the built JS files in v0.0.5, I cannot see the function being re-exported as part of sortable.js

Usage

Screenshot 2024-12-12 at 17 04 04

sortable.js

Screenshot 2024-12-12 at 17 05 14 Screenshot 2024-12-12 at 17 06 18

From what I can see, it appears as though the type-guard is being inlined during the build and is not actually being exported by the API of @dnd-kit/dom/sortable

rossyman avatar Dec 12 '24 17:12 rossyman

Oh, it must only exported in the nightly builds, I have not merged 0.0.6 yet.

Try installing one of the nightly builds, the latest one is 0.0.6-beta-20241204184550

clauderic avatar Dec 12 '24 17:12 clauderic

Ah yes nice one, it's present in the nightly build 🚀 May also make sense to export SortableDraggable and SortableDroppable for people to use in their DragOperation generics on custom handlers.

rossyman avatar Dec 12 '24 17:12 rossyman

Related to this, I was trying to import the DragEndEvent to create my own handler but I cannot find its export anywhere. This type is declared in next docs:

Image

What is the correct way to import/create it? Since I couldn't import it, I have create a type as follows:

export type DragEndEvent = {
  operation: DragOperation<Draggable<Data>, Droppable<Data>>;
  canceled: boolean;
  suspend(): {
    resume(): void;
    abort(): void;
  };
};

But it looks like the isSortable guard doesn't have the same type for its parameters...

Image

I'm importing isSortable from @dnd-kit/dom/sortable since is not exported from @dnd-kit/react/sortable v0.0.8.

Any help? Thank you!

RubenZx avatar Feb 05 '25 10:02 RubenZx

(TL;DR: Apparently @dnd-kit/react is way too new to use.)

Even though evt.operation.source.sortable appears when running...

Image

It's also not public:

Image

Even worse, I just tried inspecting source.sortable.index and target.sortable.index and they're still the same. So, problem not solved. Is there actually a way to track the order of items as they are reordered? This seems pretty mission critical.

p.s. This is on v0.0.9

CodeSmith32 avatar Mar 08 '25 00:03 CodeSmith32

I also have this problem..

theophilhenry avatar Mar 12 '25 06:03 theophilhenry

I needed nested draggable/droppable areas. Since this is broken, I reverted back to the old @dnd-kit/core / @dnd-kit/utilities packages, re-developed my code to stop using @dnd-kit/sortable, and used the raw elements instead - <DndContext>, useDndContext, useDraggable, useDroppable.

I followed this helpful discussion pretty far (not even all the way), and was able to get it working fairly decently.

TL;DR: Don't use @dnd-kit/react. It's still in beta. If you need nested dnd, you'll probably need to implement it manually without useSortable. But the raw pieces still provide a decent / fairly intuitive interface to do so.

CodeSmith32 avatar Mar 12 '25 15:03 CodeSmith32

Will this be fixed at any time soon? or any progress on @dnd-kit/react

Duckinm avatar May 16 '25 09:05 Duckinm

Will this be fixed at any time soon? or any progress on @dnd-kit/react

https://github.com/clauderic/dnd-kit/issues/1664

li1234yun avatar Jun 07 '25 01:06 li1234yun

First, Thanks @clauderic I have a solution, maybe you can refer it:

const [mapping, setMapping] = useState<string[]>([]) 
const [mappingSnapshot, setMappingSnapshot] = useState<string[]>([]) 

...


const handleBeforeDragStart: DragDropEvents['beforedragstart'] = (e) => {
  console.log('before drag start:', e)

  // save snapshot
  setMappingSnapshot(mapping)
}

const handleDragOver: DragDropEvents['dragover'] = (e) => {
  console.log('drag over event:', e)

  setMapping((mapping) => move(mapping, e))
}

const handleDragEnd: DragDropEvents['dragend'] = (e) => {
  console.log('drag end event:', e)

  if (e.canceled) {
    setMapping(mappingSnapshot)
    return
  }

  const sourceId = e.operation.source?.id
  const targetId = e.operation.target?.id
  if (!sourceId || !targetId) {
    setMapping(mappingSnapshot) // restore
    return
  }


  const sourceItem = siteMap[sourceId]
  const originIndex = mappingSnapshot.indexOf(sourceId as string) // !!! Find origin index from snapshot
  const currentIndex = mapping.indexOf(sourceId as string)
  if (!sourceItem || originIndex == -1 || currentIndex == -1) {
    return
  }

 if (originIndex === currentIndx) {
    // no change position
    return
 }
 
  const positionId = mappingSnapshot[currentIndex]
  const positionItem = siteMap[positionId]

...
}

li1234yun avatar Jun 07 '25 03:06 li1234yun

Because of this issue, I forced the DragDropProvider to use the key based on the list ID in the database. It's a method I really hate, but I can't seem to find a solution. A solution would be incredibly helpful.

MerryGoorm avatar Sep 04 '25 01:09 MerryGoorm

import { arrayMove } from '@dnd-kit/helpers'

const handleDragEnd = (event, manager) => {
    const { operation, canceled } = event
    const { source, target } = operation

    if (canceled) {
      console.log(`Cancelled dragging ${source?.id}`)
      return
    }

    if (target && isSortable(source)) {
      const newIndex = source.sortable.index
      const oldIndex = source.sortable.initialIndex
      if (oldIndex !== newIndex) {
        const newList = arrayMove(arrSource, oldIndex, newIndex)
        setArrSource(newList)
      }
    }

Should be simple enough here. Tested with TS for the manual control of the array source using onDragEnd. I still keep the optimistic update enabled.

allicanseenow avatar Sep 05 '25 13:09 allicanseenow

Sometimes, when the source array used to render the sortable list is updated after the onDragEnd, the next sorting behavior with onDragEnd now will have source.sortable.index === source.sortable.initialIndex but target will become different from source. I think this is very likely a bug

allicanseenow avatar Sep 11 '25 03:09 allicanseenow