react-move-hook
TypeScript icon, indicating that this package has built-in type declarations

0.1.2 • Public • Published

npm MIT License Actions Status codecov

react-move-hook

A unopinionated customizable react hook without dependencies (except for React) to move stuff around. This library keeps track of an element being moved around.

Description

This hook is created to track movements on an HTMLElement, it will not add any CSS properties on the element on its own accord. This is done on purpose, so a developer has control over what styles are actually being used (such as css transforms or top/left positioning).

By default this hook will bind listeners to mouse and touch events, but the hook can also be configured to use different methods to move an element around (such as keyboard events).

By default, useMovable will use getBoundingClientRect() to do measurements on HTML elements.

Install

You can install react-move-hook with npm

npm install --save react-move-hook

or yarn

yarn add react-move-hook

Usage

A simple example (open in codesandbox)
import React, { useState, useCallback, useEffect } from "react";
import { useMovable } from "react-move-hook";

import "./App.css";

function App() {
  const [state, setState] = useState({
    moving: false,
    delta: undefined,
  });

  useEffect(() => {
    // Adding a class with overflow: hidden to body, so screen doesn't move while using touch input
    document.body.classList.toggle("moving", state.moving);
  }, [state.moving]);

  const handleChange = useCallback((moveData) => {
    setState({
      ...moveData,
      delta: moveData.moving ? moveData.delta : undefined,
    });
  }, []);

  const ref = useMovable({ onChange: handleChange, bounds: "parent" });

  const style = {
    backgroundColor: state.moving ? "red" : "transparent",
    transform: state.delta
      ? `translate3d(${state.delta.x}px, ${state.delta.y}px, 0)`
      : undefined,
  };

  return (
    <div className="container">
      <div ref={ref} className="movable" style={style}>
        MOVE ME
      </div>
    </div>
  );
}

export default App;

More examples

  • codesandbox Using a handle to move an element around.
  • codesandbox Using useState() to update positions.
  • codesandbox Using the keyboard to move stuff around with a custom connect() option.
  • codesandbox Making multiple elements movable (try moving them around at the same time on your mobile device).
  • codesandbox Restricting movement through the bounds and axis options.

Options

interface UseMovableOptions {
  /**
   * The boundary in which the element can be moved.
   *
   * When not set, the element can be moved around freely.
   *
   * When set to ```"parent"```, will use parent element of referenced
   * element or the parent of the given sizeRef element.
   *
   * Can also be given a DOMRect, a function returning a DOMRect or a reference to
   * another HTMLElement.
   */
  bounds?:
    | "parent"
    | (() => DOMRect | undefined)
    | DOMRect
    | React.RefObject<HTMLElement | undefined>;

  /**
   * Restricts the axis on which moves can be made.
   * ```"x"``` is the horizontal axis, ```"y"``` is the vertical axis.
   */
  axis?: "x" | "y";

  /**
   * The element that defines the size of "what" is being moved around.
   *
   * This is used when you are moving an element, but are initiating movements
   * through another element (usually a handle of some sorts within the element
   * being moved around.
   */
  sizeRef?: React.RefObject<HTMLElement>;

  /**
   * Called when moving starts.
   */
  onMove?: (data: MoveEvent) => void;

  /**
   * Called when the elements starts moving
   */
  onMoveStart?: (data: MoveEvent) => void;

  /**
   * Called when element stops moving, make sure this function is memoized
   */
  onMoveEnd?: (data: MoveEvent) => void;

  /**
   * Function that is called on move-start, move and move-end
   */
  onChange?: (date: MoveEvent) => void;

  /**
   * A function connects the hook to the referenced HTML element. See
   * MovableConnect for more information.
   */
  connect?: MovableConnect;

  /**
   * Measuring method, defaults to getBoundingClientRect
   */
  measure?: (el: HTMLElement) => BoundingRect;
}

Using events

You can use the onMoveStart, onMove, onMoveEnd, onChange options to listen for position changes of the element being dragged. These functions will all take a MoveEvent object whenever they're called.

It's important to note that you have to use useCallback to create a memoized callback function.

const handleMove = useCallback((e) => {
  console.log(e);
}, []);

