framer / motion

Open source, production-ready animation and gesture library for React
https://framer.com/motion
MIT License
23.72k stars 805 forks source link

[BUG] Stop scrolling from interfering with dragging #185

Closed danprince closed 4 years ago

danprince commented 5 years ago

Is your feature request related to a problem? Please describe. If you render a list of drag="x" components and try to scroll down on a touch device, a small amount of horizontal drag happens on each element that your finger touches.

Describe the solution you'd like A generic solution could be to allow the variant styles to override the styles from the drag.

let variants = {
  // this would override whatever x value the drag gesture is providing
  static: { x: 0 },
  swiping: { },
};

let Swipeable = () => {
  let [isSwiping, setIsSwiping] = useState(0);

  let onDrag = (event, info) => setIsSwiping(info.offset.x > 10);

  return (
    <motion.div drag="x" animate={isSwiping ? "swiping" : "static"} onDrag={onDrag} />
  );
}

Describe alternatives you've considered I also tried overriding by passing transform styles directly to the motion.div element.

A more specific version of this might be adding a dragThreshold prop that would specify the offsets that the gesture must pass before the onDragStart event is called.

Another option might be having some way to cancel the animation (but not the event) from the onDrag handler.

mattgperry commented 5 years ago

Are you finding that scrolling is happening at all when you drag on a component? It should be blocked - ideally we'd only block if you touched in the same direction as defined in drag but there's been issues getting this to work reliably.

You can combine dragDirectionLock with a drag axis so at least, for now, it shouldn't move if the user is swiping in the opposite direciton

<motion.div dragDirectionLock drag="x" />
danprince commented 5 years ago

Thanks! That's at least a step in the right direction :+1:

I had been testing by scrolling in screen space below the list, but that's actually a much bigger issue if/when the list occupies the whole screen. Guessing there's not a way to unblock scrolling right now then?

mattgperry commented 5 years ago

Not at the moment sadly. I believe there might be a possible fix. But the problem ultimately is that to detect whether a user is dragging and in which direction we need to block the touch event from scrolling, or at least start blocking it before scrolling. But there's a distance a user can swipe within a single event that initiates scrolling. Once it's started, it's unblockable. Mobile browsers 👍

danprince commented 5 years ago

What about allowing the user to control whether blockViewportScroll() is called, via a prop?

In my case, there's no horizontal scroll space available, so a drag on the x axis isn't going to affect it. Scrolling on the y axis shouldn't interfere because of the direction lock, right?

dcecile commented 5 years ago

I have a motion component that I want to drag on "x" for animations, but it takes enough of the screen on mobile that I want users to be able to drag it on "y" for scrolling. Do you have any advice or workaround ideas right now?

thebuilder commented 5 years ago

I just build a nice slider using the drag="x" functionality, when I ran into this issue. The component takes up most of the viewport, so you should be able to scroll down the page without trigging the drag effect.

Had to look into blockViewportScroll(), and it seems like a really aggressive method of preventing scrolling while dragging, since we have no way of controlling it.

I know this is one of those difficult issues, and we can agree that you don't want scrolling to occur while dragging.

I made "fix" that determines if we are scrolling or dragging based on the initial velocity in onDragStart - but it's not perfect. Might be better if it could sample a few events before locking it.

function Example() {
  const [allowScroll, setAllowScroll] = useState(false)
  useEffect(() => {
    if (allowScroll) {
      const handleTouch = event => {
        event.stopPropagation()
      }
      document.documentElement.addEventListener('touchmove', handleTouch)
      return () => {
        document.documentElement.removeEventListener('touchmove', handleTouch)
      }
    }
  }, [allowScroll])

  return <motion.div drag="x" onDragStart={(event, info) => {
          setAllowScroll(Math.abs(info.delta.y) > Math.abs(info.delta.x))
        }}
  />
}
brunocrosier commented 4 years ago

Not sure if I fully understand why this can't currently work - but for anybody who happens upon this thread looking for a solution, I did notice that the react-swipeable-views package does seem to achieve this to some extent.

What I would find useful is the ability to scroll vertically but at the same time use framer-motion for dragging on the x-axis. Perhaps it could be opt-in to avoid the problem mentioned by @InventingWithMonster ?

I think this is quite a common use case for many UIs (eg Tinder-style "swipe" interfaces)

brunocrosier commented 4 years ago

Alternatively, for anyone looking for vertical scrolling while being able to drag an element horizontally, you can use react-spring. I have set up a quick codesandbox example:

https://codesandbox.io/s/react-use-gesture-simple-ou6dt

alilishan-omniphics commented 4 years ago

Has this been implemented yet? I am also facing the same issue ... I have a list of items that is longer than the screen and need to scroll down but he drag="x" keeps preventing it

