Skip to content

Latest commit

 

History

History
301 lines (226 loc) · 14.3 KB

sensor-api.md

File metadata and controls

301 lines (226 loc) · 14.3 KB

Sensor API 🎮

sensor api logo

With our Sensor API it is possible to:

  • Create drag and drop interactions from any input type you can think of
  • Create beautiful scripted experiences

The public Sensor API is the same API that our mouse, keyboard, and touch sensors use. So it is powerful enough to drive any experience we ship out of the box.

Examples

⚠️ These following examples are based on react-beautiful-dnd and might not work with future version of @hello-pangea/dnd.

These are some examples to show off what is possible with the Sensor API. They are currently not built to be production ready. Feel free to reach out if you would like to help improve them or add your own!

(Please be sure to use prefix rbd-)

Voice 🗣 Webcam 📷 Thought 🧠
voice sensor webcam sensor thought sensor
rbd-voice-sensor
created by @danieldelcore
rbd-webcam-sensor
created by @kangweichan
rbd-thought-sensor
created by @charliegerard
With controls Run sheet Onboarding
controls multiple-drag-drop-context onboarding
Mapping controls to movements Running scripted experiences along side a user controlled drag A scripted onboarding experience for a trip planning application

Overview

You create a sensor that has the ability to attempt to claim a lock. A lock allows exclusive control of dragging within a <DragDropContext />. When you are finished with your interaction, you can then release the lock.

function mySimpleSensor(api: SensorAPI) {
  const preDrag: PreDragActions | null = api.tryGetLock('item-1');
  // Could not get lock
  if (!preDrag) {
    return;
  }

  const drag: SnapDragActions = preDrag.snapLift();

  drag.moveDown();
  drag.moveDown();
  drag.moveDown();

  drag.drop();
}

function App() {
  return (
    <DragDropContext sensors={[mySimpleSensor]}>{/*...*/}</DragDropContext>
  );
}

Lifecycle

programmatic state flow

  1. Try to get a lock when a sensor wants to drag an item. A sensor might not be able to claim a lock for a variety of reasons, such as when another sensor already has a lock.
  2. If a lock is obtained then there are a number of pre drag actions available to you (PreDragActions). This allows a sensor to claim a lock before starting a drag. This is important for things like sloppy click detection where a drag is only started after a sufficiently large movement.
  3. A pre drag lock can be upgraded to a drag lock, which contains a different set of APIs (FluidDragActions or SnapDragActions). Once a <Draggable /> has been lifted, it can be moved around.

Rules

  • Only one <Draggable /> can be dragging at a time for a <DragDropContext />
  • You cannot use outdated or aborted locks (see below)
  • That's it!

API

Creating a sensor

A sensor is a React hook. It is fine if you do not want to use any of the React hook goodness, you can treat the sensor just as a function. React hooks are just functions that let you use the built in React hooks if you want to 🤫. You pass your sensor into the sensors array on a <DragDropContext />.

function useMyCoolSensor(api: SensorAPI) {
  const start = useCallback(function start(event: MouseEvent) {
    const preDrag: PreDragActions | null = api.tryGetLock('item-2');
    if (!preDrag) {
      return;
    }
    preDrag.snapLift();
    preDrag.moveDown();
    preDrag.drop();
  }, []);

  useEffect(() => {
    window.addEventListener('click', start);

    return () => {
      window.removeEventListener('click', start);
    };
  }, []);
}

function App() {
  return (
    <DragDropContext sensors={[useMyCoolSensor]}>
      <Things />
    </DragDropContext>
  );
}

The sensors array should not change dynamically. If you do this you run the risk of violating the rules of React hooks.

You can also disable all of the prebuilt sensors (mouse, keyboard, and touch) by setting enableDefaultSensors={false} on a <DragDropContext />. This is useful if you only want a <DragDropContext /> to be controlled programmatically.

Controlling a drag: try to get a lock

A sensor is provided with a an object (SensorAPI) which is used to try to get a lock

type Sensor = (api: SensorAPI) => void;

interface SensorAPI {
  tryGetLock: TryGetLock;
  canGetLock: (id: DraggableId) => boolean;
  isLockClaimed: () => boolean;
  tryReleaseLock: () => void;
  findClosestDraggableId: (event: Event) => DraggableId | null;
  findOptionsForDraggable: (id: DraggableId) => DraggableOptions | null;
}

interface DraggableOptions {
  canDragInteractiveElements: boolean;
  shouldRespectForcePress: boolean;
  isEnabled: boolean;
}
  • tryGetLock (TryGetLock): a function that is used to try and get a lock for a <Draggable />
  • canGetLock(id): returns whether a lock could be claimed for a given DraggableId
  • isLockClaimed(): returns true if any sensor currently has a lock
  • tryReleaseLock(): will release any active lock. This can be useful for programmatically cancelling a drag.
  • findClosestDraggableId(event): a function that will try to find the closest draggableId based on an event. It will look upwards from the event.target to try and find a drag handle
  • findOptionsForDraggable(id): tries to lookup DraggableOptions associated with a <Draggable />
