Jump to content
Search Community

Draggable blocks native scroll event

usheeen test
Moderator Tag

Warning: Please note

This thread was started before GSAP 3 was released. Some information, especially the syntax, may be out of date for GSAP 3. Please see the GSAP 3 migration guide and release notes for more information about how to update the code to GSAP 3's syntax. 

Recommended Posts

I touched on this in a previous posts but am still to find the perfect solution for mixing Draggable dragging/scrolling with native browser scrolling.


1. Open the attached Codepen

2. Emulate touch events - open Chrome inspector and emulate a device such as a Nexus 5

3. Alternatively view the Codepen on a device

4. Alternatively view the screen cap below


Basically, you will see that it isn't possible to scroll the page vertically when the drag starts on the Draggable carousel (blue blocks). I've examined Draggable.js source code but I can't figure out where you the relevant scroll / touch event are being intercepted. Could you point me in the right direction?


I know one work around is to use a second Draggable to control vertical scrolling but this isn't an option for us, we need native browser scrolling when scrolling vertically.


See the Pen azjwab by oisinlavery (@oisinlavery) on CodePen

Link to comment
Share on other sites

So basically we are using the onDrag event to update the scroll position of the natively scrolling container. Sweet!


I scribbled this down as a possible solution last night before I went to sleep, thanks so much for hacking on it for me, it's great to get consensus!


That said, I'm still interested in hearing about how / if it's possible to not block the events at all. We're happy to modify the Draggable.js code to make this happen, if Carl could point us in the right direction :)

Link to comment
Share on other sites

Thanks Jack,


I realize it is probably not a trivial change, to make this work. However this is such a common design pattern in apps that it seems important for Draggable to have this functionality.


Most of the Draggable examples show a small draggable element inside a large container. However, 99% of our use cases are for "larger" draggable elements, for example:


- slide up panels (Google Maps)

- menu drawers (Material Design)

- carousels (G+, Inbox, Facebook, Twitter, etc.)


These elements generally have scroll requirements of their own, either internally (menu drawer needs to drag out from the side then have it's own content scroll vertically) or externally (carousels).


While Greensock javascript scroll performance is second to none there are many occasions where native browser scrolling is essential.

Link to comment
Share on other sites

Yeah, that makes perfect sense, usheeen. It has been an interesting challenge because of the logical complexities involved with determining the user's intent and pulling the right levers in the browser. Typically you need to preventDefault() on the event, otherwise the native behavior completely ruins the draggability of things, but if you preventDefault() that also prevents the native scrolling which is what you're wanting in this case. And it's probably not wise to run logic that prevents the default sometimes and not others, based on the direction of movement because the user almost never moves PERFECTLY up/down OR left/right (it's often a mix of the two). Let me dig into it a bit and get back to you. 


Also, I'm not sure you saw this or not, but we just released an update to Draggable that has some nifty auto-scrolling capabilities. There are a few unique/cool things in particular that you might like:

  1. It doesn't just auto-scroll the immediate container - it auto-scrolls whatever ancestor element is necessary, including the page itself. 
  2. You don't have to keep wiggling your mouse at the edge to make it keep scrolling. 
  3. It varies its speed based on how close you are to the edge (40px is "hot" on each edge). 
  4. It doesn't suffer from the "scroll forever" behavior that plagues other tools when you drag to the right or down (past the native content)
  5. It doesn't allow the container to collapse when you drag from the far right to the left (it temporarily inserts an invisible <div> to prop it open during the drag).

I know none of that has anything to do with your original question, but seeing as how you guys seem to be kicking the tires pretty hard with Draggable and putting it to good use, I figured you might want to know about the new goodies :) We'll be doing an official blog post next week about it. In the mean time, snag Draggable 0.12.0 which is in GSAP 1.16.0 and have a blast. 

  • Like 1
Link to comment
Share on other sites

Yes of course, I can see why you do this - in many cases scrolling the parent isn't the desired behavior.


Perhaps a first step would be to provide a setting that allows events through. An accompanying method to toggle this on the fly would also be useful. I think this would add flexibility without turning Draggable into another javascript carousel plugin.


Autoscroll looks great, I'll be on the look out for a project that requires it :)

Link to comment
Share on other sites

Okay, after countless hours of wrestling with various quirks in mobile browsers, I think I've got a solution for ya :)


Please try the latest uncompressed beta version at https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/Draggable-latest-beta.jsand let me know if that works well for you.


