Webサイトでリンクやボタンのクリックを無効化したい場面は意外と多いものです。例えば、処理中のボタンを一時的に無効にしたり、特定条件下でリンクを機能させないようにしたりする場合です。CSSのpointer-events: noneを使えば簡単に実装できるはずですが、iOSのSafariでは思わぬ落とし穴があります。

今回は、iOSで発生するpointer-eventsのバグとその対策、JavaScriptのイベント制御メソッド、そしてiOS特有のクリック・タッチイベントの問題について詳しく解説します。

iOSのpointer-eventsバグとは

期待される動作

通常、pointer-events: noneを設定した要素は:

  • マウスやタッチイベントを一切受け付けない
  • イベントは透過して、下層の要素に直接届く
  • ホバー効果も無効になる
.disabled-link {
  pointer-events: none;
  cursor: default;
}

iOSでの問題

しかし、iOS Safariでは特定の条件下で:

  • pointer-events: noneを設定しても、タッチイベントが完全に無視されない
  • 要素の下層や親要素に意図しないイベントが伝播してしまう
  • 特に<a>タグやonclick属性を持つ要素で問題が発生しやすい

この挙動は他のブラウザでは発生せず、iOS Safari特有のバグです。

iOS特有のクリック・タッチイベント問題

cursor: pointer問題

iOSでは、cursor: pointerが設定されていない要素でクリックイベントが発火しないことがあります。

/* ❌ iOSでクリックイベントが効かない場合がある */
.clickable-div {
  /* cursor指定なし */
}

/* ✅ 確実にクリックイベントを発火させる */
.clickable-div {
  cursor: pointer;  /* これが重要! */
}

対策方法:

<!-- 空のonclick属性でも効果あり -->
<div onclick="">クリック可能な要素</div>

<!-- またはJavaScriptで -->
<script>
element.style.cursor = 'pointer';
</script>

:hoverの残留問題

iOSでは、タップ後に:hover状態が残ってしまう問題があります。これは、タッチデバイスには本来「ホバー」という概念がないためです。

/* ❌ タップ後もホバー状態が残る */
.button:hover {
  background: blue;
}

/* ✅ ホバー可能なデバイスのみに適用 */
@media (hover: hover) {
  .button:hover {
    background: blue;
  }
}

JavaScriptでの対策:

// タッチ終了時にフォーカスを外してホバー状態を解除
element.addEventListener('touchend', function() {
  this.blur();
});

// または、タッチデバイスを判定してクラスを付与
if ('ontouchstart' in window) {
  document.body.classList.add('touch-device');
}

CSSとJavaScriptを組み合わせた対策

実装例:クラス名による制御

以下のような実装で、iOSのバグを回避しつつ、柔軟なイベント制御を実現できます:

/* クリックもホバーも無効 */
a.ev-none {
  pointer-events: none;
  cursor: default;
}

/* クリックのみ無効(ホバーは有効) */
a.ev-prevent {
  cursor: default;
}

/* ホバー時の見た目を通常状態に保つ */
@media (hover: hover) {
  a.ev-none:hover,
  a.ev-prevent:hover {
    opacity: 1;
    filter: brightness(1);
  }
}
// iOS対策:JavaScriptで確実にイベントを止める
document.addEventListener('DOMContentLoaded', function() {
  const elements = document.querySelectorAll('a.ev-prevent, a.ev-none');
  
  elements.forEach(function(element) {
    element.addEventListener('click', function(e) {
      e.preventDefault();    // デフォルト動作を防ぐ
      e.stopPropagation();   // イベントの伝播を止める
    });
  });
});

イベント制御メソッドの違い

return false(jQuery)

jQueryでよく使われるreturn falseは、実は2つの処理を同時に行います:

// jQuery
$('.button').on('click', function() {
  return false;  // preventDefault() + stopPropagation()
});

e.preventDefault()

デフォルトの動作だけを防ぎます。イベントの伝播は止めません。

// リンクの遷移を防ぐが、親要素のクリックイベントは発火する
link.addEventListener('click', function(e) {
  e.preventDefault();  // href属性によるページ遷移を防ぐ
  // クリックイベント自体は親要素に伝播する
});

使用例:

  • <a>タグのページ遷移を防ぐ
  • <form>の送信を防ぐ
  • チェックボックスのチェック動作を防ぐ

e.stopPropagation()

イベントの伝播(バブリング)だけを止めます。デフォルト動作は実行されます。

// 子要素のクリックが親要素に伝わらないようにする
child.addEventListener('click', function(e) {
  e.stopPropagation();  // 親要素のクリックイベントは発火しない
  console.log('子要素のみ反応');
});

スクロール関連のTips

momentum scrollingの有効化

iOSでスムーズな慣性スクロールを実現するには、特別なCSSプロパティが必要です:

/* ❌ カクカクしたスクロール */
.scrollable {
  overflow-y: scroll;
}

