✅スクロールがタップ判定になってしまい誤タップする問題
from HAMPUの実装
(あらすじ)C106でドッグフーディングをした。頒布物が5個あったのでアイテム=ボタンでページが埋め尽くされた。iPadをスクロールしたらボタンがタップ判定(抑えている秒数で判定している)になってしまい、架空の売上を大量に計上する羽目になってしまった。これは問題なので早く直さなければならない...。
基素.icon直すぞ〜!AIが
Jules.iconは失敗して途中で止まる
基素.icon全然なおせないじゃないか。Jules.iconくんは最近こういうことが多いよね(パワハラ)
基素.iconなんかこういい感じのAPIはありますか?(⚠️もっとちゃんと聞いています)
GPT-5.icon
モバイルブラウザにはタッチやスワイプを扱うための標準APIが存在します。ただし「スワイプそのもの」を直接判定するAPIはなく、基本は「低レベルのタッチイベント」を自前で処理してスワイプ判定を実装します。
ブラウザ側は「タッチ座標や指の動き」をイベントで提供してくれるだけ。スワイプ認識は開発者が判定ロジックを組む必要があります。
主なAPI
Touch Events API
touchstart, touchmove, touchend, touchcancel イベント
イベントオブジェクトで touches, changedTouches などにアクセスできる
iOS SafariやAndroid Chromeで広く対応
Pointer Events API
pointerdown, pointermove, pointerup など
タッチ・マウス・ペンを統一的に扱える
新しい標準。モバイルブラウザもほぼ対応済み
補足
スワイプ判定は「指が一定距離以上動いた」「移動方向がほぼ一定」などの条件をアプリ側で組み立てて実装する
既製ライブラリ(Hammer.js など)を使えばスワイプやピンチなどを高レベルで扱える
基素.iconslugのページをipadやiphoneでスクロールすると、ボタンに触れながらスクロールした場合にタップ扱いになってしまう。スクロールとタップを判定するようにして、スクロール時にはカウントアップしないようにしてほしい。
ブラウザの標準APIを使いたい。Hammer, ZingTouch, AlloyFingerなどのライブラリの利用も検討してください
Claude Sonnet 4.icon
解決策
1. Phase 1: 標準APIによる実装
useLongPressWithShakeフックにスクロール検出機能を追加
タッチ移動距離の閾値(10px)を設定してスクロール意図を判定
スクロール検出時はタップ・長押しイベントを無効化
2. Phase 2: ライブラリ検討(必要に応じて)
標準実装で問題が残る場合、ZingTouchライブラリの導入を検討
より精密なジェスチャー認識を提供
基素.iconGO~
Claude Sonnet 4.icon
問題の概要
iPadやiPhoneでslugページをスクロールする際、BookItemボタンに触れながらスクロールすると、意図しないタップイベントが発生してカウントがアップしてしまう問題がありました。
そして問題は治ったのだった
まとめ
問題の原因
モバイルブラウザでは、スクロール動作中でもタッチイベント(touchstart、touchend)が発生する
既存のuseLongPressWithShakeフックは、タッチの移動距離を考慮していなかった
スクロール意図とタップ意図の区別ができていなかった
解決アプローチ
1. スクロール検出メカニズムの実装
code:ts
// タッチ開始位置を記録
const startPositionRef = useRef<{ x: number; y: number } | null>(null);
// スクロール状態を追跡
const hasScrolledRef = useRef(false);
2. 移動距離による判定ロジック
code:ts
const checkForScroll = useCallback((event) => {
// 開始位置と現在位置の距離を計算
const deltaX = Math.abs(currentX - startPositionRef.current.x);
const deltaY = Math.abs(currentY - startPositionRef.current.y);
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
// 閾値(10px)を超えた場合はスクロールと判定
if (distance > scrollThreshold) {
hasScrolledRef.current = true;
// 全てのタイマーをキャンセル
clearTimeouts();
setIsPressed(false);
setShouldShake(false);
}
}, scrollThreshold, clearTimeouts);
3. イベントハンドラーの拡張
onTouchMove: タッチ移動を監視してスクロール判定
onTouchMoveを定義してJSXに渡しておくだけで、ユーザーが指を動かすたびにブラウザが自動的にその関数を呼び出してくれる基素.icon
Claude Sonnet 4.iconタッチイベントの流れ
1. touchstart: 指が画面に触れた瞬間
2. touchmove: 指が画面上を移動している間(連続して発生)
3. touchend: 指が画面から離れた瞬間
実際の呼び出しタイミング
onTouchStart // 指を置いた瞬間に1回
onTouchMove // 指を動かしている間、連続して何度も
onTouchMove // ↑
onTouchMove // ↑ ブラウザが自動で呼び出し続ける
onTouchEnd // 指を離した瞬間に1回
onMouseMove: デスクトップでの互換性確保
4. スクロール時のイベント抑制
code:ts
// タップイベント発火時にスクロールチェック
if (isPressingRef.current && !longPressTriggered && !hasScrolledRef.current) {
if (releasedWithinTapWindow && shouldTriggerClickParam && onClick) {
onClick(event);
}
}
// 長押しイベント発火時にもスクロールチェック
longPressTimeoutRef.current = setTimeout(() => {
if (!hasScrolledRef.current) {
onLongPress(event);
// ...
}
}, delay);
技術的なポイント
useRefを使用する理由
1. 値の永続化: 再レンダリング時も状態を保持
2. パフォーマンス: 値変更時に再レンダリングを引き起こさない
3. 非同期安全性: タイムアウト内で最新値に確実にアクセス
設定可能なオプション
code:ts
interface LongPressOptions {
scrollThreshold?: number; // デフォルト: 10px
// その他既存オプション...
}
実装結果
スクロール中: 10px以上の移動でスクロールと判定し、タップイベントを無効化
通常タップ: 移動距離が小さい場合は従来通りタップとして処理
長押し: スクロール検出時は長押しも無効化
互換性: デスクトップ環境でも動作
期待効果
1. モバイルデバイスでの誤操作防止
2. ユーザビリティの向上
3. 意図しない販売数カウントアップの回避
4. より直感的な操作感の実現
この解決方法により、ユーザーは安心してページをスクロールでき、同時に意図的なタップ操作は正常に機能するようになりました。