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

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

Closed
jeserodz opened this issue Jan 9, 2021 · 18 comments · Fixed by #518
Closed
Labels
bug Something isn't working sponsored This issue is a high-priority sponsored issue

Comments

@jeserodz
Copy link

jeserodz commented Jan 9, 2021

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 clauderic added the bug Something isn't working label Jan 9, 2021
@clauderic
Copy link
Owner

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

@clauderic
Copy link
Owner

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
Copy link

xReaven commented Jan 29, 2021

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
Copy link

kwiss commented Feb 19, 2021

is the #54 usable ? i have the same kind of bug after scrolling a long list

@bryjch
Copy link

bryjch commented May 7, 2021

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

  • Multiple containers with grids of Droppable+Draggable squares
  • Dragging within own container: auto scrolling works fine
  • Dragging between containers: auto scrolls "over" container, then starts scrolling "from" container
  • onDragOver gets called repeatedly when the above occurs

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

dnd-kit-scroll-bug.mp4

@Shajansheriff
Copy link
Contributor

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 atlassian/react-beautiful-dnd#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 #73

I saw you are working on #54 Will this solve this issue?

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

@cmacdonnacha
Copy link

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

@greg-the-dev
Copy link

+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

2021-06-27-01-38-56_rMVI6sw8.mp4

@sanjevirau
Copy link

sanjevirau commented Jun 28, 2021

+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
Copy link

Hexiota commented Jul 8, 2021

@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
Copy link

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

@ranbena
Copy link
Contributor

ranbena commented Sep 30, 2021

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 #43 (comment).

@robstarbuck
Copy link

robstarbuck commented Oct 6, 2021

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
Copy link

nunibaranes commented Oct 11, 2021

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);
}
  1. 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';

/**
 * Returns the intersecting rectangle area between two rectangles
 */
function getIntersectionRatio(entry: LayoutRect, target: ViewRect): number {
  const top = Math.max(target.top, entry.offsetTop);
  const left = Math.max(target.left, entry.offsetLeft);
  const right = Math.min(target.left + target.width, entry.offsetLeft + entry.width);
  const bottom = Math.min(target.top + target.height, entry.offsetTop + entry.height);
  const width = right - left;
  const height = bottom - top;

  if (left < right && top < bottom) {
    const targetArea = target.width * target.height;
    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 rectIntersection = (droppableContainers: RectEntry[], collisionRect: ViewRect) => {
  let maxIntersectionRatio = 0;
  let maxIntersectingDroppableContainer: UniqueIdentifier | null = null;

  for (const droppableContainer of droppableContainers) {
    const [id, rect] = droppableContainer;

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

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

  return maxIntersectingDroppableContainer;
};

@bobolittlebear
Copy link

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
Copy link

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
Copy link

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

@bulatte
Copy link

bulatte commented Nov 23, 2023

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>
  )
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working sponsored This issue is a high-priority sponsored issue
Projects
None yet