Motion: Allow stopPropagation() to prevent whileTap/drag from child elements

Created on 15 Oct 2019  路  16Comments  路  Source: framer/motion

  • In my app I have <Link> elements inside a draggable <motion.div> element. When the user clicks those, I don鈥檛 want the drag/whileTap styles to activate.
  • I tried to achieve this by blocking propagation of the onMouseDown event, but this doesn鈥檛 seem to have any effect.
  • In fact, the onMouseDown event is never fired at all on any of the childs of the draggable element.

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?

feature

Most helpful comment

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.

All 16 comments

It seems the reason e.stopPropagation() (or e.preventDefault()) doesn鈥檛 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>?

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

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

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

@InventingWithMonster any chance we could get your take on this?

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.

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.

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.

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.

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

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.

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.

I have the same use case as well

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?

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.

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

Was this page helpful?
0 / 5 - 0 ratings