hello-pangea/dnd

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

72403 opened this issue · 6 comments

72403 commented

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

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 @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.

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

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?

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.

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.