January 27th, 2016

How we learned about iOS’s double tab feature the hard way and what it taught us

Being a developer boils down to spending enormous amounts of time troubleshooting bugs. We all have war stories. Memories of working deep into the night, empty beers cluttering our desks, running out of ideas. I certainly do. But no bug in recent memory matches this one, mostly because it was not a bug at all. We were actually to trying to circumvent a feature.

Hover events on iOS

If you are an iPad or iPhone user, you may have noticed menu items that need to be tapped twice to complete your action. It seems Apple’s developers knew web developers rely on hover more that they should at times to hide vital information behind hover states, so they took matters into their own hands. If iOS sees an event attached to hover, it fires that event on the first tap. The second event fires the touch or click event, depending on which is called. Its actually a pretty elegant fallback solution. Unfortunately, from the user perspective it feels like a bug.

My original theory was iOS was not handling delegated events properly. For clarity, event delegation is a technique that allows you to attach a single event to a parent element instead of each instance of an element. This works because events trigger on the deepest possible element and then bubble the event up to it’s parents in nesting order. This way, you don’t have to clutter memory unnecessarily with an event attached to each node that needs to complete the same action and you’re in the clear if the element triggered was added to the DOM after the event was attached. In jQuery, that looks like this:

$(ParentElement).on('click', childElement, function(event) {
  // Some Action
});

I thought iOS wasn’t recognizing the click function until after the second click, because every link afterward worked as I expected. This was even supported by an article I found. As old as the source was, it was the only source I could find. The implications of that issue, however, meant I would have to rewrite how and when events were attached in our app. It also didn’t make sense that Apple would leave a bug unfixed for 6 years, so I kept testing for other solutions.

Our Solution

Once I realized the issue was iOS handling the hover state for our menu on first touch, the solution was simple. The only think we needed to do was wrap our mouseenter and mouseleave event handlers in a conditional that checks if the browser is iOS. Our solution looked like this:

function checkIos() {
  if (navigator.userAgent.match(/(iPad|iPhone)/i)) {
    return true;
  } else {
    return false;
  }
}

if (!isIos) {
  $(ParentElement)
    .on(‘mouseenter’, childElement, function(e) {
      // Some Action
    })
    .on(‘mouseleave’, childElement, function(e) {
      // Some Action
    });
}

So what did we learn here?

I assumed since mouseenter and mouseleave don’t exist in a mobile context, they would simply be ignored. Even though that is a reasonable assumption, it was not the case. The real lesson here is the importance of craftsmanship. Because those events don’t technically exist in a mobile concept, those functions shouldn’t even be assigned. If you don’t want certain actions available for mobile devices, your code should reflect it. You can save your team, or future you some headaches.