framer / motion

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

Support e.preventDefault() and e.stopPropagation() in tap/drag events #363

Closed rijk closed 1 year ago

rijk commented 4 years ago

See: https://codesandbox.io/s/framer-motion-block-mousedown-inside-draggable-item-3ecgd

Expected/Desired behaviour: when clicking the white box, the colored <motion.div> element should not scale up and/or follow the cursor when dragging.

Is there a way to do this?

rijk commented 4 years ago

It seems the reason e.stopPropagation() (or e.preventDefault()) doesn’t work is because framer-motion bypasses the React events system.

@InventingWithMonster , do you think you could add a way to prevent clicks propagating? Maybe something like React Router does in <Link>?

ng-hai commented 4 years ago

I would love to have this feature too, here is my case

const listVariants = {
  tap: {
    scale: 0.8,
  },
}

const itemVariants = {
  tap: {},
}

// component

<motion.li
  whileTap="tap"
  variants={listVariants}
>
  <svg width="20" height="20">
    {/* svg content */}
  </svg>

  <motion.h5 variants={itemVariants}>
    {item.title}
  </motion.h5>
</motion.li>

I want the h5 tag to be prevented from animating while tapping event occurs

denkristoffer commented 4 years ago

I'd like this because I want to use onMouseDown and onMouseUp for detecting delta changes in movement, so I know when a user is dragging and when they're clicking.

@Rijk I got around the link usecase by doing something like this on my draggable elements, maybe it's helpful:

const Element = ({ link }) => {
  const isDragging = useRef(false);

  return (
    <motion.div
      onDragStart={() => {
        isDragging.current = true;
      }}
      onDragEnd={() => {
        setTimeout(() => {
          isDragging.current = false;
        }, 150);
      }}
      onClick={() => {
        if (!isDragging.current && link) {
          window.open(link.url, link.target);
        }
      }}
    />
  )
}
rijk commented 4 years ago

My problem is more with the whileTap styles that are applied. Also, the link is in a child, not the draggable element itself (see the Sandbox). The parents whileTap are always applied, regardless of which child is clicked, and you have no way to prevent this at the moment as far as I can tell.

Curious: why would you want to do a manual drag/click detection by the way? Normally, Framer Motion does a pretty good job at this out of the box (when onDragStart is called, it already means a certain delta is exceeded).

denkristoffer commented 4 years ago

It's far from perfect but I was thinking you can do something like removing the styles when isDragging.current is false.

Curious: why would you want to do a manual drag/click detection by the way? Normally, Framer Motion does a pretty good job at this out of the box (when onDragStart is called, it already means a certain delta is exceeded).

Some of my draggable divs contain iframe children that capture the mouse click, so they can't be dragged unless pointer-events: none is applied with CSS. At the moment I'm thinking I can work around this with manual drag/click detection that toggles that CSS.

rijk commented 4 years ago

It's far from perfect but I was thinking you can do something like removing the styles when isDragging.current is false.

I think you can't, as these updates happen outside of the React render cycle.

In any case, your use case sounds relatively different from mine, so you might want to open another issue for it (with a Sandbox to demonstrate) to make sure it is addressed as well.

brentjett commented 4 years ago

Any movement on this? I'm struggling to find any way to cancel a drag programmatically. I have an info card interface where each card is draggable, but I want to restrict the drag to a specific "handle" element so that the contents of the card don't cause drag, but I can't seem to come up with any way to reject the drag after I check the target element. I tried using state to set the drag="y" conditionally, but it doesn't work until the next render.

Is there something in the API that I'm missing? Drag controls don't seem to really fit this situation.

brentjett commented 4 years ago

Scratch that, found a way using useDragControls().

onDragStart={ ( e, info ) => {

    // Check if event doesn't originate in the "handle" element
    if ( ! e.target.classList.contains( 'drag-handle' ) ) {

        // Stop the drag
        controls.componentControls.forEach( entry => {
            // be sure to pass along the event & info or it gets angry
            entry.stop( e, info )
        })
    }
} }

Works great.

blake-mealey commented 4 years ago

@brentjett that works, thanks for the workaround! It feels super hacky in TS though:

onDragStart={(e, info) => {
    if (!isDragging) {
        // HACK: dragControls.componentControls is a private member
        (dragControls as any).componentControls.forEach((entry: any) => {
            entry.stop(e, info);
        });
    }
}}

@InventingWithMonster any feedback?

rijk commented 4 years ago

