react-beautiful-dnd icon indicating copy to clipboard operation
react-beautiful-dnd copied to clipboard

Strange offset while dragging

Open synapse opened this issue 4 years ago • 24 comments

Hey guys I need some help figuring out what's causing this strange behavior while dragging elements in this portal modal component. I'll leave a demo video below. This problem occurs if I grab the elements from the bottom. In the console, there aren't any warnings or errors.

Expected behavior

Actual behavior

Steps to reproduce

Suggested solution?

What version of React are you using?

^16.13.1

What version of react-beautiful-dnd are you running?

Latest

What browser are you using?

Electron 8.2.5

Demo

https://imgur.com/a/Uxyiwgl

synapse avatar Jun 29 '20 07:06 synapse

I have the same problem, in a portal modal as well

TeaBough avatar Jul 11 '20 17:07 TeaBough

I have the same issue with an extra unexpected behavior

To Produce the issue: https://codesandbox.io/s/olk8m320lq

dnd

hamed-farag avatar Aug 27 '20 15:08 hamed-farag

Same issue here. Maybe it's happening because the distant parent of the preview element has its position CSS property set to fixed or absolute and the preview element's style top and left aren't calculated as if they were starting from this parent's position, but rather from the window or body coordinates in the package. And CSS transforms the preview element starting from the parent's position.

max-sym avatar Sep 10 '20 11:09 max-sym

I have the same problem in a fixed div.

camdagr8 avatar Sep 11 '20 17:09 camdagr8

I solved this by mutating the provided.draggableProps.style.left and provided.draggableProps.style.right values when provided.isDragging is true

<Draggable >
      {(provided, snapshot) => {
          if (snapshot.isDragging) {
              const offset = { x: 100, y: 100 }          // your fixed container left/top position
              const x = provided.draggableProps.style.left - offset.x;
              const y = provided.draggableProps.style.top - offset.y;
              provided.draggableProps.style.left = x;
              provided.draggableProps.style.top = y;
           }
           return (
                <div 
                     ref={provided.innerRef}
                     {...provided.draggableProps}
                     {...provided.dragHandleProps}>
                     YOUR ITEM
                </div>
           );
       }}
</Draggable>

camdagr8 avatar Sep 11 '20 18:09 camdagr8

I solved this by mutating the provided.draggableProps.style.left and provided.draggableProps.style.right values when provided.isDragging is true

<Draggable >
      {(provided, snapshot) => {
          if (snapshot.isDragging) {
              const offset = { x: 100, y: 100 }          // your fixed container left/top position
              const x = provided.draggableProps.style.left - offset.x;
              const y = provided.draggableProps.style.top - offset.y;
              provided.draggableProps.style.left = x;
              provided.draggableProps.style.top = y;
           }
           return (
                <div 
                     ref={provided.innerRef}
                     {...provided.draggableProps}
                     {...provided.dragHandleProps}>
                     YOUR ITEM
                </div>
           );
       }}
</Draggable>

Based on this, I changed the offset based in fixed position to offsetLeft,offsetTop and it works fine. Also, I don't know the position of my container (is a modal window the user can drag around the screen) so, at least in my case, I prefer to use this as follows:

<Draggable >
      {(provided, snapshot) => {
          if (snapshot.isDragging) {
              provided.draggableProps.style.left = provided.draggableProps.style.offsetLeft;
              provided.draggableProps.style.top = provided.draggableProps.style.offsetTop;
           }
           return (
                <div 
                     ref={provided.innerRef}
                     {...provided.draggableProps}
                     {...provided.dragHandleProps}>
                     YOUR ITEM
                </div>
           );
       }}
</Draggable>

EDIT:

It seems that provided.draggableProps.style.offsetLeft is not working since its value is undefined. I thought it was working okay because it really works with no warnings, but the value is undefined. Any ideas?

underium avatar Sep 20 '20 10:09 underium

I'm having this same issue, and it only affects non-chromium browsers (Safari, Firefox). The solution that @camdagr8 proposed works great. However if your modal is scrollable, it will completely break. It will work fine if you are at the top of the window, but when you scroll down it will again reposition the draggables incorrectly