I tested it on iOS, a Windows Surface tablet, Amazon Kindle Fire, and an old Samsung Android tablet; all seem to be handling it pretty well. The only caveat is that due to some limitations in the way Android devices dispatch touch events (when you start native scrolling, they fire a "touchcancel" event and stop dispatching touchmove or touchend), I had to make Draggable act like it completes its drag when you start native scrolling on an Android device. I don't think there's any way around that, but I doubt it's a show-stopper for anyone anyway. 


I also added a new "allowEventDefault" special property that you can set to "true" if you don't want the Draggable to ever call event.preventDefault(). Keep in mind that not only does it pass the most recent event along to the associated callback (like onPress, onDrag, onRelease, etc.) as the first parameter, you can always access it directly using the Draggable instance's "pointerEvent" property, so if you want to manually call preventDefault() on that, you can. Hopefully this gives you everything you need. I doubt you'll even need to use allowEventDefault because of the new logic I added internally for handling native touch scroll stuff. 


I'm eager to hear back from you about if this serves your needs. 

  • Like 3
Link to comment
Share on other sites

I dropped this into my project and it mostly just worked :)


One tweak I made was to lock the draggable's horizontal movement when the page is being scrolled vertically. You can see how I roughly implemented it here: 

See the Pen ogPaKm by oisinlavery (@oisinlavery) on CodePen


This solution works for up / down but fails for diagonals. For example I couldn't figure out how to find out which axis is actually locked when the initial direction is left-up.

- Could you expose the Single Source of Truth instead of me imperfectly recreating this logic?

- Alternatively make this locking behavior a setting?


A couple of other thoughts:

- Do you think this new behavior should be the default? (personally I would say NO)

- Should you just expose more data and we create the logic or should this be part of Draggable?


I'm delighted that I was able to drop in the new version and it just worked but I am also wary of making Draggable into something overly specific. I think you have done a wonderful job of finding the balance so far so I'm confident you will find the best solution.

Link to comment
Share on other sites

Good feedback. As for that "single source of truth", I see a few options here:


1) Instead of adding some other property that's kinda like "lockedAxis" for the touch-scroll-specific behavior/direction, I could add an onTouchScrollStart callback. Isn't that technically more of what you're looking for anyway? Then, if you're setting up a Draggable of type:"x", you'd know that onTouchScrollStart would only fire when the user starts dragging vertically (and visa-versa for type:"y" and dragging horizontally). You could put your disable() code in there for your particular example. The main down side here is that people may also expect us to add onTouchScroll and onTouchScrollEnd callbacks which are virtually impossible for Android devices and they seem largely wasteful anyway (they'd require more hooks and potentially hurt performance). I'd want to only stick with onTouchScrollStart and perhaps frustrate some people. Then again, onTouchScrollStart does seem like the most useful and possibly most elegant solution here (still contemplating). 


- OR -


2) I could automatically set lockAxis:true for any Draggables that use a single-direction type like "x", "y", "top", or "left". Then, I could populate the lockedAxis property accordingly, so you could check that and it'll be "x" or "y", depending which way the user initially started dragging. 


As for it being the default behavior, I'm inclined to say that it SHOULD allow vertical scrolling if it's type:"x", or horizontal scrolling if it's type:"y". I'm struggling to think of a scenario where it's likely that the developer would NOT want that. And of course if they prefer a different behavior, they could always listen for an onPress and call pointerEvent.preventDefault(). Do you think it's likely that someone would want to prevent vertical scrolling when the user drags a type:"x" up/down? Or maybe you were asking a different question about default behavior.


Lastly, is there any more data you think we should expose in order to let you do what you need with Draggable? 

  • Like 1
Link to comment
Share on other sites

I prefer #2


Just to clarify... a draggable type: "x" will report a lockedAxis = "x" when dragged horizontally and a lockedAxis = "y" when dragged(scrolled) vertically. And vice versa for type: "y".


This solution is great:

- it is simple and flexible (easy to explain)

- no user expectation for matching scroll/scrollEnd events

- in general I think events are more difficult for users to grasp than simple attributes or methods


re: default behavior

When you put it like that I agree. Thinking of another practical situation, imagine you're using Draggable to create a volume slider. You wouldn't want this to  block vertical scrolling but you probably would want it to lock axis.


Out of interest, do you think you will have any issues for users' upgrading to this new default behavior?

Link to comment
Share on other sites

