atlassian / react-beautiful-dnd

Beautiful and accessible drag and drop for lists with React
https://react-beautiful-dnd.netlify.app
Other
33.37k stars 2.57k forks source link

transform on parent messes up dragging positioning #128

Open nickav opened 7 years ago

nickav commented 7 years ago

Bug

Expected behaviour

Dragging an item maintains the proper positioning.

Actual behaviour

The item is pushed down by the height of the neighboring element.

Steps to reproduce

Create the following html structure:

<div>
    <div style="height: 100px;"></div>
    <div style="transform: translateY(0);">
        <!-- react-beautiful-dnd component -->
    </div>
</div>

Such that react beautiful dnd is monted as a sibling of an element with some height. The parent of the dnd component should have some transform property as well.

Browser version

Google Chrome: Version 61.0.3163.100 (Official Build) (64-bit)

Demo

https://www.webpackbin.com/bins/-KvOoCqQRkMYJzq63hlS

video: https://user-images.githubusercontent.com/5448363/166853462-519f2b79-43de-43b8-8912-6d84a71a1847.mov

https://user-images.githubusercontent.com/5448363/166853501-c2fb236c-61ce-4b7b-ad3c-24ab8fe51295.mov

alexreardon commented 7 years ago

Check out the 'Warning: position: fixed' section under Draggable. At this stage having parent transforms are not supported. We might look at jumping to the new React portal solution to get around this in the future. However, for now it is not supported

alexreardon commented 7 years ago

Thanks for raising this @nickaversano !!

nickav commented 7 years ago

@alexreardon what about having an option to use pure position fixed positioning? In this mode, the position of the placeholder would be set entirely by top and left instead of using the transform property

alexreardon commented 7 years ago

Interesting idea @nickaversano - worth thinking about. Generally updating top/left values during a drag is not ideal as it does not use the GPU. However, it may get around the issue. We are trying to keep the API as clean as possible so adding an option might not be ideal. In the mean time, you could achieve this yourself!

Here is the type for the draggable style:

export type DraggingStyle = {|
  // Allow scrolling of the element behind the dragging element
  pointerEvents: 'none',

  // `position: fixed` is used to ensure that the element is always positioned
  // in the correct position and ignores the surrounding position:relative parents
  position: 'fixed',

  // When we do `position: fixed` the element looses its normal dimensions,
  // especially if using flexbox. We set the width and height manually to
  // ensure the element has the same dimensions as before it started dragging
  width: number,
  height: number,

  // When we set the width and height they include the padding on the element.
  // We use box-sizing: border-box to ensure that the width and height are inclusive of the padding
  boxSizing: 'border-box',

  // We initially position the element in the same visual spot as when it started.
  // To do this we give the element the top / left position with the margins considered
  top: number,
  left: number,

  // We clear any top or left margins on the element to ensure it does not push
  // the element positioned with the top/left position.
  // We also clear the margin right / bottom. This has no positioning impact,
  // but it is cleanest to just remove all the margins rather than only the top and left.
  margin: 0,

  // Move the element in response to a user dragging
  transform: ?string,

  // When dragging or dropping we control the z-index to ensure that
  // the layering is correct
  zIndex: ZIndex,
|}

You could set the transform to null and update the top and left values yourself (remember to make the adjustments relative to the initial top and left values).

Here is the format of the transform value:

transform: `translate(${point.x}px, ${point.y}px)`
alexreardon commented 7 years ago

Let me know how you go

nickav commented 7 years ago

I think for now I'm just going to remove the transform on the parent. I'm doing an animation, but can remove it after the animation completes.

alexreardon commented 7 years ago

Even better!

kasperpihl commented 6 years ago

I managed to get this to work with Portal from react-dom without changing styles etc.

I added a dom element into my html

<div id="draggable"></div>

Added the following styles

position: absolute;
pointer-events: none;
height: 100%;
width: 100%;

And then added this to the Draggable

import React, { PureComponent } from 'react';
import { createPortal } from 'react-dom';

const _dragEl = document.getElementById('draggable');

class DraggableGoal extends PureComponent {
  optionalPortal(styles, element) {
    if(styles.position === 'fixed') {
      return createPortal(
        element,
        _dragEl,
      );
    }
    return element;
  }
  render() {
    const { item } = this.props;
    return (
      <Draggable>
        {(provided, snapshot) => {
          return (
            <div>
              {this.optionalPortal(provided.draggableStyle, (
                <div
                  ref={provided.innerRef}
                  style={provided.draggableStyle}
                  {...provided.dragHandleProps}
                >
                  {item}
                </div>
              ))}
              {provided.placeholder}
            </div>
          );
        }}
      </Draggable>
    );
  }
}

