iOS Safari で、タップ時に :hover や :active を有効にする方法 詳細調査

前回の記事で、「スマホで :hover 時のアニメーションを有効にする」方法について触れた。

タップしている間だけ :hover を機能させるにはどうしたらいいか調べたところ、<body ontouchstart=""> と書けば実現できるようだ。

コレで確かに実現できるし、問題は解決したのだが、どうして onTouchStart イベントを空で作成すると :hover が効くのか、:focus:active はどうなる?など、疑問がいくつか湧いたので検証してみた。

目次

検証ページ

以下に検証用のページを作った。iOS Safari で以下のページを表示させて確認した。

ソースコードは以下。

なお、検証した環境は以下のとおり。

onTouchStart イベントの設定方法

まずは onTouchStart イベントの設定方法を試してみた。

<body ontouchstart=""> と、HTML 側に処理を書くのが嫌だったので、JavaScript コードに寄せられないか検証した。結論からいくと、以下のいずれの書き方でも問題なく、:hover を有効にできた。

window.ontouchstart = function() {};
window.addEventListener('touchstart', function() {}, true);
window.addEventListener('touchstart', function() {}, false);

document.ontouchstart = function() {};
document.addEventListener('touchstart', function() {}, true);
document.addEventListener('touchstart', function() {}, false);

document.body.ontouchstart = function() {};
document.body.addEventListener('touchstart', function() {}, true);
document.body.addEventListener('touchstart', function() {}, false);

TouchStart イベントを設定さえすれば、window でも document でも、document.body でも大丈夫だった。addEventListener でも良いし、第3引数の useCapture も影響しないようだ。

ココは window.addEventListener()document.addEventListener() を利用して空の関数を渡しておけば、ページ全体で :hover が効くようになる。

ちなみに、ココで追加するイベントは onTouchMoveonTouchEnd でも良いようだ。

:hover:focus:active の兼ね合い

次に、:hover:focus:active という3種類の擬似クラスがどのように動くか試してみた。

iOS Safari の場合は以下のようになった。

擬似クラス デフォルト onTouchStart 設定後
:hover 要素をタップして指を離すと有効になる 要素をタップした瞬間に有効になり、別の要素にフォーカスが移るまで有効のまま
:focus 有効にならない 有効にならない
:active 有効にならない 要素をタップした直後に有効に有効になり、指を離すと効果が消える

:focus は有効にならなかった。:active は、onTouchStart に空関数を設定した後は、PC における :hover っぽい感覚で効いてくれた。:hover 自体は一度タップしてフォーカスが残っている間はずっと効果が付いてしまう。

だから、例えば :hover:active を両方設定すると、「タップする (TouchStart)」→「:active 有効化」→「指を離す (TouchEnd)」→「:active 無効化・:hover 有効化」という順に表示が切り替わる。

なぜ onTouchStart に空関数を設定すると :hover:active が効くのか

さて、一番気になっていた「どうして」の部分だが、コレは調べてもそれらしい理由が出てこず分からなかった。

iOS magic.

「iOS magic」なんですか…。

:hover イベントは「マウスがポインティングされている」時に発火するので、タッチした後に発火することは理解できる。しかし、どうして onTouchStart なタイミングではなく、onTouchEnd 後なのだろうか?推測だが、onTouchStart 時点では、その後に onTouchMove、つまり「ドラッグ」的な操作もされ得るので、ポインティングしたとは言い切れないのかと推測される。

つまり、「:hover = onTouchStartonTouchMoveonTouchEnd」な順序が好ましいように感じていること自体がタッチデバイスに対しては誤りで、「ポインティングの確定」は onTouchEnd の後というのが正なのだろう。しかし、人間にとってあまり直感的ではない、と。

onTouchStartonTouchMoveonTouchEnd のいずれかに関数を指定することで、iOS がタッチ操作を区別するために待機する時間をキャンセルし、そのユーザ指定の関数を実行して (= 空だから何もせず即座に) 後続のイベントに移行するものと思われる。

参考文献