atlassian/react-beautiful-dnd

Support for nested scroll containers

alexreardon opened this issue ยท 218 comments

Adding support for n-level deep scroll containers. Currently, only a single level is supported

Current plan

This plan will allow for nested scroll containers, and also improve the performance of scroll updates

Collection (drag start)

  • Grab all of the Droppable elements
  • Take the first one and walk up the DOM tree. If the element is a scroll container then add a data-* attribute to it. (eg data-react-beautiful-dnd-scroll-container=${index}). Cache the element and its result during the collection
  • When the node.parentElement is null then move onto the next Droppable. If an element is found that has previously been investigated then skip the rest of the upwards search. Use any previously found scroll parent ancestry.
  • When visiting an element an we also need to check to see if the element is position:fixed for our position:fixed support

Storage while dragging

  • When storing a Droppable we will keep a map (or linked list) that registers a Droppables scroll containers.
  • A droppable's frame will need to be updated when any of the ancestry changes
  • When calculating droppable displacement internally, all of the ancestry will need to be taken into account

Updates (scroll events)

A single scroll listener is added to the window as a capture:true listener. This will capture all scroll events.

  • If the source of a scroll event is a scroll container that has a data-react-beautiful-dnd-scroll-container attribute then trigger an action to update all relevant Droppables. One redux update for all Droppables (fixes #244). The internal algorithms will need to be updated to account for 0 <-> many scroll containers
  • If a scroll is on the window then trigger a window scroll action. Currently, this is handled by the drag handles. Drag handles will no longer handle this
  • If the source of a scroll event is a scroll container that is NOT a data-react-beautiful-dnd-scroll-container then it can be ignored

Auto scrolling

  • There will need to be some investigation into how this will work.
  • Need to investigate how our push scroll holds up when moving with a keyboard

Clean up (drag end)

  • Remove all of the data-react-beautiful-dnd-scroll-container attributes
  • Remove single window event listener

Bonus

  • We could keep the scroll listeners active while a drop animation is occurring and flush any drop animations

Given the complexity in supporting a single level, I think this is out of scope for now!

@alexreardon Does this issue affect Core boards / Jira Software boards? I'm trying to improve our board experience in Dovetail and running into a lot of issues that seem to be caused by this one. I would love to know a little bit about how the Core / Software teams have tackled this.

goldo commented

Hi @alexreardon,
We just bumped to 8.0.1 ๐ŸŽ‰ and are noticing this warning. We have a horizontal scrollable board with vertical columns. It is no problem for us that we can't drag, then horizontal sroll, then drop at the right place, but we would like to clear this console warning, even in development mode. Is that possible ?

Thanks

I am running into this issue too (scrollable columns + horizontal scroll on the board). I think it would be very helpful if you could tackle this in the near future. Thank you!

alxtz commented

Doesn't quite understand this issue, the long lists in a short container seems to work fine

Also, agree to @goldo, spamming too much console.warn() in the codebase isn't healthy

sis commented

@alxtz Nested scroll containers would mean that you can vertically scroll through individual columns whilst having another parent scroll container as right now you can't.

@goldo perhaps we could add an option to opt out of all warnings - even in development? ๐Ÿค”

goldo commented
alxtz commented

@goldo would making the disableWarning option to be true by default solve your concern?

goldo commented

@alxtz like I said, I think warnings are useful, specially in case of bumps, so I would prefer not to remove all the warnings. In this case, we are totally OK with this issue, it's not a problem at all, in our application. That's why we would like to remove only this specific warning, and keep all the others.
If it's too complex to do, I think we will keep all the warnings, just in case of problems (at least in dev mode)

when to support dragging items from the parent list into a child list?

This issue is a real problem for Kanban/Trello like apps (a big use case for D&D apps I guess?). The warning is very nice, but it is pretty unclear before making an app, you realize it when it is too late. I hope this will be fixed in the near future though ๐Ÿ™

an option to opt out of all warnings will be good. Too much warning while the functionality work as per normal.

@jebarjonet brings up a great point. I'd echo others' points that the option to opt out would be highly appreciated!

Would it be okay for me to make a PR implementing this opt-out functionality? Should I open a new issue for it?

bmz1 commented

Hi @alexreardon,

are you planning to implement this feature in the near future?

kole commented

Just to clarify the core of this issue (not the dev warning complaints)... it is currently impossible to create a Trello clone (for example) with this library because you cannot have scrolling columns and a horizontally scrolling board container.

kole commented

@alexreardon That's great news! Do you anticipate this being a v11 improvement because of the complexity or might this fit into a v10 minor release? Just trying to get a very loose timeline picture. Thx

@alexreardon scrollable columns and horizontal scroll on the board

Correct me if I'm wrong, but this example from the react-beautiful-dnd example page has nested scroll containers does it not?

How does this example work without giving the error message "Droppable: unsupported nested scroll container detected" ?

I am starting to think about algorithms for solving this problem

Collection (drag start)

  • Grab all of the Droppable elements
  • Take the first one and walk up the DOM tree. If the element is a scroll container then add a data-* attribute to it. data-react-beautiful-dnd-scroll-container=${index}. Cache the element and its result during the collection
  • When the node.parentElement is null then move onto the next Droppable. If an element is found that has previously been investigated then skip the rest of the upwards search. Use any previously found scroll parent ancestry.
  • When visiting element an we also need to check to see if the element is position:fixed for our position:fixed support

At the end of the collection, a Droppable will have 0 <-> many associated scroll container indexes which match the indexes applied to data-react-beautiful-dnd-scroll-container=${index}.

Updates (scroll events)

A single scroll listener is added to the window as a capture:true listener. This will capture all scroll events.

  • If the source of a scroll event is a scroll container that has a data-react-beautiful-dnd-scroll-container attribute then trigger an action to update all relevant Droppables. One redux update for all Droppables (fixes #244). The internal algorithms will need to be updated to account for 0 <-> many scroll containers
  • If a scroll is on the window then trigger a window scroll action. Currently, this is handled by the drag handles.
  • If the source of a scroll event is a scroll container that is NOT a data-react-beautiful-dnd-scroll-container then it can be ignored

Clean up (drag end)

  • Remove all of the data-react-beautiful-dnd-scroll-container attributes
  • Remove single window event listener

Hi๏ผŒHow long will it take to get online?

@daviddworsky the example is mounted in an iframe, so it only has one scroll container inside of a window

@alexreardon Awesome! Do you know which browsers support window scroll event with capture?

Any updates on when this will be done and released?

Hi, when can this Issues be used? We have this problem.

Hello! I also ran into this warning when trying to make a board with horiztonal scroll and vertical scroll per column. However, all of the functionality seems to be working, despite the warning?

Here is my Proof-of-concept: https://73v6rvzml0.codesandbox.io/ - this is based on the example from the README. I didn't bother to fix the onDrop logic. But the general drag-and-drop works fine.

Am I missing something? Or can I just ignore this warning?

@jseminck
Hi, it looks like your example is using the window as the parent scroll container.
Maybe this example does the same? (note that the example is in an iframe)

What we want here is to use another container (like a div) instead of the browser's window, which is not supported now

Hello @nanopx, thanks for your answer!

That is correct. Actually, we also do not want it to be on the window. We need a header that needs to remain in place when scrolling horizontally on the board.

I created another example with the scroll on another container. This seems to work fine, except the scroll does not happen if you want to move an item from the first column to the last column. ๐Ÿ˜ž But the scrolling does work within a column... ๐Ÿค”

Actually, for us, the horizontal movements are more important than the vertical movements. So if I could figure out a way to do automatic horizontal scrolling when dragging left/right instead of vertical automatic scrolling, then it would be fine.

https://73v6rvzml0.codesandbox.io/

@jseminck same here, I've also tried to scroll the parent container outside of react-beautiful-dnd, but the card's placeholder gets out of position during a horizontal drag... ๐Ÿ˜ข

So if I could figure out a way to do automatic horizontal scrolling when dragging left/right instead of vertical automatic scrolling, then it would be fine.

I don't think this is possible at this moment ๐Ÿค” (sorry if I'm wrong...

If you don't need a scroll in each list, maybe this example could help?

We do need vertical scroll in the columns, but we do not care for vertical automatic scroll when a Draggable element is dragged towards the top or bottom of the list.

I will dig into the code and see if I can reverse the detection algorithm for scroll containers. Currently, I think it first checks inside the Droppable element and then it checks outside of the Droppable elements. If this can be reversed, maybe my issue can be fixed.

Additionally, I'm happy to help to solve this problem so both scroll containers would work. If there's any development already done I do not mind to take a look and continue working on it. It definitely sounds like an interesting issue, perhaps a bit outside my comfort zone, but with some guidance, I'd definitely like to take a shot at it! ๐Ÿ’ช

We will start work on this soon. It is a sizeable piece of work. I am keen to see this take flight!

Thank you @alexreardon, I really appreciate the work on this library, the egghead.io course and so on!

In the meantime, I would like to try to reverse the order in which the scroll containers are detected to solve my issue: currently automatic scrolling is working vertically but not horizontally. I would prefer it the other way around. Example: https://73v6rvzml0.codesandbox.io/

Do you think it would work if I switch the order in which scroll containers are detected by first looking outside of the Draggable?

I understand currently the library doesn't support this and probably it's not something that you want. But if this works it might be a good short-term solution for me through a fork.

A quick scan through the code and seems these are the files that would need adjusting:

https://github.com/atlassian/react-beautiful-dnd/blob/master/src/view/droppable-dimension-publisher/get-closest-scrollable.js#L70-L71
https://github.com/atlassian/react-beautiful-dnd/blob/master/src/view/droppable-dimension-publisher/check-for-nested-scroll-container.js#L7-L8

Edit: Just to confirm, this approach worked!

Awesome work @alexreardon! Loving the library.
Are there any updates on this? Did work on this awesome improvement to this masterpiece take flight?

For folks trying to achieve a Trello-style kanban, I was able to hack this together utilizing the onDragUpdate callback:

  handleOnDragUpdate = (update) => {
    const { draggableId, source, destination } = update;
    const sourceId = _.get(source, 'droppableId', null);
    const destinationId = _.get(destination, 'droppableId', null);

    if (sourceId !== destinationId) {
      const versionsContainer = document.getElementById('versions');
      const draggingItem = document.getElementById(draggableId);
      if (draggingItem) {
        const { x } = draggingItem.getBoundingClientRect();
        versionsContainer.scroll({
          top: 0,
          left: x,
          behavior: 'smooth',
        });
      }
    }
  };

Here, update is the value passed from onDragUpdate. That tells you what's moving and where it's trying to go. The sourceId and destinationId are me saying "if the these exist (source always should), use them." I use the lodash _.get() method as a safety mechanism instead of destination.destinationId, etc. which could fail if a destination isn't available.

Next, if those values exist and they're not equal (meaning, we're moving to another list), I get the scrollable container element (in my case, a div with an id of versions which wraps all of my kanban lists) and then I get the card being dragged in the list as draggingItem.

If I find a draggingItem, I grab its getBoundingClientRect() position which roughly tells me where the card is at in the dom (which conveniently updates as onDragUpdate is called).

From there, I just use the native .scroll() method on the scroll container (#versions) to augment the horizontal (x) position of the list.

Notes:

@cleverbeagle Nice one! I gave this a go, and while the window does scroll horizontally, the items are still not dropped in the correct list when you let go of the cursor.

@humphreybc unfortunately, yeah, found the same once I got a bit deeper into solving this. Had the wind in my hair when I posted this ๐Ÿ˜œ

Are there any updates on this?

hi sorry Are there any updates? How long it will take
I need to use it

I have updated the description with the current plan: #131 (comment)

Can appreciate the complexity of this feature. If it helps any, I suspect supporting the nesting of 2 scrolls would be sufficient for most use cases, for example, an outer horizontal scroll and an inner vertical scroll.

Hi guys.
I embedded a react-beautiful-dnd two column component in a material-ui dialog. It seems to work fine. However it gave me a message about unsupported nested scroll container detected and pointed me to this issue.
Is the message a cause for concern and is my approach not recommended, or is the message meant for a similar but separate case? I'm only looking to drag and drop in the DragAndDropContext container, and not in the dialog outside of it.

Thanks,
Nick

@NickEmpetvee seems that you have 2 containers that are scrollable.

@nicubarbaros Right, that's probably the material-ui dialog which houses the two-column react-beautiful-dnd component. It seems to work fine in allowing draggables to be moved from one column to another.

I'm just trying to figure out if there's a potential other issue I need to be aware of because of the message that I didn't catch in my basic drag-drop testing.

@NickEmpetvee probably your body is scrollable or any other element that wraps the material ui dialog.

@alexreardon any updates?

I forked a vertical codesandbox and played around with this and it seems to work like this.
This works for my use case hopefully it helps anyone else that has this issue .
If it's off topic I apologize

https://codesandbox.io/embed/vertical-list-lnmlr

@adriankott isn't this 1 deep level scroll?

@nicubarbaros yes it is but this was the effect I wanted to obtain , and at first thought it was this issue that caused it, posted it here in case anyone else has the same train of thought that I had.

@nicubarbaros yes it is but this was the effect I wanted to obtain , and at first thought it was this issue that caused it, posted it here in case anyone else has the same train of thought that I had.

There are two scroll containers here. So sure, there is a nested scroll container. I think the body doesn't count as a scroll container when reproducing this issue. If your main scrollable area was a div inside a non-scrolling body, you can't then have scrolling inside the lists as shown in the sandbox.

Is there an estimated date of completion for this?

I have an insane workaround for this. I'm dragging from a fixed list a relatively positioned one. On mousedown, I change the fixed div to position absolute, do the drag and drop, then mouseup change it back to fixed.

It sounds simple there, and works pretty well, but the code is quite horrible. Is this a very difficult issue, so it cannot have a date estimate?

@alexreardon Would love an update if you have a chance! More importantly, is there anything we can do to support this effort?

Hi @alexreardon thanks for your work. Is this solved in the 12.0 version? Looks like this is working here:

https://deploy-preview-1487--react-beautiful-dnd.netlify.com/?path=/story/virtual-react-window--board

@ssjunior isn't solved.

For folks trying to achieve a Trello-style kanban, I was able to hack this together utilizing the onDragUpdate callback:

  handleOnDragUpdate = (update) => {
    const { draggableId, source, destination } = update;
    const sourceId = _.get(source, 'droppableId', null);
    const destinationId = _.get(destination, 'droppableId', null);

    if (sourceId !== destinationId) {
      const versionsContainer = document.getElementById('versions');
      const draggingItem = document.getElementById(draggableId);
      if (draggingItem) {
        const { x } = draggingItem.getBoundingClientRect();
        versionsContainer.scroll({
          top: 0,
          left: x,
          behavior: 'smooth',
        });
      }
    }
  };

Here, update is the value passed from onDragUpdate. That tells you what's moving and where it's trying to go. The sourceId and destinationId are me saying "if the these exist (source always should), use them." I use the lodash _.get() method as a safety mechanism instead of destination.destinationId, etc. which could fail if a destination isn't available.

Next, if those values exist and they're not equal (meaning, we're moving to another list), I get the scrollable container element (in my case, a div with an id of versions which wraps all of my kanban lists) and then I get the card being dragged in the list as draggingItem.

If I find a draggingItem, I grab its getBoundingClientRect() position which roughly tells me where the card is at in the dom (which conveniently updates as onDragUpdate is called).

From there, I just use the native .scroll() method on the scroll container (#versions) to augment the horizontal (x) position of the list.

Notes:

@cleverbeagle Good one. Were you able to fix the droppable of cards to correct list ?
Its dropping to a different list.

@alexreardon Just want to say I love the library because it is beautiful but this nested scrolling issue is possibly why our team might switch to another dnd library unfortunately.

I'm using material UI with a Drawer component which holds a SwipeableView component, that holds a droppable list of draggable cards. I've been using portal implementation as well which correctly fixes any dragging offset but adding overflow: "auto !important" to any parent div, children, component, or the portal does not seem to helping scroll my draggables. If anyone has some not too hacky workarounds that would be appreciated. Also any updates or estimated fix date for this issue would be great.

Thanks @ArielFrischer. Nested scroll containers is a glaring thing to be worked on

I found that https://www.reactkanban.com/ was able to work around this, but after digging for some time I still don't know how he did it.

@trung2012 reactkanban uses a hack - app header and board header are position: fixed and when card is dragged window.scrollTo() is called so that whole app is scrolled horizontally, but headers stay in place because of the fixed position. This approach works in that particular layout but in more complex UIs it would not be possible to implement this behaviour.

https://github.com/markusenglund/react-kanban/blob/master/src/app/components/Board/Board.jsx#L94-L105

@RafikiTiki Oh I see. Thank you for the explanation!

kole commented

Can we get an update on this? Is this being actively worked on?

Any news @alexreardon ? Is there an ETA? Thanks for the hard work @alexreardon!

Okay, so I was able to create a quick and hacky solution for dnd in Material-UI Dialog. The solution is inspired by @cleverbeagle 's code, so thank you for that!

This code is nowhere near polished or thoroughly tested, but I decided to share it anyway since it seems to work for our setup (horizontal lists in Material-UI Dialog).

/**
 * Function to fix dragged item position. React-beautiful-dnd doesn't
 * yet support nested scroll containers, so we have to recalculate top
 * and left values of the dragged item based on modal position on screen.
 * Can be removed once https://github.com/atlassian/react-beautiful-dnd/issues/131 is fixed
 */
const handleOnDragStart = () => {
	const modal = document.getElementById('settings-dialog'); // MuiPaper-root component of Material-UI Dialog
	if (!modal) return;
	const draggables = [...modal.querySelectorAll('[data-rbd-draggable-id]')]; // [data-react-beautiful-dnd-draggable] for older react-beautiful-dnd versions
	const draggedItem = draggables.find(elem => elem.style.position === 'fixed');
	if (!draggedItem) return;
	const { top: modalTop, left: modalLeft } = modal.getBoundingClientRect();
	draggedItem.style.top = `${parseInt(draggedItem.style.top, 10) - modalTop}px`;
	draggedItem.style.left = `${parseInt(draggedItem.style.left, 10) - modalLeft}px`;
};

// Later on render
<DragDropContext onDragEnd={onDragEnd} onDragStart={handleOnDragStart}>

Basically it sets draggedItem top and left values to be relative to Dialog instead of body. Dragging items still work since react-beautiful-dnd uses transform to move dragged element on the screen.

EDIT: To be clear, this only fixes dragged element appearing in wrong place on the screen (i.e. not under cursor)

Any update on this?

I don't know if what i did solve this problem but, instead of set a <div style={{ 'overflow': auto }} /> after a Droppable tag, i set a div without overflow and a tag div with overflow, after Droppable tag, like this:

<Droppable droppableId={`${droppableId}`}>
 {droppableProvided => (
  <div ref={droppableProvided.innerRef} className={'body-item'}>
   <div className={'body-item-scrollable'}>
     {'Your <Draggable /> ...'}
    </div>
  </div>
 )}
</Droppable>
// Set overflow here -> remove DragDropContext auto-scroll (Pay attention)!
        .body-item {
          display: flex;
          width: 100%;
          height: 90%;
          align-items: center;
          flex-direction: column;
          justify-content: flex-start;

          @media (max-height: 900px) {
            height: 84%;
          }

          .body-item-scrollable {
            display: flex;
            overflow: auto;
            align-items: center;
            padding: 10px 15px 0;
            flex-direction: column;
          }
        }

Then works! :)

@kaue-esparta, what version of react-beautiful-dnd are you using?

    "react-beautiful-dnd": "10.1.1",

@NickEmpetvee

Solving this problem is very important for me. What's news? ))

Will next version have this issue resolved?

@alexreardon Just want to say I love the library because it is beautiful but this nested scrolling issue is possibly why our team might switch to another dnd library unfortunately.

I'm using material UI with a Drawer component which holds a SwipeableView component, that holds a droppable list of draggable cards. I've been using portal implementation as well which correctly fixes any dragging offset but adding overflow: "auto !important" to any parent div, children, component, or the portal does not seem to helping scroll my draggables. If anyone has some not too hacky workarounds that would be appreciated. Also any updates or estimated fix date for this issue would be great.

Hi @ArielFrischer ,
Which lib are you using?

@Iuriy-Budnikov We are using material-UI, we've been able to figure out how to make things work by scrapping the swipeable view, and using the new portal implementation as well which is quite handy.

Does someone know an alternative lib which is supports nested columns?

Does someone know an alternative lib which is supports nested columns?

I ended up using React Smooth DnD, wrote about the limitations of it compared to beautiful-dnd here

Does someone know an alternative lib which is supports nested columns?

I ended up using React Smooth DnD, wrote about the limitations of it compared to beautiful-dnd here

Very nice article @GraemeFulton. Thanks!

+1 Waiting for this feature

+1 for the feature, @alexreardon thanks for the amazing library just wanted to know if we still have this feature in the pipeline?

We had the problem that we needed multiple horizontally scrollable containers + a vertical scroll on the body. Our team addressed this issue by changing the CSS overflow: scroll property to overflow: visible on the horizontal scroll containers (kanban boards) inside the onBeforeCapture lifecycle method. Because of that, we were able to move Draggables to other scrollable containers out of the current viewport. Maybe this can help some of you. If you want to see this in action, check out https://app.workstreams.ai.

+1, waiting for nested scrolls. Any updates ??

+1, waiting for this

+1 waiting for this, any updates?

Is there a timeline for this feature, and are there any workarounds that might work?

ezgif com-video-to-gif
If the edges of the container/div are fixed/visible then the scrolling (simple and nested both) works fine in it

How is the react-kanban repo achieving dnd with nested scrollable containers?

How is the react-kanban repo achieving dnd with nested scrollable containers?

#131 (comment)

+1 , any update or some timeline would be helpful ?

I'm starting to suspect that leaving +1 comments might not be accomplishing anything y'all! I'm sure Alex will update us when they are able to. There's a lot going on in the world right now and I think we could all do to be patient or put our heads together on how we could help.

Might I suggest hitting the "Subscribe" button on the sidebar on the right instead to be notified when Alex has something to update us on? ๐Ÿ’ƒ

I'd love to help implement this feature. I use this project for two of my own apps!

Any extra information you can spell out to implement this feature would be appreciated!

@hpennington a few weeks ago when I encountered the problem in my side project I dived into the source code and I found there is a function that is looking for closest scroll parent. The scrolling is performed either on the window or on that scroll parent. If we could detect all scroll parents and put into an array, then scroll each step by step (depending on where the mouse pointer is) we could achieve expected behavior.

I need that feature too. I would be happy to help.

Is there any slack/discord or another communicator for this project?

In my project, I have a "position: fixed" box with a vertical scroll bar and that box contains a Droppable area. I'm getting the warning about nested scrollable areas, but the Droppable works perfectly fine because there is only one truly scrollable box surrounding the Droppable. I suspect scrollable detection should stop when it traverses to a "position: fixed" parent.