How To Track Mouse and Touch Move In Pressed State With React

July 8, 2023

3 min read

How To Track Mouse and Touch Move In Pressed State With React
Watch on YouTube

When we make complex interactive UIs like sliders, editor or color pickers, we often need to track mouse or touch movement in a pressed state. Let me share an easy-to-use abstract component designed precisely for this purpose.

Demo from RadzionKit
Demo from RadzionKit

PressTracker component

The component receives two properties:

  • render - a function that consumer component should use to render the content. It receives an object with props and position properties. The props field contains ref and mouse and touch start handlers, it should be passed to the container element. The position is a point with x and y coordinates in the range from 0 to 1. It represents the relative position of the cursor inside the container. In all my practical cases, I needed to know the relative position, so I decided to use it as a default.
  • onChange - a function that will be called when the position changes. It receives an object with the same position argument. Note that when the press origin is outside the container, the position will be null.
import { Point } from "lib/entities/Point"
import { useBoundingBox } from "lib/shared/hooks/useBoundingBox"
import { enforceRange } from "lib/shared/utils/enforceRange"
import {
  MouseEvent,
  MouseEventHandler,
  ReactNode,
  TouchEvent,
  TouchEventHandler,
  useCallback,
  useEffect,
  useState,
} from "react"
import { useEvent } from "react-use"

interface ContainerProps {
  onMouseDown: MouseEventHandler<HTMLElement>
  onTouchStart: TouchEventHandler<HTMLElement>
  ref: (node: HTMLElement | null) => void
}

interface ChangeParams {
  position: Point | null
}

interface RenderParams extends ChangeParams {
  props: ContainerProps
}

interface PressTrackerProps {
  render: (props: RenderParams) => ReactNode
  onChange?: (params: ChangeParams) => void
}

export const PressTracker = ({ render, onChange }: PressTrackerProps) => {
  const [container, setContainer] = useState<HTMLElement | null>(null)
  const box = useBoundingBox(container)

  const [position, setPosition] = useState<Point | null>(null)

  const handleMove = useCallback(
    ({ x, y }: Point) => {
      if (!box) return

      const { left, top, width, height } = box

      setPosition({
        x: enforceRange((x - left) / width, 0, 1),
        y: enforceRange((y - top) / height, 0, 1),
      })
    },
    [box]
  )

  const handleMouse = useCallback(
    (event: MouseEvent) => {
      handleMove({ x: event.clientX, y: event.clientY })
    },
    [handleMove]
  )

  const handleTouch = useCallback(
    (event: TouchEvent) => {
      const touch = event.touches[0]
      if (touch) {
        handleMove({ x: touch.clientX, y: touch.clientY })
      }
    },
    [handleMove]
  )

  useEffect(() => {
    if (onChange) {
      onChange({ position })
    }
  }, [onChange, position])

  const clearPosition = useCallback(() => {
    setPosition(null)
  }, [])
  useEvent("mouseup", position ? clearPosition : undefined)
  useEvent("touchend", position ? clearPosition : undefined)
  useEvent("mousemove", position ? handleMouse : undefined)
  useEvent("touchmove", position ? handleTouch : undefined)

  return (
    <>
      {render({
        props: {
          ref: setContainer,
          onMouseDown: handleMouse,
          onTouchStart: handleTouch,
        },
        position: position,
      })}
    </>
  )
}

The PressTracker component stores the container element in the useState and utilizes the useBoundingBox hook to obtain its position and size. To initiate the tracking process, the consumer component should render the PressTracker component and pass the necessary props to the container element.

<PressTracker
  render={({ props, position }) => (
    <Container {...props}>
      {position && (
        <Highlight
          style={{
            width: toPercents(position.x),
            height: toPercents(position.y),
          }}
        />
      )}
    </Container>
  )}
/>

We set the position on either mouse down on touch start events. So once the user have interacted with the container, we start tracking the cursor position with the useEvent hook from the react-use library. It listens for mouseup and touchend events to stop tracking, as well as mousemove and touchmove events to update the cursor position. The handleMove function converts absolute coordinates from touch and mouse events, ensuring that the coordinates remain within the range of 0 to 1, even when the cursor is outside the container.