useMove

Handles move interactions across mouse, touch, and keyboard, including dragging with the mouse or touch, and using the arrow keys. Normalizes behavior across browsers and platforms, and ignores emulated mouse events on touch devices.

installyarn add @react-aria/interactions
version3.4.0
usageimport {useMove} from '@react-aria/interactions'

API#


useMove( (props: MoveEvents )): MoveResult

Features#


useMove handles move interactions across mouse, touch, and keyboard. A move interaction starts when a user presses down with a mouse or their finger on the target, and ends when they lift their pointer. Move events are fired as the pointer moves around, and specify the distance that the pointer traveled since the last event. In addition, move events are fired when the user focuses the target element and uses the arrow keys.

  • Handles mouse and touch events
  • Handles arrow key presses
  • Uses pointer events where available, with fallbacks to mouse and touch events
  • Ignores emulated mouse events in mobile browsers
  • Handles disabling text selection on mobile while the press interaction is active
  • Normalizes many cross browser inconsistencies

Usage#


useMove returns props that you should spread onto the target element:

NameTypeDescription
movePropsHTMLAttributes<HTMLElement>Props to spread on the target element.

useMove supports the following event handlers:

NameTypeDescription
onMoveStart( (e: MoveStartEvent )) => voidHandler that is called when a move interaction starts.
onMove( (e: MoveMoveEvent )) => voidHandler that is called when the element is moved.
onMoveEnd( (e: MoveEndEvent )) => voidHandler that is called when a move interaction ends.

Each of these handlers is fired with a MoveEvent, which exposes information about the target and the type of event that triggered the interaction.

NameTypeDescription
type'move'The type of move event being fired.
deltaXnumberThe amount moved in the X direction since the last event.
deltaYnumberThe amount moved in the Y direction since the last event.
pointerTypePointerTypeThe pointer type that triggered the move event.

Example#


This example shows a ball that can be moved by dragging with a mouse or touch, or by tabbing to it and using the arrow keys on your keyboard. The movement is clamped so that the ball cannot be dragged outside a box. In addition, all of the move events are logged below so that you can inspect what is going on.