const ref = useMovable({ onMove: handleMove });

A MoveEvent has the following shape

type MoveEvent = {
  // The position from which this move was initiated.
  origin: { x: number, y: number },
  // The current position (this will not be bounded)
  position: { x: number, y: number },
  // The distance from the origin
  delta: { x: number, y: number },
  // The original [DOMRect](https://developer.mozilla.org/en-US/docs/Web/API/DOMRect) of the element being moved
  originRect: DOMRect,
  // True if moving
  moving: boolean,
  // True if started moving
  startedMoving: boolean,
  // True if stopped moving
  stoppedMoving: boolean, 
}

Restricting movement

Bounds

By default, an element passed into the useMovable callback can move unrestricted. If you want to create bounds within which an element can be moved you can use the bounds option. The bounds option can take a value of the following type:

  bounds?:
    | "parent"
    | (() => DOMRect | undefined)
    | DOMRect
    | React.RefObject<HTMLElement | undefined>;

If bounds is set to "parent" it will set the bounds to the DOMRect of the parent element of the element being moved. If the sizeRef option is also used, it will use the parent of the sizeRef element.

You can also pass a DOMRect directly, or a function returning a DOMRect.

If you pass a React.RefObject, it will use the DOMRect of that element to set the bounds.

The event handlers will now only receive events with a delta that is within the set bounds.

Axis restriction

If you want to restrict movement on an axis (for example, only horizontal movement). You can use the "axis" option. The option can be set to either "x" or "y". If set to "x", you will receive events only containing a delta on the "x" values.

Customizing behaviour

For a working example check codesandbox.

By default useMovable will listen to touch and mouse events.

This functionality can easily be extended or overwritten using a custom connect option.

useMovable takes a connect option that is a function of the following signature:

(args: {
  actions: { 
    // Called when the move starts, required before calling any of the other actions.
    moveStart: ({ x: number, y: number }) => void,

    // Called to move to a new position relative from the current position, 
    // so move ({ x: 10, y: 0 }) will move 10px to the right of the last position.
    move: ({ x: number, y: number }) => void,

    // Used to move to an absolute new position.
    moveTo: ({ x: number, y: number }) => void,

    // Called when the move ended. 
    moveEnd: () => void
  },
  // The element passed into the useMovable callback
  el: HTMLElement
}) => () => void;

This is a function that will return another function to clean up any listeners or other side effects, a bit like the useEffect hook.

The default connect function is a composition of the withMouse and withTouch connect functions defined in src/connect.ts

If you want to create your own connect function you can use the createConnect(). For example, to listen to keyboard events you could use the following code.

import { createConnect } from "react-move-hook";

export const withKeyboard = createConnect(({ actions, el }) => {
  const moveListener = (e) => {
    if (document.activeElement !== el) return;
    e.preventDefault();
    actions.moveStart();
    switch (e.key) {
      case "ArrowUp":
        actions.move({ x: 0, y: -10 });
        break;
      case "ArrowRight":
        actions.move({ x: 10, y: 0 });
        break;
      case "ArrowDown":
        actions.move({ x: 0, y: 10 });
        break;
      case "ArrowLeft":
        actions.move({ x: -10, y: 0 });
        break;
      default:
    }
    actions.moveEnd();
  };

  window.addEventListener("keydown", moveListener);

  // This will clean up the listeners when element is dismounted or
  // another element is passed into the useMovable callback.
  return () => {
    window.removeEventListener("keydown", moveListener);
  };
});

You can then use this function to compose new connect function, for example if you want to support both mouse and keyboard input, you could make a new composition like this:

const withMouseAndKeyboard = withMouse(withKeyboard());

or using a compose function (like the one in ramda):

import { compose } from "ramda";

const withMouseAndKeyboard = compose(withMouse, withKeyboard)();

This function can now be passed into the connect option like:

const ref = useMovable({ connect: withMouseAndKeyboard });

Package Sidebar

Install

npm i react-move-hook

Weekly Downloads

868

Version

0.1.2

License

MIT

Unpacked Size

72.8 kB

Total Files

17

Last publish

Collaborators

  • davidammeraal