Closed rijk closed 1 year 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>
?
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).
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
Any update on this? Currently I'm reordering items in an accordion and it always opens them when the drag ends
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.
@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
@Kinark Please bear in mind that this is free open source software and you are fully capable of implementing this feature yourself.
@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.
Nice! Keep up the good work Matt.
I've added a note in the documentation too https://www.framer.com/docs/gestures/##propagation
Thats a great solution Matt! Caveat is that it also disable the whileTap on the children where you set the onPointerDownCapture.
@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.
@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:
#child
around won't drag the #parent
around but will move the #child
.#parent
around will drag the parent as normal.What I'm seeing with "framer-motion": "^10.16.5",
is:
#child
around doesn't do anything to either #parent
or #child
#parent
around works as expectedI have tried the following:
onPointerMoveCapture
, onPointerUpCapture
...onPan
gesture on the #parent
and check the parents event.target
for motion.divs and conditionally "drag" the div with the events delta prop.
<Link>
elements inside a draggable<motion.div>
element. When the user clicks those, I don’t want the drag/whileTap
styles to activate.onMouseDown
event, but this doesn’t seem to have any effect.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?