Hope this helps :)

Valoran commented 5 years ago

Sorry if i'm not supposed to post in closed issues, but i figured this might help if someone else is still having the same problem.

I had the same issue when moving an item from a nested list to the parent list, due to the nested lists' container getting transformed when it is "moved down" to show where the dragged item will drop. The problem itself is that when an item (in this case the nested list) gets transformed, that element becomes the new target container for any child elements with position: fixed (essentially changing it to position: absolute), so the nested item i am trying to drag does not position itself to the viewport, but rather to it's parent container.

I finally managed to make a CSS based workaround and thought I should share it incase someone else still has the same problem.

My solution was to monitor the parent containers styles and when it gets a transform, i recalculate the inner items transform to compensate with the formula current X - parentX - position.left and currentY - parentY - position.top where currentX and currentY is the transform properties i get from provided.draggableProps.style.transform there is also a +20in there to account for some margins in my case.


<Draggable
         key={index}
         draggableId={cel.contentId}
         index={index}>
                {(provided, snapshot) => {
                           let transform = provided.draggableProps.style.transform;
                           if(snapshot.isDragging){
                           let regex = new RegExp('translate\\((.*?)px, (.*?)px\\)');

                           //the transform property of parent style gotten through reference and passed in as a prop
                           let parentValues = regex.exec(this.props.parentTransform ||"")
                           let childValues = regex.exec(provided.draggableProps.style.transform || "");

                           //if both the parent (the nested list) and the child (item beeing dragged) has transform values, recalculate the child items transform to account for position fixed not working
                           if(childValues != null && parentValues != null){
                                   let x = (parseFloat(childValues[1], 10)) - ((parseFloat(parentValues[1], 10)) + (parseFloat(provided.draggableProps.style.left,10))) + 20;
                                   let y = (parseFloat(childValues[2], 10)) - ((parseFloat(parentValues[2], 10)) + (parseFloat(provided.draggableProps.style.top, 10)));
                                   transform = `translate(${x}px, ${y}px)`;
                          }
              } 
return (<div
        {...provided.draggableProps}
        {...this.props}
        ref={provided.innerRef}
        isDragging={snapshot.isDragging}
        style={{...provided.draggableProps.style, transform}}
        >(... _inner content removed for brevity_ )</div>)
}
</Draggable>
lkiarest commented 5 years ago

Sorry if i'm not supposed to post in closed issues, but i figured this might help if someone else is still having the same problem.

I had the same issue when moving an item from a nested list to the parent list, due to the nested lists' container getting transformed when it is "moved down" to show where the dragged item will drop. The problem itself is that when an item (in this case the nested list) gets transformed, that element becomes the new target container for any child elements with position: fixed (essentially changing it to position: absolute), so the nested item i am trying to drag does not position itself to the viewport, but rather to it's parent container.

I finally managed to make a CSS based workaround and thought I should share it incase someone else still has the same problem.

My solution was to monitor the parent containers styles and when it gets a transform, i recalculate the inner items transform to compensate with the formula current X - parentX - position.left and currentY - parentY - position.top where currentX and currentY is the transform properties i get from provided.draggableProps.style.transform there is also a +20in there to account for some margins in my case.

<Draggable
         key={index}
         draggableId={cel.contentId}
         index={index}>
                {(provided, snapshot) => {
                           let transform = provided.draggableProps.style.transform;
                           if(snapshot.isDragging){
                           let regex = new RegExp('translate\\((.*?)px, (.*?)px\\)');

                           //the transform property of parent style gotten through reference and passed in as a prop
                           let parentValues = regex.exec(this.props.parentTransform ||"")
                           let childValues = regex.exec(provided.draggableProps.style.transform || "");

                           //if both the parent (the nested list) and the child (item beeing dragged) has transform values, recalculate the child items transform to account for position fixed not working
                           if(childValues != null && parentValues != null){
                                   let x = (parseFloat(childValues[1], 10)) - ((parseFloat(parentValues[1], 10)) + (parseFloat(provided.draggableProps.style.left,10))) + 20;
                                   let y = (parseFloat(childValues[2], 10)) - ((parseFloat(parentValues[2], 10)) + (parseFloat(provided.draggableProps.style.top, 10)));
                                   transform = `translate(${x}px, ${y}px)`;
                          }
              } 
return (<div
        {...provided.draggableProps}
        {...this.props}
        ref={provided.innerRef}
        isDragging={snapshot.isDragging}
        style={{...provided.draggableProps.style, transform}}
        >(... _inner content removed for brevity_ )</div>)
}
</Draggable>

there isn't an official solution for now, right ?

kaanbayram commented 4 years ago

I managed to get this to work with Portal from react-dom without changing styles etc.

I added a dom element into my html

<div id="draggable"></div>

Added the following styles

position: absolute;
pointer-events: none;
height: 100%;
width: 100%;

And then added this to the Draggable

import React, { PureComponent } from 'react';
import { createPortal } from 'react-dom';

const _dragEl = document.getElementById('draggable');

class DraggableGoal extends PureComponent {
  optionalPortal(styles, element) {
    if(styles.position === 'fixed') {
      return createPortal(
        element,
        _dragEl,
      );
    }
    return element;
  }
  render() {
    const { item } = this.props;
    return (
      <Draggable>
        {(provided, snapshot) => {
          return (
            <div>
              {this.optionalPortal(provided.draggableStyle, (
                <div
                  ref={provided.innerRef}
                  style={provided.draggableStyle}
                  {...provided.dragHandleProps}
                >
                  {item}
                </div>
              ))}
              {provided.placeholder}
            </div>
          );
        }}
      </Draggable>
    );
  }
}

Hope this helps :)

Budy you saved my life many thankful to you

zachsa commented 4 years ago

This mostly worked for me.. Or, at least, now when I do the drag things align. However when I drop the animation zooms back to the position it would have been in without the transform.

Is there a way that I can adjust the drop animation start position?

kaanbayram commented 4 years ago

This mostly worked for me.. Or, at least, now when I do the drag things align. However when I drop the animation zooms back to the position it would have been in without the transform.

Is there a way that I can adjust the drop animation start position?

@zachsa

if you just want to change position:fixed property you don't have to use portal. You can do override. If you don't know how you can do i can look my old project for you. Portal will isolate parent css from child DOM element.

zachsa commented 4 years ago

Ah. I didn't think of adjusting the position in the style element. Thank you

zachsa commented 4 years ago

Sorry if i'm not supposed to post in closed issues, but i figured this might help if someone else is still having the same problem. I had the same issue when moving an item from a nested list to the parent list, due to the nested lists' container getting transformed when it is "moved down" to show where the dragged item will drop. The problem itself is that when an item (in this case the nested list) gets transformed, that element becomes the new target container for any child elements with position: fixed (essentially changing it to position: absolute), so the nested item i am trying to drag does not position itself to the viewport, but rather to it's parent container. I finally managed to make a CSS based workaround and thought I should share it incase someone else still has the same problem. My solution was to monitor the parent containers styles and when it gets a transform, i recalculate the inner items transform to compensate with the formula current X - parentX - position.left and currentY - parentY - position.top where currentX and currentY is the transform properties i get from provided.draggableProps.style.transform there is also a +20in there to account for some margins in my case.

<Draggable
         key={index}
         draggableId={cel.contentId}
         index={index}>
                {(provided, snapshot) => {
                           let transform = provided.draggableProps.style.transform;
                           if(snapshot.isDragging){
                           let regex = new RegExp('translate\\((.*?)px, (.*?)px\\)');

                           //the transform property of parent style gotten through reference and passed in as a prop
                           let parentValues = regex.exec(this.props.parentTransform ||"")
                           let childValues = regex.exec(provided.draggableProps.style.transform || "");

                           //if both the parent (the nested list) and the child (item beeing dragged) has transform values, recalculate the child items transform to account for position fixed not working
                           if(childValues != null && parentValues != null){
                                   let x = (parseFloat(childValues[1], 10)) - ((parseFloat(parentValues[1], 10)) + (parseFloat(provided.draggableProps.style.left,10))) + 20;
                                   let y = (parseFloat(childValues[2], 10)) - ((parseFloat(parentValues[2], 10)) + (parseFloat(provided.draggableProps.style.top, 10)));
                                   transform = `translate(${x}px, ${y}px)`;
                          }
              } 
return (<div
        {...provided.draggableProps}
        {...this.props}
        ref={provided.innerRef}
        isDragging={snapshot.isDragging}
        style={{...provided.draggableProps.style, transform}}
        >(... _inner content removed for brevity_ )</div>)
}
</Draggable>

there isn't an official solution for now, right ?

I found that although this fixed the dragging offset, when I drop the item the animation is from where the original position would have been

kaanbayram commented 4 years ago

@zachsa i think you are changing your Droppable area's location after render. Because of position:fixed even if you change this property for draggable element acting as it was in its old position. Because Droppable area's position property still position:fixed. And unfortunately you can't override droppable area's css as draggable element. Sorry for my english. I was have similar problem as like yours. https://github.com/kaanbayram/React-antdesign-menu-beatiful-dnd

This is my project, i think this can be helpful to you.

zachsa commented 4 years ago

thank you - that does look like it's exactly what i want to do. Before I start looking through the source (still quite difficult for me), would you mind if I just double check with you that I'm on the right track?

It looks like this is something that your code will fix! I'll give it a go - please let me know if I'm NOT on the correct track !!

kaanbayram commented 4 years ago

Firstly sorry for my complicated code. I think yes you are in correct track. If you wish you can share your project, maybe i can help to you better.

alexreardon commented 4 years ago

I've reopening this as there is still discussion going. I will try to get to it soon

zachsa commented 4 years ago

Thank you - @kaanbayram. no need to apologize for showing me helpful code! For whatever reason though, the project when installed and started showed the same problem that I was having.

I don't know why this would be the case, since I see from comments above that you looked at the same portal code that I used, and which worked - something along the lines of this:

import React, { PureComponent } from 'react';
import { createPortal } from 'react-dom';

const _dragEl = document.getElementById('draggable');

class DraggableGoal extends PureComponent {
  optionalPortal(styles, element) {
    if(styles.position === 'fixed') {
      return createPortal(
        element,
        _dragEl,
      );
    }
    return element;
  }
  render() {
    const { item } = this.props;
    return (
      <Draggable>
        {(provided, snapshot) => {
          return (
            <div>
              {this.optionalPortal(provided.draggableStyle, (
                <div
                  ref={provided.innerRef}
                  style={provided.draggableStyle}
                  {...provided.dragHandleProps}
                >
                  {item}
                </div>
              ))}
              {provided.placeholder}
            </div>
          );
        }}
      </Draggable>
    );
  }
}

However the styling didn't quite work out the box. @alexreardon. What are the challenges of allowing us to pass a ref to a parent that gets the transform style, and then 'normalizing' the transforms on the draggable elements when dragging and dropping (sorry, I don't know the library well enough to know if this makes sense). If it did this... then from the user perspective it would 'just work'. (Although I am happy to learn about React portals - something I had no prior knowledge of).

cuxs commented 4 years ago

I managed to get this to work with Portal from react-dom without changing styles etc.

I added a dom element into my html

<div id="draggable"></div>

Added the following styles

position: absolute;
pointer-events: none;
height: 100%;
width: 100%;

And then added this to the Draggable

import React, { PureComponent } from 'react';
import { createPortal } from 'react-dom';

const _dragEl = document.getElementById('draggable');

class DraggableGoal extends PureComponent {
  optionalPortal(styles, element) {
    if(styles.position === 'fixed') {
      return createPortal(
        element,
        _dragEl,
      );
    }
    return element;
  }
  render() {
    const { item } = this.props;
    return (
      <Draggable>
        {(provided, snapshot) => {
          return (
            <div>
              {this.optionalPortal(provided.draggableStyle, (
                <div
                  ref={provided.innerRef}
                  style={provided.draggableStyle}
                  {...provided.dragHandleProps}
                >
                  {item}
                </div>
              ))}
              {provided.placeholder}
            </div>
          );
        }}
      </Draggable>
    );
  }
}

Hope this helps :)

