Open augustblack opened 2 years ago
You can add layoutScroll to your scrolling parent element to fix this. https://codesandbox.io/s/framer-motion-5-drag-to-reorder-lists-forked-u3v91?file=/src/App.tsx:486-498
Hi , thanks so much for taking a look!
I'm not sure I understand though. It doesn't appear like the layoutScroll has any effect. I'm still seeing the same issue in your example (unless I am missing something).
When you drag an Item to the bottom or top, I would expect it to auto scroll the parent like this:
Hey, sorry I didn't read your issue closely enough, I thought you were talking about the fact that in the example if you make the parent container overflow with scroll, then drag an item around, scroll the window, and try to drag another item, the items in the list jump around erratically and layoutScroll
fixes that.
I had a solution for this in framer 4, but it does not work in framer 5 with Reorder component. Would be nice to have Reorder scrollable component to behave like in 5fc05e08a4a65d0021ae0bf2-ejxxqkfwok.chromatic.com/?path=/story/presets-sortable-vertical--scroll-container. as it is the behaviour users expect
Is there any update?
What is the status on a feature like this or #1493
Any update on this?
Are there any update on this issue?
Did anyone come up with some solutions?
Any updates on this?
In the interim of waiting on a solution for the feature request, has anyone come up with a workaround? Or is this a hard stop?
I was really hoping this would get properly fixed by now, but here's a hacky workaround we've been using with some success. It reaches into the internals of dragging to mimic the scroll.
import { DragControls, PanInfo, Point, useDragControls } from 'framer-motion'
import { useCallback, useRef } from 'react'
const SCROLL_THRESHOLD = 25 // How close to the edge of the scroll container to start scrolling
const SCROLL_AMOUNT = 5 // How many pixels to move per scroll event
export const useHandleReorderScroll = () => {
const dragControls = useDragControls()
const scrollerRef = useRef<HTMLDivElement>(null)
const scrollStartRef = useRef<number>() // scrollTop when the drag starts
const dragStartRef = useRef<number>() // y position of the cursor when the drag starts
const draggingEltControls = useRef<VisualElementDragControls>()
const onScroll = useCallback((ev: UIEvent) => {
if (!draggingEltControls.current) return
const startPoint = getDragStartPoint(draggingEltControls.current)
const target = ev.target as HTMLElement | null
if (
!startPoint ||
!target ||
scrollStartRef.current === undefined ||
dragStartRef.current === undefined
) {
return
}
const scrollDistance = target.scrollTop - scrollStartRef.current // Distance from where the drag started
startPoint.y = dragStartRef.current - scrollDistance // Move the startPoint to account for the scroll
}, [])
const onDrag = useCallback((ev: Event, info: PanInfo) => {
const scrollContainer = scrollerRef.current
if (!scrollContainer) return
const scrollContainerRect = scrollContainer.getBoundingClientRect()
const dragPoint = info.point.y
// Check if target is the last elt in its parent container
const eventTarget = ev.target
if (!(eventTarget instanceof Element)) return
const item = eventTarget.closest('[draggable]')
const parent = item?.parentElement
if (!parent) return
if (
dragPoint < scrollContainerRect.top + SCROLL_THRESHOLD &&
(item !== parent.firstElementChild || scrollContainer.scrollTop > 0)
) {
// User is dragging card to the top of the scroll container
scrollContainer.scrollTop -= SCROLL_AMOUNT
} else if (
dragPoint > scrollContainerRect.bottom - SCROLL_THRESHOLD &&
(item !== parent.lastElementChild ||
scrollContainer.scrollTop < scrollContainer.scrollHeight)
) {
// User is dragging card to the bottom of the scroll container
scrollContainer.scrollTop += SCROLL_AMOUNT
}
}, [])
// Track the scroll distance by capturing scrollTop when the drag starts
const onDragStart = useCallback(() => {
const scroller = scrollerRef.current
const controls = findDraggingElementControls(dragControls)
if (!scroller || !controls) return
draggingEltControls.current = controls
scrollStartRef.current = scroller.scrollTop
const startPoint = getDragStartPoint(controls)
if (!startPoint) return
dragStartRef.current = startPoint.y
}, [dragControls])
const onDragEnd = useCallback(() => {
scrollStartRef.current = undefined
dragStartRef.current = undefined
draggingEltControls.current = undefined
}, [])
return {
// On the scrolling container
scrollerRef,
onScroll,
// On the item
dragControls,
onDrag,
onDragStart,
onDragEnd,
}
}
// A private Framer class that we're just using one little piece of
// https://github.com/framer/motion/blob/fb227f8b700c6e2d9c3b33d0a2af1a2d2b7849e9/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts#L53
type VisualElementDragControls = {
panSession: {
history: Point[]
}
}
const findDraggingElementControls = (dragControls: DragControls) => {
try {
return Array.from<VisualElementDragControls>(
// @ts-ignore - we're reaching into a private prop
// https://github.com/framer/motion/blob/fb227f8b700c6e2d9c3b33d0a2af1a2d2b7849e9/packages/framer-motion/src/gestures/drag/use-drag-controls.ts#L29
dragControls.componentControls
).find((c: any) => c.isDragging)
} catch (err) {
// If this private prop moves in the future, we'll start logging errors here
console.error('[caught] findDraggingElementControls', err)
return
}
}
const getDragStartPoint = (
controls: VisualElementDragControls
): Point | undefined => {
try {
// https://github.com/framer/motion/blob/fb227f8b700c6e2d9c3b33d0a2af1a2d2b7849e9/packages/framer-motion/src/gestures/pan/PanSession.ts#L257
return controls.panSession.history[0]
} catch (err) {
// If this private prop moves in the future, we'll start logging errors here
console.error('[caught] getDraggingElementStartPoint', err)
return
}
}
@thatsjonsense confirming that that did the trick! thank you!
@thatsjonsense Where would you call this hook from? I have my Reorder.Group in the parent, and Reorder.Item as a child under a map. When I put your hook in the parent, it seems like all the child Reorder.Items are getting the same dragControls and therefore only the first Reorder.Item is moving.
Is your feature request related to a problem? Please describe. A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
There is an issue when a Reorder.Group has a number of items that overflows out of the Reorder.Group or a parent element.
see here: https://codesandbox.io/s/framer-motion-5-drag-to-reorder-lists-forked-beoy0
I would expect that when I drag a visible item towards the bottom of the Reorder.Group or parent div, that it would scroll with the cursor so that you can position the element.
Describe the solution you'd like
I'm not sure what the solution is. Would it be possible to use the drag controls somehow to achieve this?
Describe alternatives you've considered
One solution is to instead use dnd-kit that handles this.
thanks for the great library and for your consideration!