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-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>
);
}
- Try to get a lock when a
sensor
wants to drag and item. A sensor might not be able to claim a lock for a variety of reasons, such as when anothersensor
already has a lock. - If a lock is obtained then there are a number of pre drag actions available to you (
PreDragActions
). This allows asensor
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. - A pre drag lock can be upgraded to a drag lock, which contains a different set of APIs (
FluidDragActions
orSnapDragActions
). 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 givenDraggableId
isLockClaimed()
: returnstrue
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 closestdraggableId
based on an event. It has the same behaviour asfindClosestDragHandle(event)
, except it just returns thedraggableId
, rather than the element.findClosestDragHandle(event)
: a function that will try to find the closest drag handle based on an event. It will look upwards from theevent.target
to try and find a drag handlefindOptionsForDraggable(id)
: tries to lookupDraggableOptions
associated with a<Draggable />
export type TryGetLock = (
draggableId: DraggableId,
forceStop?: () => void,
options?: TryGetLockOptions,
) => ?PreDragActions;
draggableId
: TheDraggableId
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 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 multipole .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
,FluidDragAction
orSnapDragAction
afterforceStop()
is called⚠️ Using anyPreDragAction
after.abort()
has been called⚠️ Using anyFluidDragAction
orSnapDragAction
after.cancel()
or.drop()
has been called.- ❌ Trying to call two
lift
functions on aPreDragAction
will result in an error being thrown.