clauderic / dnd-kit

The modern, lightweight, performant, accessible and extensible drag & drop toolkit for React.
http://dndkit.com
MIT License
12.87k stars 640 forks source link

RectIntersection is not working with draggable items inside a scrollable container #43

Closed jeserodz closed 2 years ago

jeserodz commented 3 years ago

The draggable items that are scrolled are not calculating the intersectionRatio correctly.

Please, see this code sandbox for an example: https://codesandbox.io/s/react-dnd-grid-0ylzl?file=/src/App.js

NOTE: Try dragging the last draggable item into one droppable area.

clauderic commented 3 years ago

Hey @jeserodz, seems like a legit bug, thanks for the report!

clauderic commented 3 years ago

For the time being, you can use the closestCenter or closestCorners collision detection algorithms as a workaround, as this is a bug that will take a bit of time to fix.

xReaven commented 3 years ago

For the time being, you can use the closestCenter or closestCorners collision detection algorithms as a workaround, as this is a bug that will take a bit of time to fix.

In my issue (#73 you closed for duplicate), FYI, I do reproduce the bug even with others collisions algorithm. closestCorners / closestCenter are doing the same.

kwiss commented 3 years ago

is the https://github.com/clauderic/dnd-kit/pull/54 usable ? i have the same kind of bug after scrolling a long list

bryjch commented 3 years ago

Not sure if this bug is the cause for the following as well: https://codesandbox.io/s/confident-pike-ofooi

Note: this doesn't use @dnd-kit/sortable, as it's not necessary for my particular use case.

https://user-images.githubusercontent.com/9291779/117454064-4ac08780-af78-11eb-8ce6-3ba1735d5222.mp4

Shajansheriff commented 3 years ago

First of all thanks for building such a developer friendly DnD library @clauderic

We are building a kanban board and our first choice naturally went towards using react-beautiful-dnd and after trying out that, we end up facing this issue https://github.com/atlassian/react-beautiful-dnd/issues/131 and then we have to remove the package as it is not gonna be solved anytime soon.

After searching for other libraries, we found dnd-kit to be promising. Especially after seeing this example. So we replaced react-beautiful-dnd with dnd-kit and things were going great until we hit this issue https://github.com/clauderic/dnd-kit/issues/73

I saw you are working on https://github.com/clauderic/dnd-kit/pull/54 Will this solve this issue?

I am open to discuss this and support it further with your help and guidance.

cmacdonnacha commented 3 years ago

Hey @Shajansheriff , that example only works because height is fixed right?

greg-the-dev commented 3 years ago

+1 Having a huge pain trying to find a workaround for this bug. Here's the video example, sorta duplicating the initial record, but you may find a couple of additional things.

As a solution, (or at least a temporary solution), it'd be just awesome to be able to remove certain scrollable ancestors from position calculation. Obviously, intersection is calculated using a concatenation of body scrollTop and the left panel scrollable block scrollTop. I don't need left scrollable block to be scrolled at all here, would be great to just remove it from either calculations, and scrollable items as well

https://user-images.githubusercontent.com/71796791/123527550-5f540d00-d6e9-11eb-97f0-8e6ae5e6c8e3.mp4

sanjevirau commented 3 years ago

+1 @clauderic Thank you for this incredible library!

I'm facing this exact bug today with the virtualized scrollable container using react-window library. Just informing this bug also occurs in the virtualized containers.

For now, the workaround I could think of is locating the x and y of my Droppable and act accordingly when I pull the DragOverlay element. Not really an efficient workaround, but will update if it works 🤞🏼

Hexiota commented 3 years ago

@clauderic like others have said, thank you for making this amazing dnd library!

We're running into this issue, are there any updates on PR 54 or any accepted workarounds in the meantime? Thank you!

mmehdinasiri commented 3 years ago

Thanks for your great Lib, I have the same problem and this is strange for me how this example Link is working.

ranbena commented 3 years ago

Thanks for your great Lib, I have the same problem and this is strange for me how this example Link is working.

Probably cause it's not using the default collision detection https://github.com/clauderic/dnd-kit/issues/43#issuecomment-757338784.

robstarbuck commented 3 years ago

Hi I wonder if there's an update on this ticket? The branch at #54 seems to have gone stale. I have a workaround with a different collision detector which uses the current active item rather than the collisionRect which seems to work for the meantime. Obviously not something we want to keep in our codebase however. Thanks.

import {
  Active,
  CollisionDetection,
  LayoutRect,
  UniqueIdentifier,
} from "@dnd-kit/core";

/**
 * Returns the intersecting rectangle area between two rectangles
 */
function getIntersectionRatio(entry: LayoutRect, active: Active): number {
  const {
    top: currentTop = 0,
    left: currentLeft = 0,
    width: currentWidth = 0,
    height: currentHeight = 0,
  } = active.rect.current.translated ?? {};

  const top = Math.max(currentTop, entry.offsetTop);
  const left = Math.max(currentLeft, entry.offsetLeft);
  const right = Math.min(
    currentLeft + currentWidth,
    entry.offsetLeft + entry.width
  );
  const bottom = Math.min(
    currentTop + currentHeight,
    entry.offsetTop + entry.height
  );
  const width = right - left;
  const height = bottom - top;

  if (left < right && top < bottom) {
    const targetArea = currentWidth * currentHeight;
    const entryArea = entry.width * entry.height;
    const intersectionArea = width * height;
    const intersectionRatio =
      intersectionArea / (targetArea + entryArea - intersectionArea);

    return Number(intersectionRatio.toFixed(4));
  }

  // Rectangles do not overlap, or overlap has an area of zero (edge/corner overlap)
  return 0;
}

/**
 * Returns the rectangle that has the greatest intersection area with a given
 * rectangle in an array of rectangles.
 */
export const activeRectIntersection: CollisionDetection = ({
  active,
  droppableContainers,
}) => {
  let maxIntersectionRatio = 0;
  let maxIntersectingDroppableContainer: UniqueIdentifier | null = null;

  for (const droppableContainer of droppableContainers) {
    const {
      rect: { current: rect },
    } = droppableContainer;

    if (rect) {
      const intersectionRatio = getIntersectionRatio(rect, active);

      if (intersectionRatio > maxIntersectionRatio) {
        maxIntersectionRatio = intersectionRatio;
        maxIntersectingDroppableContainer = droppableContainer.id;
      }
    }
  }

  return maxIntersectingDroppableContainer;
};
nunibaranes commented 3 years ago

Hi all, We are building an application with two lists and are wrapping each of them with a virtual list. I had had the same issue and tried to use closestCenter or closestCorners collision detection algorithms but got the same results.

@robstarbuck's comment helped me to find a workaround that works perfectly for my use case:

  1. Set a ref on the draggable element (rendered inside <DragOverlay>).
  2. Wrap the closestCenter or closestCorners with a callback, e.g.:

    (entries: RectEntry[], target: ViewRect) => {
    // Use the default dnd-kit callback when ref doesn't exist.
    if (!draggableElement?.current) return closestCenter(entries, target);
    
    // Use all values from `getBoundingClientRect` and pass them into a new object as type `ViewRect`.
    const { width, height, left, right, top, bottom, x, y } = draggableElement?.current?.getBoundingClientRect();
    const domRectTarget = { width, height, left, right, top, bottom, offsetLeft: x, offsetTop: y };
    
    // Use new collision detection algrorithm.
    return rectIntersection(entries, domRectTarget);
    }
  3. Use the following as the helper functions used in the example above (derived from dnd-kit rectIntersection.ts and modified to accommodate our implementation):
    
    import type { LayoutRect, UniqueIdentifier, RectEntry, ViewRect } from '@dnd-kit/core';

/**

/**

bobolittlebear commented 2 years ago

Hi I wonder if there's an update on this ticket? The branch at #54 seems to have gone stale. I have a workaround with a different collision detector which uses the current active item rather than the collisionRect which seems to work for the meantime. Obviously not something we want to keep in our codebase however. Thanks.

import {
  Active,
  CollisionDetection,
  LayoutRect,
  UniqueIdentifier,
} from "@dnd-kit/core";

/**
 * Returns the intersecting rectangle area between two rectangles
 */
function getIntersectionRatio(entry: LayoutRect, active: Active): number {
  const {
    top: currentTop = 0,
    left: currentLeft = 0,
    width: currentWidth = 0,
    height: currentHeight = 0,
  } = active.rect.current.translated ?? {};

  const top = Math.max(currentTop, entry.offsetTop);
  const left = Math.max(currentLeft, entry.offsetLeft);
  const right = Math.min(
    currentLeft + currentWidth,
    entry.offsetLeft + entry.width
  );
  const bottom = Math.min(
    currentTop + currentHeight,
    entry.offsetTop + entry.height
  );
  const width = right - left;
  const height = bottom - top;

  if (left < right && top < bottom) {
    const targetArea = currentWidth * currentHeight;
    const entryArea = entry.width * entry.height;
    const intersectionArea = width * height;
    const intersectionRatio =
      intersectionArea / (targetArea + entryArea - intersectionArea);

    return Number(intersectionRatio.toFixed(4));
  }

  // Rectangles do not overlap, or overlap has an area of zero (edge/corner overlap)
  return 0;
}

/**
 * Returns the rectangle that has the greatest intersection area with a given
 * rectangle in an array of rectangles.
 */
export const activeRectIntersection: CollisionDetection = ({
  active,
  droppableContainers,
}) => {
  let maxIntersectionRatio = 0;
  let maxIntersectingDroppableContainer: UniqueIdentifier | null = null;

  for (const droppableContainer of droppableContainers) {
    const {
      rect: { current: rect },
    } = droppableContainer;

    if (rect) {
      const intersectionRatio = getIntersectionRatio(rect, active);

      if (intersectionRatio > maxIntersectionRatio) {
        maxIntersectionRatio = intersectionRatio;
        maxIntersectingDroppableContainer = droppableContainer.id;
      }
    }
  }

  return maxIntersectingDroppableContainer;
};

Thanks!

AlexandruDraghia commented 2 years ago

Hi. I used @robstarbuck's algorithm but my active lost its offset in a long list while scrolling and then I did this :

 const activeRectIntersection = ({
    active,
    collisionRect,
    droppableContainers,
  }, scrollableContainersKeys) => {
    let maxIntersectionRatio = 0;
    let maxIntersectingDroppableContainer = null;
    for (const droppableContainer of droppableContainers) {
      const {
        rect: { current: rect },
      } = droppableContainer;

      if (rect) {
        let intersectionRatio = 0
        if(scrollableContainersKeys.includes(droppableContainer.id)) intersectionRatio = getIntersectionRatio(rect, active?.rect?.current?.translated || {})
        else  intersectionRatio = getIntersectionRatio(rect, collisionRect);
        if (intersectionRatio > maxIntersectionRatio) {
          maxIntersectionRatio = intersectionRatio;
          maxIntersectingDroppableContainer = droppableContainer.id;
        }
      }
    }
    return maxIntersectingDroppableContainer;
  };

I use active to calculate the intersection ratio with the scrollable container and the collisionRect with the other elements. This works for my use case. I hope it helps

CodexpathCommunity commented 1 year ago

I'm having the same issue. how can I fix drag issue on scrollable container?

bulatte commented 11 months ago

I made it work with document.elementsFromPoint. It gets all nodes located on the mouse position and works within scrollable containers as well. Not sure if it's the best solution for anyone performance-wise, but worked for me.

I've added a common className and the id to each droppable zone to be able to find it in the results array.

import {
  // ...
  rectIntersection,
  CollisionDetection,
} from '@dnd-kit/core';

// ...

const MainComponent = () => {
  const mousePosition = useRef({x: 0, y: 0});

  useEffect(() => {
    const handleMouseMove = (event: MouseEvent) => {
      mousePosition.current = {x: event.clientX, y: event.clientY};
    };
    window.addEventListener('mousemove', handleMouseMove);
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []);

  const collisionDetection: CollisionDetection = (args) => {
    const nodesAtPosition = document.elementsFromPoint(
      mousePosition.current.x,
      mousePosition.current.y,
    );

    const droppableId = nodesAtPosition.find((node) =>
      node.classList.contains('droppable-zone'),
    )?.id;

    if (droppableId) {
      return droppableId;
    }

    // fallback to default collision detection algorithm
    return rectIntersection(args);
  };

  return (
    <DndContext
      // ...
      collisionDetection={collisionDetection}
    >
      {/* ... */}
    </DndContext>
  )
};