Search code examples
javascripthtmlcsstouchtouch-event

Misplaced clickable area due to class toggle / height change on touch event


There's a strange behavior on touch devices regarding the clickable area of a link, if you toggle the height of an element placed above. If you run the following snippet (e.g. save it locally and use chrome to emulate touch events), you will notice that the hash #mylink is added to the url in some cases you did not click on the red link area. And sometimes you clicked the red area, it won't be added.

E.g.

  • If you click the upper grey area (first 200px), the grey area toggles the height. => Ok.
  • If you click the enhanced, lower grey area (200px - 400px), #mylink is added to the url as if you had clicked on the red link area. => Why?
  • If you click the upper red area (first 200px, grey area minimized), #mylink is not added to the url. => Why?
  • If you click the lower red area (last 200px, grey area enhanced), #mylink is not added to the url. => Why?

<html>
<head>
	<style type="text/css">
		.test {
			background: grey;
			height: 200px;
		}

		.test.is-visible {
			height: 400px;
		}

		.link {
			height: 600px;
			display: block;
			background: red;
		}
	</style>
	<script type="text/javascript">
	  (function() {
	    window.addEventListener( 'touchstart' , function() {
	    	document.getElementsByClassName("test")[0].classList.toggle('is-visible');
	    });	
		})();
	</script>
</head>

<body>
	<div class="test">
		Click the grey area. Element enhances and minimizes itself. Now click on the lower half of the enhanced grey area (if grey = enhanced) or on the white area up to 200px below of the red area (if grey = minimized). #mylink is added to the url as if you had clicked on the red area. Why?
	</div>
	<a href="#mylink" class="link">The red area is the "normal" click area.</a>
</body>

</html>

I tried this on iOS, Android and emulated touch events in both Chrome and Firefox. They all behave the same.

If I listen to click instead of touchstart or touchend event, it works as expected.

What's the reason for this discrepancy? Why is the link area different / misplaced if I listen to touch events instead of click event?


Solution

  • TL;DR

    Browsers execute a click event after touch events on the location of the touchend, no matter if the content has changed due to functions assigned to the touch events.


    After digging trough the Touch Events specification I think I may answer my question:

    Section 8 describes the interaction of Touch Events with Mouse Events and click. It says, that:

    The user agent may dispatch both touch events and (...) mouse events in response to the same user input.

    and

    If the user agent interprets a sequence of touch events as a tap gesture, then it should dispatch mousemove, mousedown, mouseup, and click events (in that order) at the location of the touchend event for the corresponding touch input.

    If the contents of the document have changed during processing of the touch events, then the user agent may dispatch the mouse events to a different target than the touch events.

    and most important

    The activation of an element (e.g., in some implementations, a tap) would typically produce the following event sequence (though this may vary slightly, depending on specific user agent behavior):

    1.) touchstart

    2.) Zero or more touchmove events, depending on movement of the finger

    3.) touchend

    4.) mousemove (for compatibility with legacy mouse-specific code)

    5.) mousedown

    6.) mouseup

    7.) click

    So to conclude for our example above, this means that:

    1.) touchstartor touchend is triggered.

    2.) Custom function gets processed first, which toggles the class and changes height / position of the elements.

    3.) Afterwards, the click event is executed on the same point where the touch event happened, but covers now a different target.

    The specification provides a way to prevent this too:

    If touchstart, touchmove, or touchend are canceled, the user agent should not dispatch any mouse event that would be a consequential result of the prevented touch event.

    So I guess the easiest way to "fix" this behavior is to prevent the event with `preventDefault()', then click manually on the target of the event and toggle the class at last:

         window.addEventListener( 'touchstart' , function(event) {
            event.preventDefault();
    
            event.target.click();
    
            document.getElementsByClassName("test")[0].classList.toggle('is-visible');
        }); 
    

    There's a statement in the Chromium bug tracker which proves this as well.