Skip to content

Instantly share code, notes, and snippets.

@weotch
Last active December 18, 2015 05:09
Show Gist options
  • Save weotch/5730563 to your computer and use it in GitHub Desktop.
Save weotch/5730563 to your computer and use it in GitHub Desktop.
Adding tap events to <a> elements

Adding tap events to <a> elements

Stack

  • Hammer.js
  • jQuery
  • Underscore
  • Backbone

Problem

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

Solution

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.

@danro
Copy link

danro commented Jun 8, 2013

@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 the click event?)

@weotch
Copy link
Author

weotch commented Jul 8, 2013

@danro I didn't see any bigger hitbox in the inspector. But maybe we create a little standalone test (outside of the Intel project) and see if we can reproduce the bigger hitbox that I was talking about. @mattaebersold, do you think you could look into that?

@mattaebersold
Copy link

I'll create a stand alone test today to see what's what. I'll post here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment