JavaScript

JavaScriptのtouchstartを止めるとclickイベントが発火しない

More than 1 year has passed since last update.

ブラウザゲーム作っててスマホで登録したclickイベントが発火しない現象に悩まされました。
どの行が悪さしてるか、それはしらみつぶしに探していった結果わかりました。(下記)

<body>
    <button id="button"></button>
</body>
var button = document.getElementById('button');

document.addEventListener('touchstart', function(event) {
    event.preventDefault();    //この行が悪さしてる
});

button.addEventListener('click', function(event) {
    //これ発火しない
});

touchstartイベントをpreventDefault()するとclickイベントが発火しない

当該要素はもちろん、親要素で止めてもダメです。
しかもtouchstartという別イベントの挙動に影響されてるというのがわかりづらいですね><

ちなみにこのバグ、発生する端末についての情報が錯綜しており「Androidなら大丈夫だけどiOSのSafariとChromeで起こるよ派」「AndroidのChromeでも起こるよ派」「Androidでもタブレットなら大丈夫だったよ派」など色んな方がいらっしゃいます。たぶんどのOSがとかじゃなくwebkitの一部バージョンが悪さしてるんじゃないかな…と予想。

しかし実際にこのバグが発生する端末がまだ存在するのは間違いないので、それを考慮したコードを書くことをおすすめします。ちなみに私は以下のように解決しました。

button.addEventListener('touchstart', function(event) {
    event.stopPropagation();    //親要素へ伝播しなければpreventDefault()も発生しない
});

(補足)イベントに対する「ブラウザの標準動作」は親要素への伝播が完了した後

JavaScriptで発生したイベントは、まず当該DOM要素(この場合はbutton)に登録した関数を発火し、そこでstopPropagation()されなければその親要素(→body→document)へとイベントがどんどん伝播されていきます。

で、それがすべて完了すると=最も親の要素まで到達すると(あるいはstopPropagation()で強制中断されると)最後に満を持して「ブラウザの標準動作」が実行されます。

仕様です。ただ知らないと混乱します。不具合の切り分けの際にこれが理解を妨げました。

(補足)「ブラウザの標準動作」はpreventDefault()が一回でも実行されるとキャンセルされる

いやそのまんまだろ!何が罠なんだ!!と思うなかれ。該当要素(button)でpreventDefault()する方は直感的にも納得ですが、親要素のbodyやdocumentでpreventDefault()した場合も、ブラウザの標準動作はキャンセルされます。

ここでやってしまった勘違いは、documentでpreventDefault()すればdocumentの標準動作がキャンセルされる。buttonでpreventDefault()すればbuttonの標準動作がキャンセルされる。という思い込み!

そもそも「documentの(タッチに対する)標準動作」なんてものはろくに存在しないので、親要素になんでpreventDefault()を書いたのか(目的:すべての子要素のタッチを吸収)を思い出せばこんな勘違いはしないと思うのですが…コードを書いているうちに忘れちゃうんですよねえ><

結論:問題は、切り分けよう!!

今回は、わかってみればシンプルなバグが原因でした。
しかし、イベントについて雑な理解をしていたため補足にある2つの思い込みに惑わされて
「真のバグ」の発見と発生条件の解明に手間取ってしまいました。
条件のよくわからないバグに想像した時は、背後に「別の思い込み」が無いかもう一度仕様を確認しましょう!