This almost works form me, it fixes dragging positioning issue but all the other cards disappears, I just need to tweak this a little more.

hemavidal commented 4 years ago

Sorry if i'm not supposed to post in closed issues, but i figured this might help if someone else is still having the same problem.

I had the same issue when moving an item from a nested list to the parent list, due to the nested lists' container getting transformed when it is "moved down" to show where the dragged item will drop. The problem itself is that when an item (in this case the nested list) gets transformed, that element becomes the new target container for any child elements with position: fixed (essentially changing it to position: absolute), so the nested item i am trying to drag does not position itself to the viewport, but rather to it's parent container.

I finally managed to make a CSS based workaround and thought I should share it incase someone else still has the same problem.

My solution was to monitor the parent containers styles and when it gets a transform, i recalculate the inner items transform to compensate with the formula current X - parentX - position.left and currentY - parentY - position.top where currentX and currentY is the transform properties i get from provided.draggableProps.style.transform there is also a +20in there to account for some margins in my case.

<Draggable
         key={index}
         draggableId={cel.contentId}
         index={index}>
                {(provided, snapshot) => {
                           let transform = provided.draggableProps.style.transform;
                           if(snapshot.isDragging){
                           let regex = new RegExp('translate\\((.*?)px, (.*?)px\\)');

                           //the transform property of parent style gotten through reference and passed in as a prop
                           let parentValues = regex.exec(this.props.parentTransform ||"")
                           let childValues = regex.exec(provided.draggableProps.style.transform || "");

                           //if both the parent (the nested list) and the child (item beeing dragged) has transform values, recalculate the child items transform to account for position fixed not working
                           if(childValues != null && parentValues != null){
                                   let x = (parseFloat(childValues[1], 10)) - ((parseFloat(parentValues[1], 10)) + (parseFloat(provided.draggableProps.style.left,10))) + 20;
                                   let y = (parseFloat(childValues[2], 10)) - ((parseFloat(parentValues[2], 10)) + (parseFloat(provided.draggableProps.style.top, 10)));
                                   transform = `translate(${x}px, ${y}px)`;
                          }
              } 
return (<div
        {...provided.draggableProps}
        {...this.props}
        ref={provided.innerRef}
        isDragging={snapshot.isDragging}
        style={{...provided.draggableProps.style, transform}}
        >(... _inner content removed for brevity_ )</div>)
}
</Draggable>

Worked for me but I have changed this piece to work fine:

const x =
  parseFloat(childValues[1], 10) -
  parseFloat(
    provided.draggableProps.style.left,
    10,
  )
const y =
  parseFloat(childValues[2], 10) -
  parseFloat(
    provided.draggableProps.style.top,
    10,
  )

And this one too:

style={{
  ...provided.draggableProps.style,
  transform,
  position: snapshot.isDragging
    ? 'absolute'
    : '',
}}
nikitowsky commented 4 years ago

@kasperpihl where I can buy beer for you? You saved my day.

AnujVarma commented 4 years ago

That is super exhilarating to hear @borisowsky, @kasperpihl, I tried implementing a solution in codesandbox here: https://codesandbox.io/s/react-beautiful-dnd-h4lk7?file=/src/index.js

In this example, I translate the drag and drop list by 30 in the x and y direction, however, even after using portals, the dragging behavior is offset. Any help/ a working example on code sandbox would be amazing, thank you!

ShongSu commented 4 years ago

Sorry if i'm not supposed to post in closed issues, but i figured this might help if someone else is still having the same problem.

I had the same issue when moving an item from a nested list to the parent list, due to the nested lists' container getting transformed when it is "moved down" to show where the dragged item will drop. The problem itself is that when an item (in this case the nested list) gets transformed, that element becomes the new target container for any child elements with position: fixed (essentially changing it to position: absolute), so the nested item i am trying to drag does not position itself to the viewport, but rather to it's parent container.

I finally managed to make a CSS based workaround and thought I should share it incase someone else still has the same problem.

My solution was to monitor the parent containers styles and when it gets a transform, i recalculate the inner items transform to compensate with the formula current X - parentX - position.left and currentY - parentY - position.top where currentX and currentY is the transform properties i get from provided.draggableProps.style.transform there is also a +20in there to account for some margins in my case.

<Draggable
         key={index}
         draggableId={cel.contentId}
         index={index}>
                {(provided, snapshot) => {
                           let transform = provided.draggableProps.style.transform;
                           if(snapshot.isDragging){
                           let regex = new RegExp('translate\\((.*?)px, (.*?)px\\)');

                           //the transform property of parent style gotten through reference and passed in as a prop
                           let parentValues = regex.exec(this.props.parentTransform ||"")
                           let childValues = regex.exec(provided.draggableProps.style.transform || "");

                           //if both the parent (the nested list) and the child (item beeing dragged) has transform values, recalculate the child items transform to account for position fixed not working
                           if(childValues != null && parentValues != null){
                                   let x = (parseFloat(childValues[1], 10)) - ((parseFloat(parentValues[1], 10)) + (parseFloat(provided.draggableProps.style.left,10))) + 20;
                                   let y = (parseFloat(childValues[2], 10)) - ((parseFloat(parentValues[2], 10)) + (parseFloat(provided.draggableProps.style.top, 10)));
                                   transform = `translate(${x}px, ${y}px)`;
                          }
              } 
return (<div
        {...provided.draggableProps}
        {...this.props}
        ref={provided.innerRef}
        isDragging={snapshot.isDragging}
        style={{...provided.draggableProps.style, transform}}
        >(... _inner content removed for brevity_ )</div>)
}
</Draggable>

Thank you for your share. I have a question here how can I get/set parentTransform in props?

renaudtertrais commented 4 years ago

I had the same issue. Following the great solution of @kasperpihl, I created a simple hook do do the trick:

Hook:

const useDraggableInPortal = () => {
    const self = useRef({}).current;

    useEffect(() => {
        const div = document.createElement('div');
        div.style.position = 'absolute';
        div.style.pointerEvents = 'none';
        div.style.top = '0';
        div.style.width = '100%';
        div.style.height = '100%';
        self.elt = div;
        document.body.appendChild(div);
        return () => {
            document.body.removeChild(div);
        };
    }, [self]);

    return (render) => (provided, ...args) => {
        const element = render(provided, ...args);
        if (provided.draggableProps.style.position === 'fixed') {
            return createPortal(element, self.elt);
        }
        return element;
    };
};

Usage:

Considering the following component:

const MyComponent = (props) => {
  return (
    <DragDropContext onDragEnd={/* ... */}>
      <Droppable droppableId="droppable">
        {({ innerRef, droppableProps, placeholder }) => (
          <div ref={innerRef} {...droppableProps}>
            {props.items.map((item, index) => (
              <Draggable key={item.id} draggableId={item.id} index={index}>
                {(provided) => (
                  <div
                    ref={provided.innerRef}
                    {...provided.draggableProps}
                    {...provided.dragHandleProps}
                  >
                    {item.title}
                  </div>
                )}
              </Draggable>
            )}
            {placeholder}
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );
};

Just call the hook and use the returned function to wrap the children callback of <Draggable /> component:

const MyComponent = (props) => {
+ const renderDraggable = useDraggableInPortal();

  return (
    <DragDropContext onDragEnd={/* ... */}>
      <Droppable droppableId="droppable">
        {({ innerRef, droppableProps, placeholder }) => (
          <div ref={innerRef} {...droppableProps}>
            {props.items.map((item, index) => (
              <Draggable key={item.id} draggableId={item.id} index={index}>
+               {renderDraggable((provided) => (
                  <div
                    ref={provided.innerRef}
                    {...provided.draggableProps}
                    {...provided.dragHandleProps}
                  >
                    {item.title}
                  </div>
                ))}
              </Draggable>
+           ))}
            {placeholder}
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );
};

Hope it'll help.

ghost commented 4 years ago

I managed to get this to work with Portal from react-dom without changing styles etc.

I added a dom element into my html

<div id="draggable"></div>

Added the following styles

position: absolute;
pointer-events: none;
height: 100%;
width: 100%;

And then added this to the Draggable

import React, { PureComponent } from 'react';
import { createPortal } from 'react-dom';

const _dragEl = document.getElementById('draggable');

class DraggableGoal extends PureComponent {
  optionalPortal(styles, element) {
    if(styles.position === 'fixed') {
      return createPortal(
        element,
        _dragEl,
      );
    }
    return element;
  } #
  render() {
    const { item } = this.props;
    return (
      <Draggable>
        {(provided, snapshot) => {
          return (
            <div>
**>              {optionalPortal(provided.draggableProps.style, (**
                <div
                  ref={provided.innerRef}
                  style={provided.draggableStyle}
                  {...provided.dragHandleProps}
                >
                  {item}
                </div>
              ))}
              {provided.placeholder}
            </div>
          );
        }}
      </Draggable>
    );
  }
}

Hope this helps :)

This works great, but just use: {optionalPortal(provided.draggableProps.style, (

Maxeeezy commented 4 years ago

I face the same out-of-position problem.

When I try to implement it like @daljeetv I get an error Unable to find draggable element with id: X when clicking the item that I would like to move.

Do you guys have any ideas what the reason could be?

ducktordanny commented 3 years ago

Anyone who has still this problem I made a repository where I made an example for it and in the README you can also find a more simple solution. I found it from here in a comment and it works fine.

Note: I had another problem where the dragged div was disappear and I found out it was because of z-index and it dropped behind the parent div. So if e.g. you have a sidebar and its z-index is 999 you have to set the div of Draggable component above that (e.g. z-index: 1000;).

Here is my repo: https://github.com/DucktorDanny/react-beautiful-dnd-example

I hope I could help. :)

Maxeeezy commented 3 years ago

Hey Danny, thank you so much!

Holen Sie sich Outlook für Androidhttps://aka.ms/ghei36


From: ducktor notifications@github.com Sent: Sunday, January 31, 2021 7:35:36 PM To: atlassian/react-beautiful-dnd react-beautiful-dnd@noreply.github.com Cc: Maxeeezy maximilian.brueckner.91@outlook.de; Comment comment@noreply.github.com Subject: Re: [atlassian/react-beautiful-dnd] transform on parent messes up dragging positioning (#128)

Anyone who has still this problem I made a repository where I made an example for it and in the README you can also find a more simple solution. I found it from here in a comment and it works fine.

Note: I had another problem where the dragged div was disappear and I found out it was because of z-index and it dropped behind the parent div. So if e.g. you have a sidebar and its z-index is 999 you have to set the div of Draggable component above that (e.g. z-index: 1000;).

Here is my repo: https://github.com/DucktorDanny/react-beautiful-dnd-example

I hope I could help. :)

— You are receiving this because you commented. Reply to this email directly, view it on GitHubhttps://github.com/atlassian/react-beautiful-dnd/issues/128#issuecomment-770428164, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AKDBV6LFZ4KCZCQFD2NMZ73S4WPHRANCNFSM4D5IE6IA.

nickav commented 3 years ago

@duktorD @Maxeezy - you could even do something as simple as:

const ref = React.useRef();

<DragDropContext
  onDragStart={() => {
    const el = ref?.current;
    if (!el) return;

    const bounds = el.getBoundingClientRect();

    el.style.position = 'fixed';
    el.style.top = bounds.top + 'px';
    el.style.left = bounds.left + 'px';
    el.style.transform = 'none';
  }}
  onDragEnd={() => {
    const el = ref?.current;
    if (!el) return;

    el.style.position = null;
    el.style.top = null;
    el.style.left = null;
    el.style.transform = null;
  }}
>
{/* ... */}
</DragDropContext>

it would only work as long as your component doesn't re-render during the drag i think

bknill commented 3 years ago

I used @ducktorD 's lovely example.

Fixed the problem but now I have ugly items.

image

Does it seem snapshot might be included in a later version? with the is dragging props. I'm running 13.1.0

montacerdk commented 2 years ago

I had the same issue. Following the great solution of @kasperpihl, I created a simple hook do do the trick:

Hook:

const useDraggableInPortal = () => {
    const self = useRef({}).current;

    useEffect(() => {
        const div = document.createElement('div');
        div.style.position = 'absolute';
        div.style.pointerEvents = 'none';
        div.style.top = '0';
        div.style.width = '100%';
        div.style.height = '100%';
        self.elt = div;
        document.body.appendChild(div);
        return () => {
            document.body.removeChild(div);
        };
    }, [self]);

    return (render) => (provided, ...args) => {
        const element = render(provided, ...args);
        if (provided.draggableProps.style.position === 'fixed') {
            return createPortal(element, self.elt);
        }
        return element;
    };
};

Usage:

Considering the following component:

const MyComponent = (props) => {
  return (
    <DragDropContext onDragEnd={/* ... */}>
      <Droppable droppableId="droppable">
        {({ innerRef, droppableProps, placeholder }) => (
          <div ref={innerRef} {...droppableProps}>
            {props.items.map((item, index) => (
              <Draggable key={item.id} draggableId={item.id} index={index}>
                {(provided) => (
                  <div
                    ref={provided.innerRef}
                    {...provided.draggableProps}
                    {...provided.dragHandleProps}
                  >
                    {item.title}
                  </div>
                )}
              </Draggable>
            )}
            {placeholder}
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );
};

Just call the hook and use the returned function to wrap the children callback of <Draggable /> component:

const MyComponent = (props) => {
+ const renderDraggable = useDraggableInPortal();

  return (
    <DragDropContext onDragEnd={/* ... */}>
      <Droppable droppableId="droppable">
        {({ innerRef, droppableProps, placeholder }) => (
          <div ref={innerRef} {...droppableProps}>
            {props.items.map((item, index) => (
              <Draggable key={item.id} draggableId={item.id} index={index}>
+               {renderDraggable((provided) => (
                  <div
                    ref={provided.innerRef}
                    {...provided.draggableProps}
                    {...provided.dragHandleProps}
                  >
                    {item.title}
                  </div>
                ))}
              </Draggable>
+           ))}
            {placeholder}
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );
};

Hope it'll help.

For React TypeScript, the hook will look like :

import { DraggableProvided, DraggingStyle } from 'react-beautiful-dnd'
import { ReactElement, useEffect, useRef } from 'react'
import { createPortal } from 'react-dom'

export const useDraggableInPortal = () => {
  const element = useRef<HTMLDivElement>(document.createElement('div')).current

  useEffect(() => {
    if (element) {
      element.style.pointerEvents = 'none'
      element.style.position = 'absolute'
      element.style.height = '100%'
      element.style.width = '100%'
      element.style.top = '0'

      document.body.appendChild(element)

      return () => {
        document.body.removeChild(element)
      }
    }
  }, [element])

  return (render: (provided: DraggableProvided) => ReactElement) => (provided: DraggableProvided) => {
    const result = render(provided)
    const style = provided.draggableProps.style as DraggingStyle
    if (style.position === 'fixed') {
      return createPortal(result, element)
    }
    return result
  }
}
thalysonalexr commented 2 years ago

@renaudtertrais thanks so much bro!!! Congratulations! This works for me. Thanks for types @montacerdk

nomi2213 commented 2 years ago

Maybe it will help someone. I also had problems with positioning when dragging. I had the "fixed" and "transform" styles on the sidebar, but the problem was a different "will-change: transform" style. Everything worked without this style. My versions of the packages "react-beautiful-dnd": "13.1.0" and "@storybook/react": "6.5.9".

larsnystrom commented 2 years ago

I can't see anyone mentioning this yet, but there is actually some official documentation on how to deal with transform on parent: https://github.com/atlassian/react-beautiful-dnd/blob/HEAD/docs/guides/reparenting.md

This means that if you have a transform: * on one of the parents of a then the positioning logic will be incorrect while dragging. Lame!

The solution seems to be to use the renderClone prop on your <Droppable>. I just tried this myself and it works fine.

feresr commented 1 year ago

At this stage having parent transforms are not supported

I wonder why even transform: translate(0px, 0px); causes issues? It seems to re-apply the top/left properties again to the item being dragged. Unticking that from css devtools and everything works again

efeguerrero commented 1 year ago

I was having the same issue while doing an animation as the component mounted and my fix was to simply remove the transform property after the animation was done.

Use onTransitionEnd={() => handleTransitionEnd()} and then just remove the transform property and it will be fixed.

  const handleTransitionEnd = () => {
    console.log('transition end');
    transitionDivRef.current?.removeAttribute('style');
  };

I removed whole style attribute because it was a div only used for the transition.

zindler323 commented 1 year ago

I managed to get this to work with Portal from react-dom without changing styles etc.

I added a dom element into my html

<div id="draggable"></div>

Added the following styles

position: absolute;
pointer-events: none;
height: 100%;
width: 100%;

And then added this to the Draggable

import React, { PureComponent } from 'react';
import { createPortal } from 'react-dom';

const _dragEl = document.getElementById('draggable');

class DraggableGoal extends PureComponent {
  optionalPortal(styles, element) {
    if(styles.position === 'fixed') {
      return createPortal(
        element,
        _dragEl,
      );
    }
    return element;
  }
  render() {
    const { item } = this.props;
    return (
      <Draggable>
        {(provided, snapshot) => {
          return (
            <div>
              {this.optionalPortal(provided.draggableStyle, (
                <div
                  ref={provided.innerRef}
                  style={provided.draggableStyle}
                  {...provided.dragHandleProps}
                >
                  {item}
                </div>
              ))}
              {provided.placeholder}
            </div>
          );
        }}
      </Draggable>
    );
  }
}

Hope this helps :)

@kasperpihl 's portal solution helped me a lot. And in consideration of the case where i use scoped className, i find another solution: calculating the relative position to the 'transform parent':

import { merge } from 'lodash';
import cls from './style.module.scss';

const MyComponent = (props) => {
  const viewportRef = useRef(null); // find the 'transform parent'

  return (<div ref={viewportRef} style={{ transform: 'translateX(0)' }}>
    <DragDropContext onDragEnd={/* ... */}>
      {/* ... */}
            {props.items.map((item, index) => (
              <Draggable key={item.id} draggableId={item.id} index={index}>
                {(provided, { isDragging }) => (
                  <div
                    {/* ... */}
                    className={cls.item}
                    style={merge(
                      provided.draggableProps.style,
                      /** relative position item to window - relative position parent to window = relative position item to parent */
                      isDragging ? {
                         left: provided.draggableProps.style.left - viewportRef.current.getBoundingClientRect().left,
                         top: provided.draggableProps.style.top - viewportRef.current.getBoundingClientRect().top,
                      } : {}
                    )}
                  >
                    {item.title}
                  </div>
                )}
              </Draggable>
            ))}
       {/* ... */}
    </DragDropContext>
  <div>);
};
ankit-sapkota commented 6 months ago

Has anyone had any luck if the parent is scaled instead of just shifted?

bhcjohnnysiu commented 4 months ago

Has anyone had any luck if the parent is scaled instead of just shifted?

same issue....

rohitnegi-dev commented 4 months ago

So the fix that will work for everybody is to use renderClone as described https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/reparenting.md and ya'll are good to go.

linpan commented 2 months ago

So the fix that will work for everybody is to use renderClone as described https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/reparenting.md and ya'll are good to go.

404

larsnystrom commented 2 months ago

So the fix that will work for everybody is to use renderClone as described https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/reparenting.md and ya'll are good to go.

404

https://github.com/atlassian/react-beautiful-dnd/blob/HEAD/docs/guides/reparenting.md