もっと詳しく

書きなぐりですが、メモ。

PCのホバーとiOSのタッチは問題なし

あるサイト制作でアコーディオンメニューを作成し、ホバーとタッチの両方に対応させたく、jQueryでメニュー開閉のスクリプトを書きました。PCの場合はマウスをメニュータイトルにかざすと開閉でき、iPhoneやiOSではタップで開閉できました。ところが…。

Windowsタブレットで問題発生

クリックしかできないデバイスと、タップしかできないデバイスならこれで良かったのですが…。タップもクリックもできるデバイスとなると挙動が変わりました。マウスホバーでの開閉はいいのですが、途中でタップに切り替えると、2度タップしないとメニューが開かないようになりました。閉じるのは1回でもOKでした。

問題のあるソース例

メニューのタイトル部分<li class="menu_list">タイトル</li>で、開閉して表示・非表示となる部分が.menu_contentsだった場合の例。

//PCの場合はマウスオーバーで開閉
$('li.menu_list').not('.active').hover(function () { 
  $(this).addClass('active');
  $(this).find('.menu_contents').stop().slideDown();
}, function () {
  $(this).removeClass('active');
  $(this).find('.menu_contents').hide();
});

//スマホやタブレットはタッチで開閉
var clickEventType = ((window.ontouchstart !== null) ? 'click' : 'touchend');
$('li.menu_list').on(clickEventType, function () {
  // 状態が active の場合は閉じる
  if ($(this).hasClass('.active')){
    $(this).removeClass('active');
    $(this).find('.menu_contents').hide();
    }
  // 状態が activeなしの場合は開く  
  else if (!$(this).hasClass('active') {
    $(this).addClass('active');  
    $(this).find('.menu_contents').stop().slideDown();
    }
});

hoverとtoutchの切替えがうまくいっていないのか?

仕組みがよくわからないのですが、タップで閉じた後に同じメニューを開くのは何度やってもタップ1回で繰り返し開閉できる。しかし、隣りのメニューをタップすると、2度タップしないと開かない…。タップした時点で要素を捕まえきれていない感じがします。タップからマウスホバーは問題ありませんでした。

想像ですが、2回タップしないといけないのは、指がタッチパネルから離れていてもホバー状態になっていて、1回目のタップでは以前タップした場所をからhoverを解放し、2回目のタップで現在のボタンをクリックしているのではないかと思います。しかし、初回表示時でも2回タップしないとメニューが開きませんでした。つまり、ページ表示時やタップ時にホバーを解除する命令を加えれればうまくいくような気がします。

↓このページを見てそう思いました。

模索した解決策の候補

クリックとタップのみでの開閉はPCでもタブレットでもうまくいくため、『hoverの開閉を諦める』これしかないかもしれません。どれも試していませんがメモ。

hoverをmouseenter、mouseleaveに変えてみる

これはダメでした。clicktoutchstartのようにon('mouseenter', function(){~のように書けるので、これならイケるかも?と思いましたが、挙動は同じでした。

発火方法を細かく分けてみる

これもダメでした。PCのマウスホバーのみ動作やiOSでのタッチのみの動作はOKでしたが、Windowsタブレットでは2回タップしないとメニューが開きませんでした。

CSSからhoverのルールを削除して無効化する例

タップされたら、一時的にhoverを無効化。ontouchstartでhoverを無効化するという方法がありましたが、hoverを復活したいときはどうするんだろう?


単純にこれは、hoverの機能を無効化するというより、CSSにある:hoverのルールを削除するだけみたいです。

疑似的に:hoverを追加削除する方法



指定した要素にJavaScript で ontouchstart 属性を追加する方法

onclick 属性を付与する方法

ontouchstartと同じかな。

mouseenterの替わりにtoutchenterを使用してみる

初回イベント時のみ取得できない状況を解消する?

これに近い挙動のように感じます。しかし、これはAndoroidアプリ制作上での話みたいで、コマンドがJavascriptではない感じ。以下のページに関連するページのリンクがあるので、進んで行くと『初回の1タップ処理が、何か別の処理に搾取されていると推測できる。』と記載があった。まさにこの挙動だと感じていますが、これをどうやって調べるかが課題。


イベントハンドラの指定方法を変えてみる

$('li.menu_list').on('toutchstart', function () {

ではなく、

$('document').on('toutchstart', 'li.menu_list', function () {

とする方法。

後日、試してみたところ、これが当たりでした!

click / touch / pointer イベントを完全に制御する

『タッチディスプレイを搭載したPC』について言及があったので、これが案外有力そうです。Windowsタブレットで試したところ、タップやクリックの度に取得したイベントが異なっていたので良さそうでした。しかし、私のスキルが追い付いていないため、うまくfunctionを組み立てることができず、使いこなせませんでした。使いこなせる方ならこれが一番いいような気がします。

clickとtouchを交互にsettimeoutで入れ替える

なんか邪道だけどメモ。

これ以外で役に立ちそうなついでの対策

スクロールとタッチイベントの衝突回避

何のイベントがどの要素で発生しているかを調べるのに良さそうなコマンド

解決後のソース例

結局、何とか解決できました。実際にテストしたソースとは異なりますが、解決に至ったソースを簡単にまとめました。もっと効率よく書きたいところですが、とりあえずメモ。

これでPCのマウスホバーによる開閉、Windowsタブレットでのマウスホバーとタップによる開閉、スマホのタッチによる開閉を同時に実装できました。不思議なのはクリックイベントの指定をon('click mouseend touchend'としたことです。この3つのイベントを同時に入れることで何故か理想通りの動きになりました。ここにpointerstartなどのpointer〇〇を入れると不具合が起こりました。

//マウスカーソルをかざすと開
$(document).on('mouseenter', 'li.menu_list', function () {
  // class に active がない場合は開く
  if (!$(this).hasClass('active')) {
    $('.menu_contents').hide(); //他のメニューを閉じる
    $(this).addClass('active');
    $(this).find('.menu_contents').stop().slideDown();
  }
});

//マウスカーソルが外れると閉じる    
$(document).on('mouseleave', 'li.menu_list', function () {
  // class に active がある場合は開く
  if ($(this).hasClass('open')) {
    $('.menu_contents').hide();
  }
});

//スマホやタブレットはタッチで開閉
$(document).on('click mouseend touchend', 'li.menu_list', function () {
  // class に active がある場合は閉じる
  if ($(this).hasClass('.active')){
    $(this).removeClass('active');
    $(this).find('.menu_contents').hide();
    }
  // class に active がない場合は開く  
  else if (!$(this).hasClass('active') {
    $('.menu_contents').hide(); //他のメニューを閉じる
    $(this).addClass('active');  
    $(this).find('.menu_contents').stop().slideDown();
    }
});