docs/sensors/sensor-api.md
With our Sensor API it is possible to:
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-sensor | ||
| created by @danieldelcore | rbd-webcam-sensor | |
| created by @kangweichan | rbd-thought-sensor | |
| created 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>
);
}
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.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.FluidDragActions or SnapDragActions). Once a <Draggable /> has been lifted, it can be moved around.<Draggable /> can be dragging at a time for a <DragDropContext />sensorA 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 given DraggableIdisLockClaimed(): returns true if 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 closest draggableId based on an event. It will look upwards from the event.target to try and find a drag handlefindOptionsForDraggable(id): tries to lookup DraggableOptions associated with a <Draggable />export type TryGetLock = (
draggableId: DraggableId,
forceStop?: () => void,
options?: TryGetLockOptions,
) => ?PreDragActions;
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.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 checkingPreDragAction)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
PreDragAction, FluidDragAction or SnapDragAction after forceStop() is calledPreDragAction after .abort() has been calledFluidDragAction or SnapDragAction after .cancel() or .drop() has been called.lift functions on a PreDragAction will result in an error being thrown.