function Example() {
  const CONTAINER_SIZE = 200;
  const BALL_SIZE = 30;

  let [events, setEvents] = React.useState([]);
  let [color, setColor] = React.useState('black');
  let [position, setPosition] = React.useState({
    x: 0,
    y: 0
  });

  let clamp = (pos) => Math.min(Math.max(pos, 0), CONTAINER_SIZE - BALL_SIZE);
  let {moveProps} = useMove({
    onMoveStart(e) {
      setColor('red');
      setEvents((events) => [
        `move start with pointerType = ${e.pointerType}`,
        ...events
      ]);
    },
    onMove(e) {
      setPosition(({x, y}) => {
        // Normally, we want to allow the user to continue
        // dragging outside the box such that they need to
        // drag back over the ball again before it moves.
        // This is handled below by clamping during render.
        // If using the keyboard, however, we need to clamp
        // here so that dragging outside the container and
        // then using the arrow keys works as expected.
        if (e.pointerType === 'keyboard') {
          x = clamp(x);
          y = clamp(y);
        }

        x += e.deltaX;
        y += e.deltaY;
        return {x, y};
      });

      setEvents((events) => [
        `move with pointerType = ${e.pointerType}, deltaX = ${e.deltaX}, deltaY = ${e.deltaY}`,
        ...events
      ]);
    },
    onMoveEnd(e) {
      setColor('black');
      setEvents((events) => [
        `move end with pointerType = ${e.pointerType}`,
        ...events
      ]);
    }
  });

  return (
    <>
      <div
        style={{
          width: CONTAINER_SIZE,
          height: CONTAINER_SIZE,
          background: 'white',
          border: '1px solid black',
          position: 'relative',
          touchAction: 'none'
        }}>
        <div
          {...moveProps}
          tabIndex={0}
          style={{
            width: BALL_SIZE,
            height: BALL_SIZE,
            borderRadius: '100%',
            position: 'absolute',
            left: clamp(position.x),
            top: clamp(position.y),
            background: color
          }}
        />
      </div>
      <ul
        style={{
          maxHeight: '200px',
          overflow: 'auto'
        }}>
        {events.map((e, i) => (
          <li key={i}>{e}</li>
        ))}
      </ul>
    </>
  );
}
function Example() {
  const CONTAINER_SIZE = 200;
  const BALL_SIZE = 30;

  let [events, setEvents] = React.useState([]);
  let [color, setColor] = React.useState('black');
  let [position, setPosition] = React.useState({
    x: 0,
    y: 0
  });

  let clamp = (pos) =>
    Math.min(Math.max(pos, 0), CONTAINER_SIZE - BALL_SIZE);
  let {moveProps} = useMove({
    onMoveStart(e) {
      setColor('red');
      setEvents((events) => [
        `move start with pointerType = ${e.pointerType}`,
        ...events
      ]);
    },
    onMove(e) {
      setPosition(({x, y}) => {
        // Normally, we want to allow the user to continue
        // dragging outside the box such that they need to
        // drag back over the ball again before it moves.
        // This is handled below by clamping during render.
        // If using the keyboard, however, we need to clamp
        // here so that dragging outside the container and
        // then using the arrow keys works as expected.
        if (e.pointerType === 'keyboard') {
          x = clamp(x);
          y = clamp(y);
        }

        x += e.deltaX;
        y += e.deltaY;
        return {x, y};
      });

      setEvents((events) => [
        `move with pointerType = ${e.pointerType}, deltaX = ${e.deltaX}, deltaY = ${e.deltaY}`,
        ...events
      ]);
    },
    onMoveEnd(e) {
      setColor('black');
      setEvents((events) => [
        `move end with pointerType = ${e.pointerType}`,
        ...events
      ]);
    }
  });

  return (
    <>
      <div
        style={{
          width: CONTAINER_SIZE,
          height: CONTAINER_SIZE,
          background: 'white',
          border: '1px solid black',
          position: 'relative',
          touchAction: 'none'
        }}>
        <div
          {...moveProps}
          tabIndex={0}
          style={{
            width: BALL_SIZE,
            height: BALL_SIZE,
            borderRadius: '100%',
            position: 'absolute',
            left: clamp(position.x),
            top: clamp(position.y),
            background: color
          }}
        />
      </div>
      <ul
        style={{
          maxHeight: '200px',
          overflow: 'auto'
        }}>
        {events.map((e, i) => (
          <li key={i}>{e}</li>
        ))}
      </ul>
    </>
  );
}
function Example() {
  const CONTAINER_SIZE = 200;
  const BALL_SIZE = 30;

  let [
    events,
    setEvents
  ] = React.useState([]);
  let [
    color,
    setColor
  ] = React.useState(
    'black'
  );
  let [
    position,
    setPosition
  ] = React.useState({
    x: 0,
    y: 0
  });

  let clamp = (pos) =>
    Math.min(
      Math.max(pos, 0),
      CONTAINER_SIZE -
        BALL_SIZE
    );
  let {
    moveProps
  } = useMove({
    onMoveStart(e) {
      setColor('red');
      setEvents(
        (events) => [
          `move start with pointerType = ${e.pointerType}`,
          ...events
        ]
      );
    },
    onMove(e) {
      setPosition(
        ({x, y}) => {
          // Normally, we want to allow the user to continue
          // dragging outside the box such that they need to
          // drag back over the ball again before it moves.
          // This is handled below by clamping during render.
          // If using the keyboard, however, we need to clamp
          // here so that dragging outside the container and
          // then using the arrow keys works as expected.
          if (
            e.pointerType ===
            'keyboard'
          ) {
            x = clamp(x);
            y = clamp(y);
          }

          x += e.deltaX;
          y += e.deltaY;
          return {x, y};
        }
      );

      setEvents(
        (events) => [
          `move with pointerType = ${e.pointerType}, deltaX = ${e.deltaX}, deltaY = ${e.deltaY}`,
          ...events
        ]
      );
    },
    onMoveEnd(e) {
      setColor('black');
      setEvents(
        (events) => [
          `move end with pointerType = ${e.pointerType}`,
          ...events
        ]
      );
    }
  });

  return (
    <>
      <div
        style={{
          width: CONTAINER_SIZE,
          height: CONTAINER_SIZE,
          background:
            'white',
          border:
            '1px solid black',
          position:
            'relative',
          touchAction:
            'none'
        }}>
        <div
          {...moveProps}
          tabIndex={0}
          style={{
            width: BALL_SIZE,
            height: BALL_SIZE,
            borderRadius:
              '100%',
            position:
              'absolute',
            left: clamp(
              position.x
            ),
            top: clamp(
              position.y
            ),
            background: color
          }}
        />
      </div>
      <ul
        style={{
          maxHeight:
            '200px',
          overflow:
            'auto'
        }}>
        {events.map(
          (e, i) => (
            <li key={i}>
              {e}
            </li>
          )
        )}
      </ul>
    </>
  );
}