How To Track Mouse and Touch Move In Pressed State With React
Tags: website
Published: 2023-07-09 10:23:56

✨ [**Watch on YouTube**](https://youtu.be/Gj4Szl5pYFM)  |  🐙 [GitHub](https://github.com/RodionChachura/reactkit/blob/main/pages/press-tracker.tsx)  |  🎮 [Demo](https://reactkit.radzion.com/press-tracker)

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](https://cdn-images-1.medium.com/proxy/1*_PZQthX2w3M-cp85g6mFdA.jpeg)

## 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`.

```tsx
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.

```tsx
<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.