Jeyloh commented 4 years ago
function Example() {
  const [allowScroll, setAllowScroll] = useState(false)
  useEffect(() => {
    if (allowScroll) {
      const handleTouch = event => {
        event.stopPropagation()
      }
      document.documentElement.addEventListener('touchmove', handleTouch)
      return () => {
        document.documentElement.removeEventListener('touchmove', handleTouch)
      }
    }
  }, [allowScroll])

  return <motion.div drag="x" onDragStart={(event, info) => {
          setAllowScroll(Math.abs(info.delta.y) > Math.abs(info.delta.x))
        }}
  />
}

Thank you very much, this solved my Carousel issue ❤ Have you seen any issues with it? The X drag was hit a little bit in responsiveness, but it's worth being able to scroll.

FlorinSenoner commented 4 years ago

Thank you very much, this solved my Carousel issue ❤ Have you seen any issues with it? The X drag was hit a little bit in responsiveness, but it's worth being able to scroll.

Do you have a working example of this? Can't get this to work in a reliable manner

k-ode commented 4 years ago

I have this that used to work (in 1.6.7), but not in the latest version.

const Example = () => {
    const clientYStart = useRef(null);
    const clientXStart = useRef(null);
    const horizontalScrollStart = useRef(0);
    const isDragging = useRef(false);
    const isScrolling = useRef(false);

    return (
        <div
            onTouchStart={e => {
                // Track current direction
                const touch = e.targetTouches[0];
                clientYStart.current = touch.clientY;
                clientXStart.current = touch.clientX;
                horizontalScrollStart.current = horizontalScroll.get();
            }}
            onTouchMove={e => {
                const touch = e.targetTouches[0];
                const deltaY = Math.abs(touch.clientY - clientYStart.current);
                const deltaX = Math.abs(touch.clientX - clientXStart.current);
                if (isDragging.current) {
                    return;
                }
                if (isScrolling.current) {
                    e.stopPropagation();
                    return;
                }
                if (deltaX > deltaY) {
                    isDragging.current = true;
                } else {
                    isScrolling.current = true;
                    e.stopPropagation();
                }
            }}
            onTouchEnd={e => {
                if (isScrolling.current) {
                    horizontalScroll.set(horizontalScrollStart.current);
                }
                isDragging.current = false;
                isScrolling.current = false;
            }}>
            {items.map(item => (
                <motion.div drag={'x'} style={{ x: horizontalScroll }}></motion.div>
            ))}
        </div>
    );
};
sparlos commented 4 years ago

I'm also dealing with this right now. I have drag='x' with dragDirectionLock enabled on an element that can have overflow-y which is set to scroll, but on mobile I'm not able to scroll it. Perhaps implementing a way to cancel a drag in progress would be helpful? For example, a way to cancel the drag in the onDirectionLock callback if the axis returned is 'y'. Not sure if that would be feasible though; just spitballing.

wintercounter commented 4 years ago

I just want to maintain body scroll, but doesn't matter if i set dragDirectionLock or not, it won't allow me scroll up/down on mobile. I simply use drag="x".

dimitriirybakov commented 4 years ago

As temporary solution we use react-use-gesture instead of drag properties and animate everything with framer-motion.

mattgperry commented 4 years ago

@dimitriirybakov Can you show the basic pattern how you use that to fix this? I am not above copying as this one has stumped me.

dimitriirybakov commented 4 years ago

@InventingWithMonster well, it's basically something like this

const x = useMotionValue(0);

const bind = useDrag((state) =>  x.set(state.movement[0]));

return <div style={{ x }} {...bind()}/>;
Weffe commented 4 years ago

Can confirm I also have this issue as well. Is this issue on the roadmap of getting fixed in the next major/minor version?

oygen87 commented 4 years ago

i also have this issue with a list taking all my mobile screen and i cant scroll y-axis because of the listItems having drag=x.. any progress?

zephy20 commented 4 years ago

Has anyone been able to solve this? Been stuck on this for hours.

danilockthar commented 4 years ago

style={{ x }} {...bind()}

Hi! is there a way to make this work with motion dragcontraints? im tried this and the scrolling seems to work along with the x drag, but i have to take out the framer motion "drag" property to acomplish. This breaks the dragContraints property.

BenjaminCorey commented 4 years ago

style={{ x }} {...bind()}

Hi! is there a way to make this work with motion dragcontraints? im tried this and the scrolling seems to work along with the x drag, but i have to take out the framer motion "drag" property to acomplish. This breaks the dragContraints property.

@danilockthar I just did this last night and it seemed to work...

const myComponent = ({children}) => {
  const animation = useAnimation({
    x: 0,
    transition: {
      type: "spring",
      stiffness: 1,
    },
  });
  const bind = useDrag(
    (state) => {
      if (state.dragging) {
        animation.start({ x: state.movement[0] });
      } else {
        animation.start({ x: 0 });
      }
    },
    {
      axis: "x",
    }
  );

  return (
    <motion.div
      {...bind()}
      animate={animation}
    >
      {children}
    </motion.div>
  )
}
danilockthar commented 4 years ago

