Katashin .info

タッチデバイスで pointercancel イベントによるドラッグ中断を回避する方法

高度なインタラクションを持つ UI には、画面上のオブジェクトをつまんで移動するような、ドラッグアンドドロップの実装があります。リスト項目の並べ替えや、ホワイトボードアプリでの自由な要素配置などがその代表例です。Pointer Events API(pointerdown、pointermove など)を使用することで、マウス、タッチ、ペンといった多様な入力デバイスに対応したドラッグアンドドロップ機能を実現できます。

スマートフォンのようなタッチデバイスでは画面のスクロールと競合してドラッグの処理がキャンセルされることがあります。ユーザーが要素をドラッグしようと画面に触れた際、ブラウザーがそのジェスチャーをスクロール操作と解釈すると、pointercancel イベントが発生してドラッグ処理が中断されます。この問題は、スクロール可能な領域内にドラッグ可能な要素を配置した場合に現れます。

以下のデモは、タッチデバイスでスクロールとドラッグが競合して pointercancel イベントが発生する様子を示しています。点線で囲まれた領域はスクロール可能な要素で、画面上部には最後に発生したポインターイベントの種類が表示されます。タッチデバイスで緑の四角形をドラッグしようとすると、ブラウザーがスクロールを優先し、pointercancel イベントが発生することを確認できます。

上記のデモのコード
<div>PointerEvent:&nbsp;<output id="output"></output></div>

<div id="scroller">
  <div class="content">
    <div id="rect"></div>
  </div>
</div>
body {
  position: absolute;
  inset: 0;
  overflow: clip;
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0;
  padding: 8px;
  font-family: sans-serif;
}

p {
  margin: 0;
}

#scroller {
  flex: 1 1 0;
  overflow: auto;
  border: 1px dashed black;
}

.content {
  position: relative;
  background: linear-gradient(to bottom, white, gray);
  height: 600px;
}

#rect {
  position: absolute;
  top: 50px;
  left: 80px;
  background: green;
  width: 50px;
  height: 50px;
}
const scroller = document.getElementById('scroller')
const output = document.getElementById('output')

scroller.onpointerdown =
  scroller.onpointermove =
  scroller.onpointerup =
  scroller.onpointercancel =
    printOutput

function printOutput(event) {
  output.textContent = event.type
}

このようにドラッグがキャンセルされる現象の対策として、touch-action を指定する方法と、touch イベントリスナーで preventDefault() する方法があります。いずれもブラウザーのデフォルトタッチ動作を制御し、開発者が意図したインタラクションを実現するためのアプローチです。

touch-action を指定 #

CSS の touch-action プロパティでタッチによってブラウザーのスクロールが発生しないようにすることで、ドラッグの処理を行えます。touch-action: none を指定すると、該当要素上でのすべてのデフォルトタッチ動作(スクロール、ズーム、ダブルタップなど)が無効化されます。

スクロール要素に touch-action: none を追加すると、スクロール機能は失われますが、pointercancel イベントの発生を防ぎ、ドラッグ処理を正常に実行できるようになります。

#scroller {
  touch-action: none;
}

touch イベントリスナーで preventDefault #

touchstart や touchmove イベントのリスナー関数で event.preventDefault() を呼び出すことでもスクロールを止め、ドラッグの処理を走らせることができます。JavaScript による動的な制御が可能なため、条件に応じた挙動の切り替えが必要な場合に適しています。たとえば、特定の条件下でのみドラッグを許可し、それ以外ではスクロールを維持するといった柔軟な実装が可能です。

touch 関連イベントのリスナー内で要素に touch-action を動的に設定してもスクロールを防げないため、このような場合は preventDefault() を使用します。これは、ブラウザーがジェスチャー開始時点で touch-action の値を評価し、その後の変更は無視される仕様となっているためです。

以下のデモでは、緑の四角形上で touchstart イベントが発生した際に preventDefault() を呼び出し、スクロールを抑制してドラッグ操作を継続できるようにしています。

const rect = document.getElementById('rect')

rect.addEventListener(
  'touchstart',
  (event) => {
    event.preventDefault()
  },
  {
    passive: false,
  },
)

結論 #

タッチデバイスでのドラッグアンドドロップ実装においては、スクロールとの競合によって pointercancel されてしまう問題に対処する必要があります。touch-action プロパティの設定やタッチイベントでの preventDefault() 呼び出しにより、この問題を解決できます。シンプルな実装には touch-action が適していますが、動的な制御が必要な場合は preventDefault() の使用も検討しましょう。