dnd-kit
dnd-kit copied to clipboard
React 18: Autoscroll is broken with multiple horizontal drop zones which have vertical scroll
Hi,
I'm seeing this behavior only after updating to React 18. I was using 6.0.5, updating dnd-kit to 6.0.8 does not resolve this.
Setup
I have a horizontal, scrollable list of "useDroppable" columns. Each column contains a vertical list of "useDraggable" items. When the column contents' CSS overflow is set to auto, scroll, or hidden, this behavior occurs. If I change it to none, scrolling works as it did previously.
Behavior
When I try to drag an item from one column to another, as soon as the item is over a different drop zone, the main container (of columns) continues to scroll horizontally all the way to the end. I cannot drag in the other direction to scroll it back.
Changing DndContext's autoScroll with various options of order and acceleration has no effect.
Sandbox Eample
https://codesandbox.io/s/stoic-robinson-wgcrcx?file=/src/App.jsx There are no handlers attached to DndContext, no underlying data is changed, so this appears to be contained within autoScroll.
Try dragging from a middle column to see the "unable to reverse" behavior. Search for "changeme" in App.js to find the relevant style change in the Column component. In my app, this style is set by a CSS file instead of inline, and the behavior is the same.
Note: I'm not using Sortable here because the column contents have a strict order defined by the server. https://github.com/clauderic/dnd-kit/issues/1098 might be related, but that's for Sortable so I'm not sure.
I have confirmed that by loading my app the old way, with render(app, mountPoint) from the "react-dom" package to get React 17 behavior, this bug goes away. This is not a valid workaround though, as this method is not supported in React 18.
I encountered the same issue. Does anyone have a solution for React 18?
Same here. @ThomDevine have you found a workaround yet?
@alexdonets No, we are still stuck on React 17 because of this issue.
I poked around in the library code to try and figure it out, which is how I came across the order option for autoScroll; it's not found in the docs. I expected setting that to TreeOrder.ReversedTreeOrder
to work, that seems like what it's designed for, but it didn't.
In our use case, some columns might have 3 items and others 300, so we need this functionality. I have no idea why React 18's way of rendering breaks this.
has same issue
I've just encountered this bug as well and can't find anyway work around.
So this is my super hacky DIY fix.
1. First disable auto scroll
<DndContext autoScroll={false}>
<ScrollableSection />
</DndContext>
2. Make sure your horizontal scrollable element is inside DndContext in another component like ScrollableSection
.
3. Inside of ScrollableSection
do your auto scrolling
const ScrollableSection = ({columns}) => {
const { active } = useDndContext()
const sectionRef = useRef(null)
const [scrollDirection, setScrollDirection] = useState(null)
// this scrolls the section based on the direction
useEffect(() => {
if (!scrollDirection) return
const el = sectionRef.current
if (!el) return
const speed = 10
const intervalId = setInterval(() => {
el.scrollLeft += speed * scrollDirection
}, 5)
return () => {
clearInterval(intervalId)
}
}, [scrollDirection, sectionRef.current])
// if we are dragging, detect if we are near the edge of the section
useEffect(() => {
const handleMouseMove = (event) => {
const el = sectionRef.current
if (!active || !el) return
const isOverflowing = el.scrollWidth > el.clientWidth
if (!isOverflowing) return
// get bounding box of the section
const { left, right } = el.getBoundingClientRect()
// xPos of the mouse
const xPos = event.clientX
const threshold = 200
const newScrollDirection = xPos < left + threshold ? -1 : xPos > right - threshold ? 1 : null
if (newScrollDirection !== scrollDirection) {
setScrollDirection(newScrollDirection)
}
}
if (active) {
window.addEventListener('mousemove', handleMouseMove)
} else {
window.removeEventListener('mousemove', handleMouseMove)
setScrollDirection(null)
}
return () => {
window.removeEventListener('mousemove', handleMouseMove)
setScrollDirection(null)
}
}, [active, sectionRef.current])
return (
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
overflowX: 'auto',
}}
ref={sectionRef}
>
{columns.map(({ id }) => {
return (
<Column
key={id}
id={id}
/>
)
})}
</div>
)
}
You might want to do some throttling on that mouse event listener to improve performance.
If anyone has any other way of cleaning this up please feel free to!
So this is my super hacky DIY fix.
1. First disable auto scroll
<DndContext autoScroll={false}> <ScrollableSection /> </DndContext>
2. Make sure your horizontal scrollable element is inside DndContext in another component like
ScrollableSection
.3. Inside of
ScrollableSection
do your auto scrollingconst ScrollableSection = ({columns}) => { const { active } = useDndContext() const sectionRef = useRef(null) const [scrollDirection, setScrollDirection] = useState(null) // this scrolls the section based on the direction useEffect(() => { if (!scrollDirection) return const el = sectionRef.current if (!el) return const speed = 10 const intervalId = setInterval(() => { el.scrollLeft += speed * scrollDirection }, 5) return () => { clearInterval(intervalId) } }, [scrollDirection, sectionRef.current]) // if we are dragging, detect if we are near the edge of the section useEffect(() => { const handleMouseMove = (event) => { const el = sectionRef.current if (!active || !el) return const isOverflowing = el.scrollWidth > el.clientWidth if (!isOverflowing) return // get bounding box of the section const { left, right } = el.getBoundingClientRect() // xPos of the mouse const xPos = event.clientX const threshold = 200 const newScrollDirection = xPos < left + threshold ? -1 : xPos > right - threshold ? 1 : null if (newScrollDirection !== scrollDirection) { setScrollDirection(newScrollDirection) } } if (active) { window.addEventListener('mousemove', handleMouseMove) } else { window.removeEventListener('mousemove', handleMouseMove) setScrollDirection(null) } return () => { window.removeEventListener('mousemove', handleMouseMove) setScrollDirection(null) } }, [active, sectionRef.current]) return ( <div style={{ height: '100%', width: '100%', display: 'flex', overflowX: 'auto', }} ref={sectionRef} > {columns.map(({ id }) => { return ( <Column key={id} id={id} /> ) })} </div> ) }
You might want to do some throttling on that mouse event listener to improve performance.
If anyone has any other way of cleaning this up please feel free to!
Thanks to @Innders for the solution, I also wanted to DIY my own scroll, but my business is that there are multiple boards, each board requires virtual scrolling vertically, each board has multiple columns, and each column also requires virtual scrolling, It's so hard
Well I don't know if this "less hacky" or even better performance-wise, but I had to disable vertical autoscrolling, while allowing horizontal autoscrolling to proceed. To disable vertical auto scrolling:
(<DndContext
autoScroll={{
threshold: {
x: 0.2,
y: 0
}
}}
...otherProps
>
...
<div>
<DragOverlay
style={{
// important for vertical custom scrolling
pointerEvents: 'none'
}}
>
</DragOverlay>
</div>
</DndContext>)
Then to add manual vertical scrolling to a container:
const [isNearTop, setIsNearTop] = useState(false)
const [isNearBottom, setIsNearBottom] = useState(false)
const isDragging = useRef(false)
useDndMonitor({
onDragStart(e) {
isDragging.current = true
},
onDragEnd(e) {
isDragging.current = false
}
})
function handleMouseMove(e: React.MouseEvent<HTMLDivElement>) {
if (containerRef.current == null) {
return
}
const { top, bottom } = containerRef.current.getBoundingClientRect()
const nearTop = e.clientY - top < 50
const nearBottom = bottom - e.clientY < 50
setIsNearTop(nearTop)
setIsNearBottom(nearBottom)
}
useEffect(() => {
if (!isDragging.current) {
return
}
const container = containerRef.current
if (!container) return
const scrollAmount = 20
const scrollContainer = () => {
if (isNearTop) {
container.scrollTop -= scrollAmount
} else if (isNearBottom) {
container.scrollTop += scrollAmount
}
}
const intervalId = setInterval(scrollContainer, 20)
return () => clearInterval(intervalId)
}, [isNearTop, isNearBottom])
<div
onMouseMove={handleMouseMove}
style={{
...overflowHeightAndOtherPropsToEnableScroll
}}
>
</div>
In order to stop scrolling when the mouse is located in the center. I have added these lines in the useEffect that sets the scroll direction:
else {
setScrollDirection(null)
}
1. First disable auto scroll
<DndContext autoScroll={false}>
<ScrollableSection />
</DndContext>
2. Make sure your horizontal scrollable element is inside DndContext in another component like ScrollableSection
.
3. Inside of ScrollableSection
do your auto scrolling
const ScrollableSection = ({columns}) => {
const { active } = useDndContext()
const sectionRef = useRef(null)
const [scrollDirection, setScrollDirection] = useState(null)
// this scrolls the section based on the direction
useEffect(() => {
if (!scrollDirection) return
const el = sectionRef.current
if (!el) return
const speed = 10
const intervalId = setInterval(() => {
el.scrollLeft += speed * scrollDirection
}, 5)
return () => {
clearInterval(intervalId)
}
}, [scrollDirection, sectionRef.current])
// if we are dragging, detect if we are near the edge of the section
useEffect(() => {
const handleMouseMove = (event) => {
const el = sectionRef.current
if (!active || !el) return
const isOverflowing = el.scrollWidth > el.clientWidth
if (!isOverflowing) return
// get bounding box of the section
const { left, right } = el.getBoundingClientRect()
// xPos of the mouse
const xPos = event.clientX
const threshold = 200
const newScrollDirection = xPos < left + threshold ? -1 : xPos > right - threshold ? 1 : null
if (newScrollDirection !== scrollDirection) {
setScrollDirection(newScrollDirection)
//Stops scrolling
} else {
setScrollDirection(null)
}
}
if (active) {
window.addEventListener('mousemove', handleMouseMove)
} else {
window.removeEventListener('mousemove', handleMouseMove)
setScrollDirection(null)
}
return () => {
window.removeEventListener('mousemove', handleMouseMove)
setScrollDirection(null)
}
}, [active, sectionRef.current])
return (
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
overflowX: 'auto',
}}
ref={sectionRef}
>
{columns.map(({ id }) => {
return (
<Column
key={id}
id={id}
/>
)
})}
</div>
)
}