ポインターキャプチャの活用
この記事はユアマイスター アドベントカレンダー 2022の 5 日目の記事です。
はじめに
PointerEventの便利なメソッドの一つ、ポインターキャプチャをご存じでしょうか。
ポインターキャプチャは、ポインターキャプチャリリースが発生するまでの PointerEvent
が、常にキャプチャを行なった要素上で行われているかのような挙動を実現するものです。それを活用して簡単にドラッグスクロール等の動きを実装することができます。
このポインターキャプチャを実装する上でいくつか躓きがありましたので、原因と解決例を共有したいと思います。
使用例
ポインターキャプチャを使用してのドラッグスクロールの実装方法例が MDN に記載されています。
HTML
<div id="slider">SLIDE ME</div>
JavaScript
function beginSliding(e) { slider.onpointermove = slide slider.setPointerCapture(e.pointerId) } function stopSliding(e) { slider.onpointermove = null slider.releasePointerCapture(e.pointerId) } function slide(e) { slider.style.transform = `translate(${e.clientX - 70}px)` } const slider = document.getElementById('slider') slider.onpointerdown = beginSliding slider.onpointerup = stopSliding
slider 要素上でドラッグし始めると、pointerdown
が発生し setPointerCapture()
にてポインターキャプチャが行われます。それによりその後の slider 要素外で行われる pointermove
が slider 要素上で行われていると認識される状態になり、ドラッグに伴い slider 要素がスライドします。ドラッグを離すと、pointerup
が発生しキャプチャのリリースが行なわれるため通常状態に戻り、スライドが止まります。
※ releasePointerCapture()
を行わなくても、pointerup
の際にはリリースが発生します。
以下ではこの例を使用して発生した問題を紹介していきます。
問題1:スライダーにボタンを乗せるとクリックができない
商品に対して複数タグ付けを行いたい・タグをドラッグスクロール可能にしたい・タグをクリックして遷移させたい、という要件の元開発を進めていたところ、クリックしても遷移が発生しないという状況に陥ったのが経緯です。ボタンに限らず、スライダーの子要素にクリックイベントで動作させたいものを設置してもクリックを行えません。
<div id="slider">
<button id="inner">SLIDE ME</button>
</div>
const inner = document.getElementById('inner')
inner.onclick = doSomething
function doSomething(e) {
// 任意の処理
}
原因
子要素の click
イベントが発生していないためです。
ドラッグを離した時、 pointerup
→ click
→ キャプチャリリース の順にイベントが発生します。そのため、click
(=PointerEvent の一つ)
は slider 要素のキャプチャ中に行われているために slider 要素上で発生し、バブリングの原理に則り子要素であるボタンには伝播しません。
解決例
pointermove
が発生した時にキャプチャするようにします。そうすると pointermove
しない場合にはキャプチャが行われず、子要素で click
を発生させることができます。
function beginSliding(e) {
slider.onpointermove = slide
}
function slide(e) {
slider.style.transform = `translate(${e.clientX - 70}px)`
slider.setPointerCapture(e.pointerId) // 移動
}
問題2:Safari ではドラッグを離すとボタンをクリックしてしまう
上記問題とは反対に、Safariだとドラッグした後にボタン上でクリックを離すとボタンをクリックしてしまうという事象が起きました。
※Safari バージョン 15.2 (2022/11/30 時点) にて確認
原因
キャプチャのリリースが click イベントよりも早いためです。
他ブラウザでは pointerup
→ click
→ リリース の順だったのに対して、Safari では pointerup
→ リリース → click
の順でイベントが発生するため、click
が子要素で発生してしまいます。
解決例
click
時に行いたかった処理を pointerup
時に行うようにします。キャプチャ時は子要素で pointerup
は発生せず、キャプチャしていない時は発生するため、pointermove
しない時のみ処理を行うなど上述の他ブラウザの場合と同じような挙動が期待できます。
inner.onpointerup = doSomething
問題3:ドラッグを離してもスライドしてしまう時がある
ドラッグを離しても、カーソルを動かすとスライドが行われる現象が発生する場合があります。
原因
意図しない pointercancel が発生しているためです。
キャプチャがリリースされるタイミングは、pointerup
時と releasePointerCapture()
を行なった時に加えてもう一つあり、それはpointercancel イベントの発生時です。pointercancel
が起こるとリリースが行われ、slider 要素外でドラッグを離しても slider 要素の pointerup
とは認識されなくなるため、pointerup
時のメソッドに記述している slide 関数の解除が行われません。(もちろん要素内でドラッグを離した場合はキャプチャ関係なく slider 要素の pointerup
が発生します)
解決例
pointercancel
時、あるいはキャプチャリリース時に関数の解除を行うようにします。
slider.onpointercancel = stopSliding
// あるいは
slider.onlostpointercapture = stopSliding
まとめ
- キャプチャが行われているときはその子要素で
PointerEvent
を発生させられない pointerup
時のイベントの発生順はpointerup
→click
→ リリース 、Safari ではpointerup
→ リリース →click
pointerup
又はpointercancel
又はreleasePointerCapture()
の時にキャプチャのリリースが発生する