- 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
click
listeners 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 tap
s. 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
click
listeners 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.
@weotch I agree with using
tap
events to avoid the 'click' delay, but I'm not on board with the throttle thing. Is it possible that the artificial hitbox was being applied to the<a>
tags themselves (instead of being added when you register theclick
event?)