Jacob-Roberts avatar Oct 16 '20 15:10 Jacob-Roberts

as found on another thread, I think a better solution is using a portal-- no need to know the offset:

let portal = document.createElement("div");
document.body.appendChild(portal);

function MyDraggableItem_render(props) {
  let result = (
    <div>[...]</div>
  );

  if (props.snapshot.isDragging) {
    return ReactDOM.createPortal(result, portal);
  }
  return result;
}

[credit: @Venryx] https://github.com/atlassian/react-beautiful-dnd/issues/499#issuecomment-471334917

stevesch avatar May 13 '21 00:05 stevesch

If anyone hits this thread, Just want to share my issue and solution. My project was fixed width and height with applied transform: translate(-50%, -50%) as I needed to use top: 50%; left: 50% to have it bang in the middle with fixed dimensions. Obviously, I hit the offset issue, and my solution was to get that particular element offset divided in half (to cover transform)

so this is the solution (inside component):

const viewportNode = document.getElementById('viewport') // container aligned in the middle

// In render part for Draggable
  <Draggable index={Number(idx)} draggableId={String(idx)} key={idx}>
      {(provided, snapshot) => {
          if (snapshot.isDragging) {
              provided.draggableProps.style.left = provided.draggableProps.style.left - (viewportNode.offsetLeft - (viewportNode.offsetWidth/2));
              provided.draggableProps.style.top = provided.draggableProps.style.top - (viewportNode.offsetTop - (viewportNode.offsetHeight/2));
          }
          return ( // rest of your render )
   }}
</Draggable>

Hope it helps if anyone comes across similar challange

michalnag avatar Jul 01 '21 22:07 michalnag

{(provided, snapshot) => { if (snapshot.isDragging) { provided.draggableProps.style.left = provided.draggableProps.style.offsetLeft; provided.draggableProps.style.top = provided.draggableProps.style.offsetTop; } return (
YOUR ITEM
); }}

Incase you experience this error:

TypeError: Cannot assign to read only property 'left' of object '#<Object>'

You change the code to:

<Draggable >
      {(provided, snapshot) => {
          if (snapshot.isDragging) {
              provided.draggableProps.style = {
                              ...provided.draggableProps.style,
                              left: provided.draggableProps.style.offsetLeft,
                              top: provided.draggableProps.style.offsetTop
                            }
           }
           return (
                <div 
                     ref={provided.innerRef}
                     {...provided.draggableProps}
                     {...provided.dragHandleProps}>
                     YOUR ITEM
                </div>
           );
       }}
</Draggable>

It also sorts out the issue of provided.draggableProps.style.offsetLeft being undefined. And this works for me!

karimkkanji avatar Oct 20 '22 20:10 karimkkanji

For anyone who is having hard time implementing Portal with this This example of what portal with draggable item look like (Code only): https://codesandbox.io/s/admiring-joji-rgmve6?file=/src/DraggableTaskItem.jsx

SalehAbuhussein avatar Jan 18 '23 16:01 SalehAbuhussein

Hey guys i manged to fix it quite easily and it works well for me. I wrote css class with important, this made my item drag start on his current position.

apply on draggable div: left: auto !important; top: auto !important;

Pawel1894 avatar Mar 11 '23 16:03 Pawel1894

Thank you very much, I tried it and it worked for me

mohammaddaryooshi avatar Jun 06 '23 08:06 mohammaddaryooshi

@Pawel1894 's solution worked for me until I needed to contain the draggables in scrollable divs with a fixed height.

I ended up fixing this by, instead of passing top: 'auto !important' into the component's style, doing this:

  • get a ref to the component that has the offset within your portal
  • pass that ref to your Draggable item
  • calculate the offset const offset = modalRef.current?.getBoundingClientRect().top ?? 0
  • conditionally apply the style when dragging
<Draggable
  draggableId={myId}
  index={index}
>
  {(provided, snapshot) => {
    const offset = modalRef.current?.getBoundingClientRect().top ?? 0
    return (
      <div
        className="border border-gray-200 rounded-lg p-1.5 flex-1 bg-white"
        ref={provided.innerRef}
        {...provided.draggableProps}
        {...provided.dragHandleProps}
        style={{
          ...provided.draggableProps.style,
          left: 'auto !important',
          top: snapshot.isDragging
            ? provided.draggableProps.style?.top - offset
            : 'auto !important',
        }}
          >

modalRef is a ref for the div of the modal. Specifically it should be the div of the modal that sets the position the relative

jessebrennan avatar Jul 26 '23 21:07 jessebrennan

This works for me, I used querySelector to get the id of the droppable then use the offsets

<Draggable >
      {(provided, snapshot) => {
        if (snapshot.isDragging) {
            let offsetEl = document.querySelector(`[data-rbd-draggable-id='${provided.draggableProps["data-rbd-draggable-id"]}']`) as HTMLElement;
            provided.draggableProps.style = {
                ...provided.draggableProps.style,
                left: offsetEl ? offsetEl.offsetLeft : 0,
                top: offsetEl ? offsetEl.offsetTop : 0
            }
        }
           return (
                <div 
                     ref={provided.innerRef}
                     {...provided.draggableProps}
                     {...provided.dragHandleProps}>
                     YOUR ITEM
                </div>
           );
       }}
</Draggable>

jmandia08 avatar Aug 01 '23 16:08 jmandia08

@Pawel1894 's solution worked for me until I needed to contain the draggables in scrollable divs with a fixed height.

I ended up fixing this by, instead of passing top: 'auto !important' into the component's style, doing this:

  • get a ref to the component that has the offset within your portal
  • pass that ref to your Draggable item
  • calculate the offset const offset = modalRef.current?.getBoundingClientRect().top ?? 0
  • conditionally apply the style when dragging
<Draggable
  draggableId={myId}
  index={index}
>
  {(provided, snapshot) => {
    const offset = modalRef.current?.getBoundingClientRect().top ?? 0
    return (
      <div
        className="border border-gray-200 rounded-lg p-1.5 flex-1 bg-white"
        ref={provided.innerRef}
        {...provided.draggableProps}
        {...provided.dragHandleProps}
        style={{
          ...provided.draggableProps.style,
          left: 'auto !important',
          top: snapshot.isDragging
            ? provided.draggableProps.style?.top - offset
            : 'auto !important',
        }}
          >

whats the modalRef?

IttzyTT avatar Aug 09 '23 09:08 IttzyTT

whats the modalRef?

@IttzyTT sorry I should have clarified that!

modalRef is a ref for the div of the modal. Specifically it should be the div of the modal that sets the position the relative.

jessebrennan avatar Aug 09 '23 17:08 jessebrennan

I solved this by mutating the provided.draggableProps.style.left and provided.draggableProps.style.right values when provided.isDragging is true

<Draggable >
      {(provided, snapshot) => {
          if (snapshot.isDragging) {
              const offset = { x: 100, y: 100 }          // your fixed container left/top position
              const x = provided.draggableProps.style.left - offset.x;
              const y = provided.draggableProps.style.top - offset.y;
              provided.draggableProps.style.left = x;
              provided.draggableProps.style.top = y;
           }
           return (
                <div 
                     ref={provided.innerRef}
                     {...provided.draggableProps}
                     {...provided.dragHandleProps}>
                     YOUR ITEM
                </div>
           );
       }}
</Draggable>

Based on this, I changed the offset based in fixed position to offsetLeft,offsetTop and it works fine. Also, I don't know the position of my container (is a modal window the user can drag around the screen) so, at least in my case, I prefer to use this as follows:

<Draggable >
      {(provided, snapshot) => {
          if (snapshot.isDragging) {
              provided.draggableProps.style.left = provided.draggableProps.style.offsetLeft;
              provided.draggableProps.style.top = provided.draggableProps.style.offsetTop;
           }
           return (
                <div 
                     ref={provided.innerRef}
                     {...provided.draggableProps}
                     {...provided.dragHandleProps}>
                     YOUR ITEM
                </div>
           );
       }}
</Draggable>

EDIT:

It seems that provided.draggableProps.style.offsetLeft is not working since its value is undefined. I thought it was working okay because it really works with no warnings, but the value is undefined. Any ideas?

thanks underium it works perfectly after finding many issue this was perfect solution thanks.

aniket-solulab avatar Sep 03 '23 14:09 aniket-solulab

Hey guys i manged to fix it quite easily and it works well for me. I wrote css class with important, this made my item drag start on his current position.

apply on draggable div: left: auto !important; top: auto !important;

I had the same problem and ended up needed to remove display:flex from the droppable container.

maclementED avatar Mar 21 '24 22:03 maclementED

I had this problem, could not fix it so I created a component that smoothly follows the cursor that I set at the same level in the hierarchy as DragDropContext In my case items are imgs but you could just as easily use other types of components, make sure you can easily set the props of the item to use the correct item props maybe using redux or useState

const ImageFollowCursor = ({ src }) => {
  const mouseListener1 = (e) => {
    const circle = document.getElementById('circle');
    const left = e.clientX;
    const top = e.clientY;
    circle.style.transform = `translate(${left}px, ${top}px)`;
  };
  useEffect(() => {
    document.addEventListener('mousemove', mouseListener1);
    return () => {
      document.removeEventListener('mousemove', mouseListener1);
    };
  }, [src]);
  return (
    <img
      id="circle"
      aria-label="cursor"
      style={{
        display: 'none',
        objectFit: 'cover',
        borderRadius: '10px',
        opacity: 0.9,
        position: 'fixed',
        top: 0,
        left: 0,
        marginTop: '5px',
        marginLeft: '5px',
        width: '60px',
        height: '60px',
        pointerEvents: 'none',
        zIndex: 999999,
        transition: 'transform 0.1s',
        transform: 'translate(0, 0)'
      }}
    />
  );
};

And I modified the handlers onDragUpdate and onDragEnd like so

const onDragUpdate = (result) => {
    const circle = document.getElementById('circle');
    if (circle) {
      const draggingElement = mediaList.find((m) => m.id === result.draggableId);
      circle.src = draggingElement?.get('thumbnailUrl');
      circle.style.display = 'block';
    }
// Rest of the handler...
}

const onDragEnd = (result) => {
    const circle = document.getElementById('circle');
    if (circle) {
      circle.src = null;
      circle.style.display = 'none';
    }
// Rest of the handler...
}

R3D347HR4Y avatar May 10 '24 19:05 R3D347HR4Y

I'm having this same issue, and it only affects non-chromium browsers (Safari, Firefox). The solution that @camdagr8 proposed works great. However if your modal is scrollable, it will completely break. It will work fine if you are at the top of the window, but when you scroll down it will again reposition the draggables incorrectly

I there any way so that it will work for scrollable container?

manishkr94 avatar May 22 '24 06:05 manishkr94

Same problem inside a scrollable modal and none of the above solutions work

dyelax avatar Jul 16 '24 23:07 dyelax

I have the same issue with an extra unexpected behavior

To Produce the issue: https://codesandbox.io/s/olk8m320lq

dnd

This happens because your modal has a div with position = relative, absolute or fixed that has a positioning style like margin: auto, or anything that manages it's placement on screen. You should make this div fullscreen and manage the placement of your dialog by creating another div with static position and place it anywhere you want using flex or etc...

HamedSiaban avatar Jul 28 '24 12:07 HamedSiaban

Hey guys I need some help figuring out what's causing this strange behavior while dragging elements in this portal modal component. I'll leave a demo video below. This problem occurs if I grab the elements from the bottom. In the console, there aren't any warnings or errors.

Expected behavior

Actual behavior

Steps to reproduce

Suggested solution?

What version of React are you using?

^16.13.1

What version of react-beautiful-dnd are you running?

Latest

What browser are you using?

Electron 8.2.5

Demo

https://imgur.com/a/Uxyiwgl

And this happens because your modal is scrollable. There is no way to implement this library for two scrollable layouts inside each other right now, and this is mentioned in the official documentation of react beautiful dnd.

The only way is to make your modal un-scrollable, and instead set overflow-y: auto and max-height: x for the droppable section in your modal.

HamedSiaban avatar Jul 28 '24 13:07 HamedSiaban