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.
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 🧠 |
|---|---|---|
![]() |
![]() |
![]() |
rbd-voice-sensorcreated by @danieldelcore |
rbd-webcam-sensorcreated by @kangweichan |
rbd-thought-sensorcreated by @charliegerard |
| With controls | Run sheet | Onboarding |
|---|---|---|
![]() |
![]() |
![]() |
| Mapping controls to movements | Running scripted experiences along side a user controlled drag | A scripted onboarding experience for a trip planning application |
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 = 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>
);
}- Try to get a lock when a
sensorwants to drag an item. A sensor might not be able to claim a lock for a variety of reasons, such as when anothersensoralready has a lock. - If a lock is obtained then there are a number of pre drag actions available to you (
PreDragActions). This allows asensorto 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. - A pre drag lock can be upgraded to a drag lock, which contains a different set of APIs (
FluidDragActionsorSnapDragActions). Once a<Draggable />has been lifted, it can be moved around.
- Only one
<Draggable />can be dragging at a time for a<DragDropContext /> - You cannot use outdated or aborted locks (see below)
- That's it!
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 = 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.
A sensor is provided with a an object (SensorAPI) which is used to try to get a lock
type Sensor = (api: SensorAPI) => void;
type SensorAPI = {|
tryGetLock: TryGetLock,
canGetLock: (id: DraggableId) => boolean,
isLockClaimed: () => boolean,
tryReleaseLock: () => void,
findClosestDraggableId: (event: Event) => ?DraggableId,
findOptionsForDraggable: (id: DraggableId) => ?DraggableOptions,
|};
type 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 givenDraggableIdisLockClaimed(): returnstrueif any sensor currently has a locktryReleaseLock(): will release any active lock. This can be useful for programmatically cancelling a drag.findClosestDraggableId(event): a function that will try to find the closestdraggableIdbased on an event. It will look upwards from theevent.targetto try and find a drag handlefindOptionsForDraggable(id): tries to lookupDraggableOptionsassociated with a<Draggable />
export type TryGetLock = (
draggableId: DraggableId,
forceStop?: () => void,
options?: TryGetLockOptions,
) => ?PreDragActions;draggableId: TheDraggableIdof 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.
type 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
The PreDragAction object contains a number of functions:
type 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().
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).
type DragActions = {|
drop: (args?: StopDragOptions) => void,
cancel: (args?: StopDragOptions) => void,
isActive: () => boolean,
shouldRespectForcePress: () => boolean,
|};
type StopDragOptions = {|
shouldBlockNextClick: boolean,
|};✍️ 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)
type FluidDragActions = {|
...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)<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 type SnapDragActions = {|
...DragActions,
moveUp: () => void,
moveDown: () => void,
moveRight: () => void,
moveLeft: () => void,
|};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 = 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);
}These are all caused by not respecting the lifecycle (see above)
⚠️ = warning logged ❌ = error thrown
⚠️ Using anyPreDragAction,FluidDragActionorSnapDragActionafterforceStop()is called⚠️ Using anyPreDragActionafter.abort()has been called⚠️ Using anyFluidDragActionorSnapDragActionafter.cancel()or.drop()has been called.- ❌ Trying to call two
liftfunctions on aPreDragActionwill result in an error being thrown.







