dnd icon indicating copy to clipboard operation
dnd copied to clipboard

Draggable items are positioned incorrectly when parent of Droppable has `transform`

Open ghost opened this issue 2 years ago • 8 comments
trafficstars

Expected behavior

If I place a Droppable and Draggables in a div and add a transform to that div (e.g. transform="translateX(0%) translateY(0px) translateZ(0px)"), Draggables should follow my mouse as I drag them.

Actual behavior

Items being dragged positioned way to the right. I originally thought they were disappearing when dragging, but if I drag my cursor way to the left they'll appear on the right side of the screen.

Steps to reproduce

  1. https://codesandbox.io/s/reverent-lucy-eibotf
  2. Drag an item around and it will disappear/be placed way to the right.

Suggested solution?

Not sure.

What version of React are you using?

18.2.0

What version of @hello-pangea/dnd are you running?

16.2.0

What browser are you using?

The issue appears in Firefox and Safari. I haven't tested Chrome.

Demo

https://codesandbox.io/s/reverent-lucy-eibotf

ghost avatar May 22 '23 21:05 ghost

I have the same issue. When it's inside Modal (I'm using Mantine Modal), it's not positioned properly when it is dragging.

coolneo4u avatar Jun 01 '23 18:06 coolneo4u

@coolneo4u @72403 Had the same issue. Went through the docs and found this:

Warning: position: fixed

@hello-pangea/dnd uses position: fixed to position the dragging element. This is quite robust and allows for you to have position: relative | absolute | fixed parents. However, unfortunately position:fixed is impacted by transform (such as transform: rotate(10deg);). This means that if you have a transform: * on one of the parents of a <Draggable /> then the positioning logic will be incorrect while dragging. Lame! For most consumers this will not be an issue. To get around this you can reparent your <Draggable />. We do not enable this functionality by default as it has performance problems.

This fixed the issue for me.

CodrinSocol avatar Aug 15 '23 16:08 CodrinSocol

Perhaps we could do this when drag start?

const TRANSFORM_DEFAULT_VALUES = ['none', 'initial', 'inherit', 'unset'];
const WILL_CHANGE_DEFAULT_VALUES = ['auto', 'initial', 'inherit', 'unset'];

export function getTransformedParentCoords(element: Element) {
  let parentNode = element.parentNode;
  while (parentNode !== null) {
    if (isHTMLElement(parentNode)) {
      const { transform, willChange } = getComputedStyle(parentNode);
      if (
        !TRANSFORM_DEFAULT_VALUES.includes(transform) ||
        !WILL_CHANGE_DEFAULT_VALUES.includes(willChange)
      ) {
        const { x, y } = parentNode.getBoundingClientRect();
        return { x, y };
      }
    }
    parentNode = parentNode.parentNode;
  }
  return { x: 0, y: 0 };
}

export const getBoundingClientRect = (element: Element, isFixedStrategy = false) => {
  const clientRect = element.getBoundingClientRect();

  let offsetX = 0;
  let offsetY = 0;
  if (isFixedStrategy) {
    const { x, y } = getTransformedParentCoords(element);
    offsetX = x;
    offsetY = y;
  }

  return DOMRect.fromRect({
    x: clientRect.left - offsetX,
    y: clientRect.top - offsetY,
    width: clientRect.width,
    height: clientRect.height,
  });
};

inomdzhon avatar Nov 15 '23 13:11 inomdzhon

Perhaps we could do this when drag start?

const TRANSFORM_DEFAULT_VALUES = ['none', 'initial', 'inherit', 'unset'];
const WILL_CHANGE_DEFAULT_VALUES = ['auto', 'initial', 'inherit', 'unset'];

export function getTransformedParentCoords(element: Element) {
  let parentNode = element.parentNode;
  while (parentNode !== null) {
    if (isHTMLElement(parentNode)) {
      const { transform, willChange } = getComputedStyle(parentNode);
      if (
        !TRANSFORM_DEFAULT_VALUES.includes(transform) ||
        !WILL_CHANGE_DEFAULT_VALUES.includes(willChange)
      ) {
        const { x, y } = parentNode.getBoundingClientRect();
        return { x, y };
      }
    }
    parentNode = parentNode.parentNode;
  }
  return { x: 0, y: 0 };
}

export const getBoundingClientRect = (element: Element, isFixedStrategy = false) => {
  const clientRect = element.getBoundingClientRect();

  let offsetX = 0;
  let offsetY = 0;
  if (isFixedStrategy) {
    const { x, y } = getTransformedParentCoords(element);
    offsetX = x;
    offsetY = y;
  }

  return DOMRect.fromRect({
    x: clientRect.left - offsetX,
    y: clientRect.top - offsetY,
    width: clientRect.width,
    height: clientRect.height,
  });
};

I'm trying to understand this. So we would call getBoundingClientRect in the onDragStart of the DragDropContext passing it an element? What specifically gets passed as the element?

mdodge-ecgrow avatar Mar 05 '24 16:03 mdodge-ecgrow

I also have this problem while doing dnd inside a radix Dropdown menu. A temporary fix was to overwrite top and left style properties of Draggable to unset. But this still presents problems if you try to drag an item from one list to the other in a vertical placement. What a headache.

acalinica avatar Apr 19 '24 06:04 acalinica

Perhaps we could do this when drag start?

const TRANSFORM_DEFAULT_VALUES = ['none', 'initial', 'inherit', 'unset'];
const WILL_CHANGE_DEFAULT_VALUES = ['auto', 'initial', 'inherit', 'unset'];

export function getTransformedParentCoords(element: Element) {
  let parentNode = element.parentNode;
  while (parentNode !== null) {
    if (isHTMLElement(parentNode)) {
      const { transform, willChange } = getComputedStyle(parentNode);
      if (
        !TRANSFORM_DEFAULT_VALUES.includes(transform) ||
        !WILL_CHANGE_DEFAULT_VALUES.includes(willChange)
      ) {
        const { x, y } = parentNode.getBoundingClientRect();
        return { x, y };
      }
    }
    parentNode = parentNode.parentNode;
  }
  return { x: 0, y: 0 };
}

export const getBoundingClientRect = (element: Element, isFixedStrategy = false) => {
  const clientRect = element.getBoundingClientRect();

  let offsetX = 0;
  let offsetY = 0;
  if (isFixedStrategy) {
    const { x, y } = getTransformedParentCoords(element);
    offsetX = x;
    offsetY = y;
  }

  return DOMRect.fromRect({
    x: clientRect.left - offsetX,
    y: clientRect.top - offsetY,
    width: clientRect.width,
    height: clientRect.height,
  });
};

I'm trying to understand this. So we would call getBoundingClientRect in the onDragStart of the DragDropContext passing it an element? What specifically gets passed as the element?

We should pass dragging element and call getBoundingClientRect when change dragging element coords.

inomdzhon avatar Apr 22 '24 08:04 inomdzhon