PC の Web ブラウザ用に HTML と JavaScript でインタラクティブな UI を作る時に、ちょっとした工夫が使い勝手の印象を大きく変えます。今回はスライダーの UI を題材として、ドラッグ時の動きが良い感じになる工夫について説明します。
結論
対象の要素の mousemove
ではなく、document
の mousemove
を使うと良い感じになるよ
ドラッグ対象の要素のイベントを使う場合
mousedown
でドラッグを開始し、mousemove
イベントが発火する度に offsetX や offsetY を取得してスライダーに反映します。
マウスポインターがスライダーの領域を外れるとドラッグが継続できず、ぎこちない感じになりますね。また、スライダーの領域外でドラッグを終了すると、mouseup
が呼ばれず、次にスライダーにポインターを当てるとクリックしていない状態でもドラッグ状態になります。
document のイベントを使う場合
mousedown
でドラッグを開始するのは同じですが、mousemove
と mouseup
は document のイベントを使います。
こうすることで、スライダーの領域外だけでなく、ブラウザーのウィンドウ外でもドラッグが効くことにご注目ください。
document のイベントを使うため、offsetX がドラッグ対象の要素のローカル座標になっていません。そのためドラッグ開始時の clientX と、ドラッグ中の clientX の差分をとって結果を計算します。
便利関数と React
このパターンは良く使うので、以下のような関数を作っておくと便利です。
export interface DragHandler { onMouseMove?: (e: MouseEvent) => void onMouseUp?: (e: MouseEvent) => void onClick?: (e: MouseEvent) => void } export const observeDrag = ({ onMouseMove, onMouseUp, onClick, }: DragHandler) => { let isMoved = false const onGlobalMouseMove = (e: MouseEvent) => { isMoved = true onMouseMove?.(e) } const onGlobalMouseUp = (e: MouseEvent) => { onMouseUp?.(e) if (!isMoved) { onClick?.(e) } document.removeEventListener("mousemove", onGlobalMouseMove) document.removeEventListener("mouseup", onGlobalMouseUp) } document.addEventListener("mousemove", onGlobalMouseMove) document.addEventListener("mouseup", onGlobalMouseUp) }
また、下記のように React と組み合わせて使うのもなかなかに良いです。
const onMouseDown = useCallback((e) => { ... observeDrag({ onMouseMove: (e) => { // なにかする }, onMouseUp: (e) => { } }) }, []) <div onMouseDown={onMouseDown}> hello </div>
mousedown 時のイベントを observeDrag の引数に取って、clientX, clientY の差分を取る処理を実装するのもいいかもしれません。
export const observeDrag = ({ mouseDownEvent, onMouseMove, onMouseUp, onClick, }) => { let isMoved = false const startClientX = mouseDownEvent.clientX const startClientY = mouseDownEvent.clientY const onGlobalMouseMove = (e: MouseEvent) => { isMoved = true const deltaX = e.clientX - startClientX const deltaY = e.clientY - startClientY onMouseMove?.(e, deltaX, deltaY) } const onGlobalMouseUp = (e: MouseEvent) => { onMouseUp?.(e) if (!isMoved) { onClick?.(e) } document.removeEventListener("mousemove", onGlobalMouseMove) document.removeEventListener("mouseup", onGlobalMouseUp) } document.addEventListener("mousemove", onGlobalMouseMove) document.addEventListener("mouseup", onGlobalMouseUp) }
おわりに
以上、UX を改善する小ネタでした。 大した内容ではありませんが、こういうちょっとした使い勝手の良さを積み重ねることって大事ですよね。仕事だとこういった工夫は細かすぎて仕様として載らなかったり、なかなかやる余裕が無いことも多いですが、できればやっていきたいものです。 自作のドロップダウンメニューをエスケープキーで閉じられるようにするとか、そういう感じのことをぜひやっていきましょう。