If we're calling in the Monster, I'd also like to add that as far as I've seen this (or setting dragListeners={false}) does not work to prevent the whileTap animation.

I use whileTap to make the item start 'floating' (scale up and increase shadow) on mouse down (but before drag). This is annoying when clicking a link or button inside the draggable item; you briefly see it scale up and down a bit, looks very clunky. Ideally I'd want to prevent these clicks from 'propagating' and triggering the whileTap styles.

brentjett commented 4 years ago

Yea @Rijk I had to drop the whileTap stuff out of mine because I couldn't work out a way to prevent the contents from causing it too. Same kind of situation where I'd like to be able to check if the tap originated in the "handle" element, and if not, reject the animation.

blake-mealey commented 4 years ago

I have the same use case as well

shakib609 commented 3 years ago

Scratch that, found a way using useDragControls().

onDragStart={ ( e, info ) => {

    // Check if event doesn't originate in the "handle" element
    if ( ! e.target.classList.contains( 'drag-handle' ) ) {

        // Stop the drag
        controls.componentControls.forEach( entry => {
            // be sure to pass along the event & info or it gets angry
            entry.stop( e, info )
        })
    }
} }

Works great.

Have been using this and was working really well. Thanks @brentjett. But facing some issues with this implementation in iPhone 11 iOS 13. Not working as expected. I have a scrollable child element inside the draggable div. For some reason, the child scrollable div is not scrollable in the iPhone 11. Is anyone looking into this issue?

mattgperry commented 3 years ago

I think with these slightly more advanced use-cases you're better off handling gesture animation states yourself with useState. I'm going to leave the ticket open though as I think the request itself is a fair one.

mohux commented 3 years ago

This works perfectly when the div has no overflow

but when I want the overflow to be treated as scroll and the drag to be dragging the parent (Bottom sheet is a big usecase)

it doesn't work

it ignores the overflow scroll

jonahallibone commented 2 years ago

Any update on this? Currently I'm reordering items in an accordion and it always opens them when the drag ends

iamjoshua commented 1 year ago

This still seems to be an issue. I'm using a text editor inside of a draggable (to dismiss) motion.div which seems to consume the first tap on the child contenteditable even with the hack above so that the editor never receives focus. If I disable drag, focus is properly set. i've tried every hack I could think of.

Kinark commented 1 year ago

@mattgperry there no sense in removing the bug label from this issue. If I set some style manually through animate or style props, that will cause conflict with whileHover and actually any other event handler from framer-motion. And no, this is not an "advanced case". Preventing an action triggered in a children from propagating to its parents is a really REALLY common use case. Even CSS handles this.

If you have an item with a whileTap={{ scale: 0.9 }} and a delete button inside (which is literally every web app on the whole internet), you can't click the delete button without the whole item starts to shrink. That's nonsense.

And if having a delete button inside an element with whileTap set is an advanced case, this means you guys built the whole automatic side of the library just for proof of concept apps.

But that's not the case. This is not an advanced feature and you simply are wrong in closing this ticket.

It's insane that this has been going for 3 years now. I mean, you literally considered it like it's a super niche feature and not a bug lol

mattgperry commented 1 year ago

@Kinark Please bear in mind that this is free open source software and you are fully capable of implementing this feature yourself.

mattgperry commented 1 year ago

@rijk In the latest versions of Framer Motion and React you can achieve this by setting

<div onPointerDownCapture={e => e.stopPropagation()} />

On the target you want to prevent propagation.

rijk commented 1 year ago

Nice! Keep up the good work Matt.

mattgperry commented 1 year ago

I've added a note in the documentation too https://www.framer.com/docs/gestures/##propagation

groyGetaway commented 10 months ago

Thats a great solution Matt! Caveat is that it also disable the whileTap on the children where you set the onPointerDownCapture.

lukasjoho commented 9 months ago

@mattgperry Thanks for adding to documentation. It does not however work for the bottom sheet use case where overflow-scroll is needed as pointed out by @mohux earlier.

Setting onPointerDownCapture={e => e.stopPropagation()} on the child will prevent scrolling.

laurens-mesure commented 6 months ago

@mattgperry thank you for the work on this issue. After reading the documentation I'm wondering if it is expected that e.stopPropagation() also stops the child element from "animating". For example:

<motion.div id="parent" drag>
  <div onPointerDownCapture={(e) => e.stopPropagation()}>
    <motion.div id="child" drag />
  </div>
</motion.div>

Here I would expect the following:

What I'm seeing with "framer-motion": "^10.16.5", is:

I have tried the following: