3

I have a dashboard, with multiple DnD elements and a fixed AppBar on top. I found out that if you drag the element to the top of the page (when you've already scrolled the page down a bit) it doesn't scroll upwards, but if you remove the position: fixed attribute from the AppBar, it does. So the fixed AppBar "blocks" the HTML5 backed from scrolling the page.
I already checked and it's not a z-index problem. It has to do with the position: fixed attribute so with the CSS Stacking Context
I'm using react-dnd and MaterialUi.
Anyone encountered anything like this before?
Edit:
I have tried both react-dnd-scrollzone and the frontend-collective fork. Both are not supported anymore and neither of them worked when I downgrade my react-dnd version.
By the way, I'm using react-dnd version 10.0.2.

Amit Toren
  • 351
  • 3
  • 13

3 Answers3

1

Here is a solution for mobile, with some optional enhancements. If the dragged item gets within 100px of the top or bottom it will start scrolling. It is not specific to react-dnd. You just need to have a global isDragging state (mine is stored in Redux). (e.g. set isDragging = true on beginDrag and isDragging = false on endDrag.)

I found the suggestions for react-dnd to be confusing and vary a lot between different versions of the library. None of the solutions that used the dragover browser event work on mobile, i.e. when using the TouchBackend.

Useful discussion: https://github.com/react-dnd/react-dnd/issues/553

Basic Example

// number of pixels near the top or bottom edge when auto scroll will begin
const DISTANCE_FROM_EDGE = 100

// number of pixels that are scrolled on each touchmove event
// this could be replaced with an easing function
const SCROLL_RATE = 1

/** Handles auto scroll on drag near the edge of the screen on mobile. */
const onTouchMove = e => {
  // global isDragging state that must be set to true on beginDrag and false on endDrag
  if (isDragging()) {

    // distance of touch from top of screen
    const y = e.touches[0].clientY

    // scroll down
    if (y < DISTANCE_FROM_EDGE) {
      window.scrollTo(0, document.documentElement.scrollTop + SCROLL_RATE)
    }
    // scroll up
    else if (y > window.innerHeight - DISTANCE_FROM_EDGE) {
      window.scrollTo(0, document.documentElement.scrollTop - SCROLL_RATE)
    }
  }
}

window.addEventListener('touchmove', onTouchMove)

Example with easing

You may want it to scroll faster the closer you get to the edge. A cubic easing function works well.

// number of pixels near the top or bottom edge when auto scroll will begin
const DISTANCE_FROM_EDGE = 100

// cubic easing function for a more natural-feeling scroll speed
const ease = n => Math.pow(n, 3)

/** Handles auto scroll on drag near the edge of the screen on mobile. */
const onTouchMove = e => {
  // global isDragging state that must be set to true on beginDrag and false on endDrag
  if (isDragging()) {

    // distance of touch from top of screen
    const y = e.touches[0].clientY

    // scroll down
    if (y < DISTANCE_FROM_EDGE) {
      const rate = ease(1 + (DISTANCE_FROM_EDGE - y) / DISTANCE_FROM_EDGE)
      window.scrollTo(0, document.documentElement.scrollTop - rate)
    }
    // scroll up
    else if (y > window.innerHeight - DISTANCE_FROM_EDGE) {
      const rate = ease(1 + (y - window.innerHeight + DISTANCE_FROM_EDGE) / DISTANCE_FROM_EDGE)
      window.scrollTo(0, document.documentElement.scrollTop + rate)
    }
  }
}

window.addEventListener('touchmove', onTouchMove)

Example with continuous scroll

A limitation with the above solution is that it only auto scrolls when it is receiving touchmove events. If the user pauses their movement, but they are still dragging, the scroll stops. An improvement would be to scroll continuously when the user is dragging near the edge. I abstracted out an autoscroll function that handles starting, stopping, and updating the rate.

/** An autoscroll function that will continue scrolling smoothly in a given direction until autoscroll.stop is called. Takes a number of pixels to scroll each iteration. */
const autoscroll = (() => {
  /** Cubic easing function. */
  const ease = (n: number) => Math.pow(n, 3)

  // if true, the window will continue to be scrolled at the current rate without user interaction
  let autoscrolling = false

  // scroll speed (-1 to 1)
  let rate = 1

  /** Scroll vertically in the direction given by rate until stop is called. */
  const scroll = () => {
    window.scrollTo(0, document.documentElement.scrollTop + rate)
    window.requestAnimationFrame(() => {
      if (autoscrolling) {
        scroll()
      }
    })
  }

  /** Starts the autoscroll or, if already scrolling, updates the scroll rate (-1 to 1). */
  const startOrUpdate = (rateNew: number) => {
    // update the scroll rate
    rate = ease(rateNew)

    // if we are already autoscrolling, do nothing
    if (autoscrolling) return

    // otherwise kick off the autoscroll
    autoscrolling = true
    scroll()
  }

  /** Stops scrolling. */
  startOrUpdate.stop = () => {
    autoscrolling = false
  }

  return startOrUpdate
})()

// number of pixels near the top or bottom edge when auto scroll will begin
const DISTANCE_FROM_EDGE = 100

/** Handles auto scroll on drag near the edge of the screen on mobile. */
const onTouchMove = e => {
  // global isDragging state that must be set to true on beginDrag and false on endDrag
  if (isDragging()) {

    // distance of touch from top of screen
    const y = e.touches[0].clientY

    // scroll down
    if (y < DISTANCE_FROM_EDGE) {
      const rate = 1 + (DISTANCE_FROM_EDGE - y) / DISTANCE_FROM_EDGE
      autoscroll(-rate)
    }
    // scroll up
    else if (y > window.innerHeight - DISTANCE_FROM_EDGE) {
      const rate = 1 + (y - window.innerHeight + DISTANCE_FROM_EDGE) / DISTANCE_FROM_EDGE
      autoscroll(rate)
    }
    // stop scrolling when not near the edge of the screen
    else {
      autoscroll.stop()
    }

  }
}

window.addEventListener('touchmove', onTouchMove)
Raine Revere
  • 30,985
  • 5
  • 40
  • 52
0

Have been encountering this issue also, it does appear to be a stacking context issue but I am unable to come to a CSS solution.

react-dnd-scrollzone may have worked to solved thisbut is not being supported anymore, the frontend-collective forked it and have updated it to support react-dnd 7. However I have not tested this and am instead opting to implement this functionality from scratch.

Dahmon
  • 1
  • 1
  • 1
0

Came by this when I was having the same issue.

I ended up using some code I ran into earlier when implementing my own drag and drop. Link

let stop = true;

const scroll = (step) => {

    window.scrollBy(0, step, "smooth");
      
    if (!stop) {
  
      setTimeout(function () { 
        scroll(step) }, 20
      );
  
    }
  
}

const handleDragEnter = (e) => {

     stop = false;
     scroll(-10);
    
}

const handleDragLeave = (e) => {

    stop = true;
    
}

Assign handleDragEnter and handleDragLeave to your fixed component. When you drag over it, it will scroll.