/* ✅ スムーズな慣性スクロール */
.scrollable {
  overflow-y: scroll;
  -webkit-overflow-scrolling: touch;  /* 必須! */
  
  /* スクロール中の要素消失バグ対策 */
  transform: translateZ(0);
}

スクロール禁止時の問題と対策

モーダル表示時など、背景のスクロールを防ぎたい場合:

let scrollY = 0;

// スクロール禁止
function disableScroll() {
  scrollY = window.scrollY;
  document.body.style.position = 'fixed';
  document.body.style.top = `-${scrollY}px`;
  document.body.style.width = '100%';
}

// スクロール再開
function enableScroll() {
  document.body.style.position = '';
  document.body.style.top = '';
  document.body.style.width = '';
  window.scrollTo(0, scrollY);  // 元の位置に戻す
}

タッチイベントの最適化

passive listenerでパフォーマンス改善

// ❌ スクロールがカクつく可能性
element.addEventListener('touchstart', handleTouch);

// ✅ スムーズなスクロール
element.addEventListener('touchstart', handleTouch, { passive: true });
element.addEventListener('touchmove', handleTouch, { passive: true });

// preventDefaultが必要な場合は明示的にfalse
element.addEventListener('touchmove', function(e) {
  e.preventDefault();  // スクロール防止など
}, { passive: false });

passive: trueを指定すると、ブラウザはpreventDefault()が呼ばれないことを前提に最適化できます。

タッチイベントとマウスイベントの両対応

// タッチとマウスの両方に対応
function addClickEvent(element, handler) {
  // タッチデバイス
  element.addEventListener('touchstart', handler);
  
  // マウスデバイス(タッチイベント後のマウスイベントを防ぐ)
  let touchHandled = false;
  
  element.addEventListener('touchstart', function() {
    touchHandled = true;
  });
  
  element.addEventListener('click', function(e) {
    if (!touchHandled) {
      handler(e);
    }
    touchHandled = false;
  });
}

デバッグTips

iOS実機でのデバッグ方法

iOS実機でのデバッグは難しいですが、以下の方法が役立ちます:

// エラーをアラートで表示(開発時のみ)
window.addEventListener('error', function(e) {
  if (window.location.hostname === 'localhost') {
    alert(`Error: ${e.message}nLine: ${e.lineno}nFile: ${e.filename}`);
  }
});

// タッチ座標の可視化
document.addEventListener('touchstart', function(e) {
  const touch = e.touches[0];
  console.log(`Touch: X=${touch.clientX}, Y=${touch.clientY}`);
  
  // 視覚的なフィードバック
  const marker = document.createElement('div');
  marker.style.cssText = `
    position: fixed;
    left: ${touch.clientX - 10}px;
    top: ${touch.clientY - 10}px;
    width: 20px;
    height: 20px;
    background: rgba(255, 0, 0, 0.5);
    border-radius: 50%;
    pointer-events: none;
    z-index: 9999;
  `;
  document.body.appendChild(marker);
  
  setTimeout(() => marker.remove(), 500);
});

// イベントの伝播を確認
function debugEventPropagation(selector) {
  const elements = document.querySelectorAll(selector);
  
  elements.forEach((el, index) => {
    ['click', 'touchstart', 'touchend'].forEach(eventType => {
      el.addEventListener(eventType, function(e) {
        console.log(`${eventType} on element ${index}:`, {
          target: e.target.className,
          currentTarget: e.currentTarget.className,
          propagation: !e.cancelBubble
        });
      }, true);  // キャプチャフェーズでも確認
    });
  });
}

Safari開発者ツールの活用

  1. Mac の Safari で「開発」メニューを有効化
  2. iPhone を USB で接続
  3. iPhone の Safari で「Web インスペクタ」を有効化
  4. Mac の Safari から iPhone のページをデバッグ

実装のポイント

デバイスごとの出し分け

/* スマホ用 */
@media screen and (max-width: 743.9px) {
  a.ev-none_sp { pointer-events: none; }
  a.ev-prevent_sp { cursor: default; }
}

/* PC用 */
@media screen and (min-width: 744px) {
  a.ev-none_pc { pointer-events: none; }
  a.ev-prevent_pc { cursor: default; }
}

パフォーマンスを考慮した実装

// DOMContentLoadedで一度だけ実行
document.addEventListener('DOMContentLoaded', function() {
  // 必要な要素だけを選択
  const elements = Array.from(document.querySelectorAll('a.ev-prevent, a.ev-none'));
  
  // イベントデリゲーションで効率化(要素が多い場合)
  if (elements.length > 20) {
    document.body.addEventListener('click', function(e) {
      const target = e.target.closest('a.ev-prevent, a.ev-none');
      if (target) {
        e.preventDefault();
        e.stopPropagation();
      }
    });
  } else {
    // 要素が少ない場合は個別に登録
    elements.forEach(element => {
      element.addEventListener('click', handleClick);
    });
  }
});

まとめ

これらの知識を組み合わせることで、iOSを含むすべてのデバイスで快適なユーザー体験を提供できます。