motion icon indicating copy to clipboard operation
motion copied to clipboard

[FEATURE] allow scrolling of parent div with Reorder Items

Open augustblack opened this issue 4 years ago • 18 comments
trafficstars

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!

augustblack avatar Nov 07 '21 19:11 augustblack

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

rbourdon avatar Nov 10 '21 13:11 rbourdon

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:

https://5fc05e08a4a65d0021ae0bf2-ejxxqkfwok.chromatic.com/?path=/story/presets-sortable-vertical--scroll-container

augustblack avatar Nov 10 '21 16:11 augustblack

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.

rbourdon avatar Nov 11 '21 15:11 rbourdon

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

hoangbn avatar Jan 07 '22 19:01 hoangbn

Is there any update?

tienne avatar Jun 19 '22 02:06 tienne

Has anyone found a workaround besides using Dnd?

ljones87 avatar Sep 28 '22 03:09 ljones87

What is the status on a feature like this or #1493

MagnusHJensen avatar Sep 28 '22 07:09 MagnusHJensen

Any update on this?

piotrjanosz avatar Dec 07 '22 15:12 piotrjanosz

Are there any update on this issue?

sethdumaguin avatar Feb 14 '23 23:02 sethdumaguin

Did anyone come up with some solutions?

maderesponsively avatar Jul 19 '23 17:07 maderesponsively

Any updates on this?

acrylicode avatar Feb 01 '24 12:02 acrylicode

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?

kauffecup avatar May 20 '24 20:05 kauffecup

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 avatar May 21 '24 00:05 thatsjonsense

@thatsjonsense confirming that that did the trick! thank you!

kauffecup avatar May 21 '24 18:05 kauffecup

@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.

ajayvignesh01 avatar Aug 02 '24 20:08 ajayvignesh01