type TryGetLock = (
  draggableId: DraggableId,
  forceStop?: () => void,
  options?: TryGetLockOptions,
) => PreDragActions | null;
  • draggableId: The DraggableId of the <Draggable /> that you want to drag.
  • forceStop (optional): a function that is called when the lock needs to be abandoned by the application. See force abandoning locks.
interface TryGetLockOptions {
  sourceEvent?: Event;
}
  • sourceEvent (optional): Used to do further validation when starting the drag from a user input event. We will do some interactive element checking

Controlling a drag: pre drag (PreDragAction)

The PreDragAction object contains a number of functions:

interface PreDragActions {
  // discover if the lock is still active
  isActive: () => boolean;
  // whether it has been indicated if force press should be respected
  shouldRespectForcePress: () => boolean;
  // Lift the current item
  fluidLift: (clientSelection: Position) => FluidDragActions;
  snapLift: () => SnapDragActions;
  // Cancel the pre drag without starting a drag. Releases the lock
  abort: () => void;
}

This phase allows you to conditionally start or abort a drag after obtaining an exclusive lock. This is useful if you are not sure if a drag should start such as when using long press or sloppy click detection. If you want to abort the pre drag without lifting you can call .abort().

Controlling a drag: dragging

You can lift a dragging item by calling either .fluidLift(clientSelection) or snapLift(). This will start a visual drag and will also trigger the onDragStart responder. There are two different lift functions, as there are two different dragging modes: snap dragging (SnapDragActions) and fluid dragging (FluidDragActions).

Shared

interface DragActions {
  drop: (args?: StopDragOptions) => void;
  cancel: (args?: StopDragOptions) => void;
  isActive: () => boolean;
  shouldRespectForcePress: () => boolean;
}

interface StopDragOptions {
  shouldBlockNextClick: boolean;
}

Fluid dragging

✍️ Raathi Kugarajan has written a blog : "Scripted natural motion with react-beautiful-dnd" which outlines how you can create scripted user behaviour with the Sensor API 👏

<Draggable />s move around naturally in response a moving point. The impact of the drag is controlled by a collision engine. (This is what our mouse sensor and touch sensor use)

interface FluidDragActions extends DragActions {
  move: (clientSelection: Position) => void;
}

Calls to .move() are throttled using requestAnimationFrame. So if you make multiple .move() calls in the same animation frame, it will only result in a single update

const drag: SnapDragActions = preDrag.fluidLift({ x: 0, y: 0 });

// will all be batched into a single update
drag.move({ x: 0, y: 1 });
drag.move({ x: 0, y: 2 });
drag.move({ x: 0, y: 3 });

// after animation frame
// update(x: 0, y: 3)

Snap dragging

<Draggable />s are forced to move to a new position using a single command. For example, "move down". (This is what our keyboard sensor uses)

export interface SnapDragActions extends DragActions {
  moveUp: () => void;
  moveDown: () => void;
  moveRight: () => void;
  moveLeft: () => void;
}

Force abandoning locks

A lock can be aborted at any time by the application, such as when an error occurs. If you try to perform actions on an aborted lock then it will not do anything. The second argument to SensorAPI.tryGetLock() is a forceStop function. The forceStop function will be called when the lock needs to be abandoned by the application. If you try to use any functions on the lock after it has been abandoned they will have no effect and will log a warning to the console.

function useMySensor(api: SensorAPI) {
  let unbindClick;

  function forceStop() {
    if (unbindClick) {
      unbindClick();
    }
  }

  const preDrag: PreDragActions | null = api.tryGetLock('item-1', forceStop);
  // Could not get lock
  if (!preDrag) {
    return;
  }

  const drag: SnapDragActions = preDrag.snapLift();
  const move = () => drag.moveDown();
  window.addEventListener('click', move);
  unbindClick = window.removeEventListener('click', move);
}

The PreDragActions, FluidDragActions and SnapDragActions all have a isActive() function which can be called to discover if a lock is still active. So if you do not want to provide a forceStop() function, it is best to defensively call api's with a isActiveCheck.

function useMySensor(api: SensorAPI) {
  const preDrag: ?PreDragActions = api.tryGetLock();
  // Could not get lock
  if (!preDrag) {
    return;
  }

  const drag: SnapDragActions = preDrag.snapLift();
  const move = () => {
    if (drag.isActive()) {
      drag.moveDown();
      return;
    }
    // unbinding if no longer active
    window.removeEventListener('click', move);
  };
  window.addEventListener('click', move);
}

Invalid behaviours

These are all caused by not respecting the lifecycle (see above)

⚠️ = warning logged ❌ = error thrown

  • ⚠️ Using any PreDragAction, FluidDragAction or SnapDragAction after forceStop() is called
  • ⚠️ Using any PreDragAction after .abort() has been called
  • ⚠️ Using any FluidDragAction or SnapDragAction after .cancel() or .drop() has been called.
  • ❌ Trying to call two lift functions on a PreDragAction will result in an error being thrown.

← Back to documentation