I'm rethinking this a bit because the #2 behavior basically prevents you from being able to touch-scroll AND drag on the other axis simultaneously which may be the desired behavior sometimes (although some browsers throttle that so it's a tad jerky). So how about this?: I add a boolean lockAxisOnTouchScroll config special property. That way, we're not locking people into a certain behavior (pun intended). 


In addition, I'm thinking about allowing folks to pass in a function as the "lockAxis" value so that they can get notified exactly when the axis becomes locked. So lockAxis:true would still work, but lockAxis:function() { ... } would also work and get called at that moment when the lock occurs. 




(by the way, others are invited to chime in here and share opinions)

Link to comment
Share on other sites

Pun appreciated!


That sounds reasonable. I've noticed the jerkiness you describe in Chrome emulator. Browsers will improve in time though so good to account for this use case.


re: lockAxis function

Isn't this in effect an event? onLockAxis: function(){ ... }

Is there a reason you want to share the same lockAxis attribute? I guess Snap is kind of similar to this, it accepts a function which is evaluated onDragEnd.


I don't have a big opinion on this, just interested in your thought process.


+1 to others chiming in, I am greedily asking for everything I need but there's a big community of Draggable users with many unique use cases.

Link to comment
Share on other sites

Yeah, I think onLockAxis would probably make more sense. The multi-purpose "snap" thing works because there are so many different ways you might want to implement snapping logic (an array or a function that returns a value). In this case, though, "onLockAxis" is clearer and more consistent with the rest of the API. 

Link to comment
Share on other sites

Sounds good.


See the Pen bNxZrq by oisinlavery (@oisinlavery) on CodePen


I've made another practical example which is slightly different. A drawer which can be dragged open from the side of the screen (recreating Material Design drawer component), the difference is that this time the scrollable area is inside the Draggable element.


At a glance it seems to work just as well as the previous example. It might come in useful when testing.

Link to comment
Share on other sites

Alright, here we go... I have attached a revised [uncompressed] file that has the changes in place. 

  • lockAxisOnTouchScroll is true by default because I really think that's what most people will expect anyway, especially since many browsers throttle the updates along the other axis and there's nothing we can do about that. 
  • There's a new onLockAxis callback.
  • lockedAxis is now populated on the Draggable when you touch-scroll (or drag in the non-scrollable direction). 

Does it work well for you? I'd love to release this very soon. Thanks for your suggestions and the extra codepen test. Very helpful. 


  • Like 1
Link to comment
Share on other sites

Glad it's working well.


As for allowEventDefault, yeah, it might be handy in some rare situations. Ultimately it allows the developer total control of how the events are handled. For example, maybe you want to add an onPress/onDrag handler that runs some logic that decides whether or not the default behavior should still happen with that event (maybe a scroll, maybe a tap or whatever) and then you can call this.pointerEvent.preventDefault(). I doubt many people will use it, but I bet there'll be a situation sometime when someone says "oh GOOD, they thought of that...I can get the behavior I want". :)


Let me know if you run into anything odd. Thus far in my tests it's performing like a champ, but it'd be great to have another set of eyes on it. 

  • Like 1
Link to comment
Share on other sites

This update broke something I was working on.


Maybe I missed something, or never noticed this, but my drag related callbacks are being called when the mouse is pressed on a draggable. Is this normal? This behavior seems to only occur when the draggable is somewhere in between its min/max bounds. It behaves normally using touch.


Using Oisin's example, notice how the onDrag/Start/End callbacks are being called when you press the carousel when its somewhere in the middle. When the carousel is at its left or right limit, it behaves normally when pressed, and turns green.


See the Pen gbBQPY by osublake (@osublake) on CodePen

Link to comment
Share on other sites

Hi OSUblake  :)


pls check this pen with latest TweenMax beta and Draggable beta :


See the Pen bNmQZW by MAW (@MAW) on CodePen


by the way , i think when we use draggable type:x,y ( using subpixels ) ; onPress draggable will snap to pixels (  maybe mouse pointer pixel limitation ) and will cause to fire onDrag Fn too , with a Snap round function we can fix that .

Link to comment
Share on other sites

I feel like I must be missing something here. I have tried a BUNCH of times and I cannot seem to find any problems. When I press, I only see "PRESSED" logged. And it doesn't fire "drag", etc. until it's supposed to. And yes, I dragged the boxes to a mid-point. Same behavior. Are you saying that when you press the mouse down, it fires multiple events (press, drag, and dragend)? What browser? 


I'm also grasping to understand the snapping suggestion - Diaco, are you saying that you must force snapping to whole pixels in order to avoid the wrong events from being fired? 

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
  • Create New...