- Hammer.js
- jQuery
- Underscore
- Backbone
click events are not good UX on phones because:
- There is longish (.5s) delay before they get handled
- They sometimes fail to get fire at all
- Some mobile browsers apply an artificially larger hitbox to
clicklisteners that can obscure other targets
BKWLD has decided to standarize on using Hammer.js to register tap listeners across the board, replacing both jQuery's on('click') and Backbone view's events hash. This is typically a simple drop in replacement that takes something like this:
$('.btn').on('click, function(e) {});
And replaces it with:
$('.btn').hammer().on('tap', function(e) {});
However, you run into issues when trying to replace an <a> default behavior. On most sites, you'd want your nav markup to be like:
<ul class="nav">
<li><a href="/">Home</a></li>
<li><a href="/news">News</a></li>
</ul>
If the site brings in sub pages via AJAX, you would still use this markup for progressive enhancment / SEO reasons. But you need JS to intercept those clicks and fire your AJAX routing code. Here's the non-Hammer way you'd do this:
$('.nav a').on('click', function(e) {
e.preventDefault();
router.navigate($(this).attr('href'));
});
But this won't work with Hammer because it can't preventDefault() on the click, it is only listening to taps. As a result, you router JS would run, but milliseconds later, when the click is handled, a full page refresh would occur. So we need to add a listener to click as well, to preventDefault().
This was the first way I thought to do this:
$('.nav a').on('click', function(e) {
e.preventDefault();
}).hammer().on('tap', function(e) {
router.navigate($(this).attr('href'));
});
And this works for the most part, except for one of the problems with click events that I mentioned before:
Some mobile browsers apply an artificially larger hitbox to
clicklisteners that can obscure other targets
As a result, if the user's browser plays a sound or has some other effect when the user clicks a link, and if the user clicks outside of the tap hitbox but inside the click hitbox, the feedback they get from the browser is that they clicked a link but nothing happened. As a result, I propose this solution, where we trigger our logic on both listeners through a throttled function to prevent the JS from responding to both events:
// Change page
var navTap = _.throttle(function(e) {
router.navigate($(this).attr('href'));
}, 1000);
// Listen for interaction
$('.nav a').on('click', function(e) {
e.preventDefault();
navTap(e);
}).hammer().on('tap', function(e) {
navTap(e);
});
The 1000ms delay could be reduced probably. It needs to be >= the longest possible delay a browser might take between the "touch" tap event and the "release"-ish click event.
I plan to create an AMD module to make registering events in this style easier.
I'll create a stand alone test today to see what's what. I'll post here.