DEV Community

Cover image for How To Track Mouse and Touch Move In Pressed State With React
Radzion Chachura
Radzion Chachura

Posted on • Originally published at radzion.com

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

Watch on YouTube | 🐙 GitHub | 🎮 Demo

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 ReactKit

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,
      })}
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )}
/>
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)