Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Stop scrolling from interfering with dragging #185

Closed
danprince opened this issue Jul 11, 2019 · 31 comments · Fixed by #779
Closed

[BUG] Stop scrolling from interfering with dragging #185

danprince opened this issue Jul 11, 2019 · 31 comments · Fixed by #779
Labels
bug Something isn't working

Comments

@danprince
Copy link

danprince commented Jul 11, 2019

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.

@danprince danprince added the feature New feature or request label Jul 11, 2019
@mattgperry
Copy link
Collaborator

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
Copy link
Author

Thanks! That's at least a step in the right direction 👍

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
Copy link
Collaborator

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
Copy link
Author

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
Copy link

dcecile commented Sep 27, 2019

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
Copy link
Contributor

thebuilder commented Oct 16, 2019

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
Copy link

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
Copy link

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
Copy link

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
Copy link

Jeyloh commented Jan 29, 2020

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
Copy link

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
Copy link

k-ode commented Feb 13, 2020

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
Copy link

sparlos commented Feb 20, 2020

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
Copy link

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
Copy link

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

@mattgperry
Copy link
Collaborator

@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
Copy link

@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
Copy link

Weffe commented Apr 10, 2020

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
Copy link

oygen87 commented Apr 20, 2020

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
Copy link

zephy20 commented Apr 27, 2020

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

@danilockthar
Copy link

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
Copy link

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
Copy link

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
Copy link

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?

@mattgperry mattgperry changed the title [FEATURE] Stop scrolling from interfering with dragging [BUG] Stop scrolling from interfering with dragging Jul 31, 2020
@mattgperry mattgperry added bug Something isn't working and removed feature New feature or request labels Jul 31, 2020
@vimtor
Copy link

vimtor commented Aug 10, 2020

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
Copy link

em commented Oct 6, 2020

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
Copy link
Collaborator

mattgperry commented Oct 6, 2020 via email

@Dm1Korneev
Copy link

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
Copy link

omarryhan commented Jan 14, 2021

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
Copy link

Any news on this?

@jmikolajczyk
Copy link

jmikolajczyk commented Jan 10, 2024

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 },
          );
        }}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.