style={{ x }} {...bind()}

Hi! is there a way to make this work with motion dragcontraints? im tried this and the scrolling seems to work along with the x drag, but i have to take out the framer motion "drag" property to acomplish. This breaks the dragContraints property.

@danilockthar I just did this last night and it seemed to work...

const myComponent = ({children}) => {
  const animation = useAnimation({
    x: 0,
    transition: {
      type: "spring",
      stiffness: 1,
    },
  });
  const bind = useDrag(
    (state) => {
      if (state.dragging) {
        animation.start({ x: state.movement[0] });
      } else {
        animation.start({ x: 0 });
      }
    },
    {
      axis: "x",
    }
  );

  return (
    <motion.div
      {...bind()}
      animate={animation}
    >
      {children}
    </motion.div>
  )
}

This looks great Benjamin ! Thank you very much. It works great :D

omarryhan commented 4 years ago

Thanks @BenjaminCorey

Where did you import the useAnimation hook from? Framer Motion's useAnimation hook does not accept any arguments. Is it from a different lib?

vimtor commented 4 years ago

I just build a nice slider using the drag="x" functionality, when I ran into this issue. The component takes up most of the viewport, so you should be able to scroll down the page without trigging the drag effect.

Had to look into blockViewportScroll(), and it seems like a really aggressive method of preventing scrolling while dragging, since we have no way of controlling it.

I know this is one of those difficult issues, and we can agree that you don't want scrolling to occur while dragging.

I made "fix" that determines if we are scrolling or dragging based on the initial velocity in onDragStart - but it's not perfect. Might be better if it could sample a few events before locking it.

function Example() {
  const [allowScroll, setAllowScroll] = useState(false)
  useEffect(() => {
    if (allowScroll) {
      const handleTouch = event => {
        event.stopPropagation()
      }
      document.documentElement.addEventListener('touchmove', handleTouch)
      return () => {
        document.documentElement.removeEventListener('touchmove', handleTouch)
      }
    }
  }, [allowScroll])

  return <motion.div drag="x" onDragStart={(event, info) => {
          setAllowScroll(Math.abs(info.delta.y) > Math.abs(info.delta.x))
        }}
  />
}

Using this solution with vertical list I have the same problem, the event gets locked infinitely onDrag which I am only able to exit by clicking outside.

em commented 4 years ago

I think I'm having the opposite issue here in that I have my own scroll-blocking in place using https://www.npmjs.com/package/body-scroll-lock. And framer-motion's built-in blocking appears to break mine after the first drag by colliding with body-scroll-lock. Not sure why yet, perhaps both are modifying body style? Is there a way to tell framer-motion not to attempt any touch scroll blocking because I'm already handling it? My entire UI is in a fullscreen modal - so I know that when the modal is open I want to only allow touch events through that initiated inside the model.

mattgperry commented 4 years ago

The way it works is by applying touch-action style to the element - maybe you can override it with an !important rule and see if that fixes it?

On Tue, 6 Oct 2020 at 05:18, Emery Denuccio notifications@github.com wrote:

I think I'm having the opposite issue here in that I have my own scroll-blocking in place using https://www.npmjs.com/package/body-scroll-lock. And framer-motion's built-in blocking appears to break mine after the first drag by colliding with body-scroll-lock. Not sure why yet, perhaps both are modifying body style?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/framer/motion/issues/185#issuecomment-704003534, or unsubscribe https://github.com/notifications/unsubscribe-auth/AB34WKQOHFXWBM3HWSS56KTSJKD7VANCNFSM4IBETH2A .

Dm1Korneev commented 3 years ago

Thanks @BenjaminCorey

Where did you import the useAnimation hook from? Framer Motion's useAnimation hook does not accept any arguments. Is it from a different lib?

Hey @omarryhan do you found the way how to handle it? I have stucked with tha same problem. Ca't drag scrollable container on mobile...

omarryhan commented 3 years ago

Hey @Dm1Korneev

Here's the code I used: https://github.com/omarryhan/trendzz/blob/master/components/WithSlide/Component.tsx

And for the live demo, go to: https://trendzz.netlify.app and try swiping a repo card to the left.

Hope that helps.

Edit:

On desktop, you'll need to click and drag from either the very top of the card or the very bottom. Because if you click in the middle, it will trigger an onclick event which will open a new tab.

omattman commented 1 year ago

Any news on this?

jmikolajczyk commented 9 months ago

Scrolling interfering with dragging because of pointercancel event. pointermove after pointerdown event fires onDragStart and after dragging without scroll pointerup event fires onDragEnd (this is what we want). Scrolling during dragging fires pointercancel event which fires onDragEnd.

Below example prevents animation when dragging horizontally and scrolling:

 onDragEnd={e => {
          if (e.type === 'pointercancel') return;
          animate(
            ref.current,
            { x: 200 },
            { duration: 0.2 },
          );
        }}