Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Variable Height Sortable - Inaccurate Dragging to Corresponding Position When Width is Large #1246

Closed
goodjun opened this issue Oct 6, 2023 · 3 comments

Comments

@goodjun
Copy link

goodjun commented Oct 6, 2023

When the width is large, it becomes difficult to accurately drag and drop to the desired position.

In the example below, I am unable to move the element "3" between "1" and "2".

2023-10-06.1.16.54.mov

example: https://codesandbox.io/s/dnd-kit-sortable-issue-9wk6js?file=/src/App.tsx

@klalicki
Copy link

klalicki commented Oct 7, 2023

Hi! I recently worked through a problem similar to this on my own project, and was able to tweak your sandbox to be a bit more user-friendly.

https://codesandbox.io/s/dnd-kit-sortable-issue-forked-s6p4cs?file=/src/App.tsx

There are a couple of changes I made:

adding an onDragOver event handler:

by adding a handler for onDragOver that does the same thing as your onDragEnd handler, the drag and drop will now shuffle the actual items around when you drag, meaning that the 'hole' under your dragged item will always match the size of the dragged item, rather than the item it is displacing (which is default behavior, works fine for same-sized items)

const onDragOver = ({ active, over }: DragOverEvent) => {
    if (active.id !== over?.id) {
      setItems((prev) => {
        const activeIndex = prev.findIndex((i) => i === active.id);
        const overIndex = prev.findIndex((i) => i === over?.id);
        return arrayMove(prev, activeIndex, overIndex);
      });
    }
  };

adding a custom collision algorithm

The default collision algorithm (which is what lets dnd-kit decide whether or not you've dragged something far enough to trigger the onDragOver/onDragEnd handlers) relies on the bounding box of the dragged item overlapping with the bounding box of the target item. I find that this doesn't work as well on lists of differently-sized items, since you'll often end up with a situation where the dragged item overlaps many other items.

I put together a composed collision algorithm, following the pattern shown in the docs. This algorithm first attempts to use 'pointer within', which picks the item that is directly under the cursor. If the cursor is not directly over an item, it uses the closestCorners algorithm to pick the item that is closest. This gives you the ability to continue dragging even if the pointer is not over the items (ie you drag up or down). I believe the closestCorners algorithm is also more friendly if you require keyboard accessibilty, but I'm not 100% sure on that.

const pointerAndClosestCorners = (collisionArgs: {
    active: Active;
    collisionRect: ClientRect;
    droppableRects: RectMap;
    droppableContainers: DroppableContainer[];
    pointerCoordinates: Coordinates | null;
  }) => {
    // First, let's see if there are any collisions with the pointer
    const pointerCollisions = pointerWithin(collisionArgs);

    // Collision detection algorithms return an array of collisions
    if (pointerCollisions.length > 0) {
      return pointerCollisions;
    }

    // If there are no collisions with the pointer, return Closest Corners algorithm
    return closestCorners(collisionArgs);
  };

I hope this was helpful; let me know if this behavior is more in line with what you want!

@goodjun
Copy link
Author

goodjun commented Oct 10, 2023

Hi! I recently worked through a problem similar to this on my own project, and was able to tweak your sandbox to be a bit more user-friendly.

https://codesandbox.io/s/dnd-kit-sortable-issue-forked-s6p4cs?file=/src/App.tsx

There are a couple of changes I made:

adding an onDragOver event handler:

by adding a handler for onDragOver that does the same thing as your onDragEnd handler, the drag and drop will now shuffle the actual items around when you drag, meaning that the 'hole' under your dragged item will always match the size of the dragged item, rather than the item it is displacing (which is default behavior, works fine for same-sized items)

const onDragOver = ({ active, over }: DragOverEvent) => {
    if (active.id !== over?.id) {
      setItems((prev) => {
        const activeIndex = prev.findIndex((i) => i === active.id);
        const overIndex = prev.findIndex((i) => i === over?.id);
        return arrayMove(prev, activeIndex, overIndex);
      });
    }
  };

adding a custom collision algorithm

The default collision algorithm (which is what lets dnd-kit decide whether or not you've dragged something far enough to trigger the onDragOver/onDragEnd handlers) relies on the bounding box of the dragged item overlapping with the bounding box of the target item. I find that this doesn't work as well on lists of differently-sized items, since you'll often end up with a situation where the dragged item overlaps many other items.

I put together a composed collision algorithm, following the pattern shown in the docs. This algorithm first attempts to use 'pointer within', which picks the item that is directly under the cursor. If the cursor is not directly over an item, it uses the closestCorners algorithm to pick the item that is closest. This gives you the ability to continue dragging even if the pointer is not over the items (ie you drag up or down). I believe the closestCorners algorithm is also more friendly if you require keyboard accessibilty, but I'm not 100% sure on that.

const pointerAndClosestCorners = (collisionArgs: {
    active: Active;
    collisionRect: ClientRect;
    droppableRects: RectMap;
    droppableContainers: DroppableContainer[];
    pointerCoordinates: Coordinates | null;
  }) => {
    // First, let's see if there are any collisions with the pointer
    const pointerCollisions = pointerWithin(collisionArgs);

    // Collision detection algorithms return an array of collisions
    if (pointerCollisions.length > 0) {
      return pointerCollisions;
    }

    // If there are no collisions with the pointer, return Closest Corners algorithm
    return closestCorners(collisionArgs);
  };

I hope this was helpful; let me know if this behavior is more in line with what you want!

Thank you so much for resolving my issue! I really appreciate your help.

@goodjun goodjun closed this as completed Oct 10, 2023
@Royce
Copy link

Royce commented Jul 8, 2024

This PR has a collision detection algorithm that solved this issue for me (although my scenario was in the vertical orientation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants