Drop targets are elements that can be dropped upon by something that is dragging. There are different drop targets for different types of draggable items, such as elements or files.
// drop targets are exposed through adapters
import { dropTargetForExternal } from '@atlaskit/pragmatic-drag-and-drop/external/adapter';
const cleanup = dropTargetForExternal({
element: myElement,
});
// basic usage with react
import React, { useEffect, useRef } from 'react';
import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
function DropTarget() {
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const el = ref.current;
if (!el) {
throw new Error('ref not set correctly');
}
return dropTargetForElements({
element: el,
});
}, []);
return <div ref={ref}>Drop elements on me!</div>;
}
- Drop targets are scoped to a particular entity type. For example,
dropTargetForElements
is a drop target for elements, anddropTargetForExternal
is a drop target for files. - A single
element
can be used as a drop target for multiple entity types.
// ✅ Using the same element as a drop target for elements and for files
const cleanup = combine(
dropTargetForElements({
element: myElement,
}),
dropTargetForExternal({
element: myElement,
}),
);
- A single
element
cannot be used to create multiple drop targets for the same entity type (you will get a warning in yourconsole
if you make a mistake).
// ❌ Using the same element for two drop targets of the same entity type is not allowed
const cleanup = combine(
dropTargetForElements({
element: myElement,
}),
// ⚠️ A warning will be logged if this is detected
dropTargetForElements({
element: myElement,
}),
);
- Drop targets can be nested.
dropTargetForElements({
element: parentElement,
});
dropTargetForElements({
element: childElement,
});
- During a drag operation:
- You can add new drop targets
- You can remove drop targets
- You can remount a drop targets (see reconciliation)
- You can change the dimensions of any drop target
High level:
element
- theElement
the drop target will be attached togetData()
- data to associate with the drop targetcanDrop()
- whether the drop target can be dropped ongetDropEffect()
- control the cursor when over a drop targetgetIsSticky()
- whether the drop target will hold onto selection after no longer being dragged overonGenerateDragPreview
onDragStart
onDrag
onDropTargetChange
onDrop
element: Element;
The Element
you want to attach the drop target to.
getData?: (args: GetFeedbackArgs) => Record<string, unknown>;
getData()
is a function that returns data you want to attach to the drop target.
getData()
is called with GetFeedbackArgs
(see below) which contains limited information about
the current drag operation. Try to make your getData()
function pure (same input results in the
same output)
const cleanup = dropTargetForExternal({
element: myElement,
getData: () => ({ id: 'Alex' }),
});
getData()
is called repeatedly while the user is dragging over the drop target in order to power
addons. If your getData()
function is expensive, consider using the
once
utility function.
If you want to understand the type of data attached to a drop target elsewhere in your application, see our typing data guide.
canDrop?: (args: GetFeedbackArgs) => boolean
canDrop()
is used to conditionally block dropping on a drop target.
Returning false
from canDrop()
will not block dropping on parent or child drop targets. All drop
targets that want to not allow dropping, need to return false
from their canDrop()
function.
When looking for valid drop targets, a lookup starts from the deepest part of the DOM tree the user
is currently over and searches upwards for valid targets. If a drop target blocks dragging
(canDrop()
returns false
), then that drop target is ignored and the search upwards continues.
canDrop()
is called repeatedly while a drop target is being dragged over to allow you to
dynamically change your mind as to whether a drop target can be dropped on (including after it has
already been entered into). This could be helpful in a situation where you are waiting on some
permission information from a backend service.
// I can never be dropped on!
dropTargetForExternal({
element: myElement,
canDrop: () => false,
});
// only allow 'cards' to be dropped on this drop target
dropTargetForElements({
element: myOtherElement,
canDrop: ({ source }: GetFeedbackArgs) => {
return source.data.type === 'card';
},
});
type GetFeedbackArgs = {
input: Input;
source: SourcePayload; // this payload type will be different for different adapters
element: Element;
};
getDropEffect?: (args: GetFeedbackArgs) => DataTransfer['dropEffect']
The dropEffect
property will control the visual feedback (cursor) when dragging over it. As with
getData()
, getDropEffect()
is repeatedly called throughout a drag operation. The default
dropEffect
is dependent on the adapter.
dropTargetForElements({
getDropEffect: () => 'link',
});
getDropEffect()
is called repeatedly while a drop target is being dragged over to allow you
change your mind about which drop effect should be applied.
When working with nested drop targets, the inner most drop targets dropEffect
is the one that
will be applied; even if inner most drop target is using the default value ("move"
).
For more information about controlling the users cursor while dragging, see our cursor guide
getIsSticky?: (args: GetFeedbackArgs) => boolean;
Used to control whether a drop target is "sticky" or not.
Drop targets are generally calculated based on where the user's pointer is currently located. In some scenarios you might want to hold on to a previous drop target (make it "sticky"), even when the drop target is no longer being directly dragged over. This is useful if you want to maintain a selection while you are in gaps between drop targets.
getIsSticky()
is called repeatedly
while a drop target is no longer being dragged over to
determine whether a drop target should be sticky.
dropTargetForElements({
element: myElement,
getIsSticky: () => true,
});
dropTargetForElements({
element: myElement,
getIsSticky: ({ source }: GetFeedbackArgs): boolean => {
// only be sticky when dragging something with 'author: Alex'
return source.data.author === 'Alex';
},
});
The stickiness algorithm
A drop target that otherwise would not be dragged over any more due to changes in the users pointer, will continue to be marked as "dragged over" when:
- The drop target is still mounted in the DOM, AND
- The drop target
canDrop()
returnstrue
AND - The drop target
getIsSticky()
returnstrue
AND - The parent of a drop target is unchanged
[]
= Which drop targets are currently dragged over
[A]
= over the A
drop target
Scenario: [A(sticky)]
→ []
Result: [A]
Scenario: [B(sticky), A(sticky)]
→ []
Result: [B, A]
Scenario: [C, B(sticky), A(sticky)]
→ []
Result: [B, A]
Scenario: [A(sticky)]
→ [B]
Result: [B]
Scenario: [B(sticky), A]
→ [A]
Result: [B, A]
Scenario: [B, A(sticky)]
→ [A]
Result: [A]
Scenario: [B(sticky), A]
→ [X]
Result: [X]
Scenario: [B(sticky), A]
→ []
Result: []
Scenario: [B(sticky), A(sticky)]
→ [X]
Result: [X]
Scenario: [A(sticky)]
→ []
+ A:canDrop()
returns false
Result: []
Stickiness is not maintained when an old drop target states it cannot be dropped on
Scenario: [A(sticky)]
→ []
+ A:getIsSticky()
returns false
Result: []
Stickiness is not maintained when an old drop target states it it is no longer sticky
Scenario: [A(sticky)]
→ []
+ A
is unmounted
Result: []
Stickiness is not maintained when an old drop target is unmounted
Other notes:
- All drop targets, regardless of stickiness, will no longer be "dragged over" when the users
pointer moves out of the
window
. - Drop targets that are no longer dragged over, but are still active due to stickiness, will not
have their data recomputed with
getData()
, or drop effect recomputed withgetDropEffect()
. The lastdata
anddropEffect
created when a drop target is actively being dragged over is preserved.
When calculating what drop targets are currently being dragged over, we look from the deepest
possible drop target upwards (bubble ordering). We will search up to the document root to find any
available drop targets. If a drop target specifies that it cannot be dropped on (canDrop()
returns false
), then it will be ignored and the search will continue upwards.
Scenario [] -> [B, A(blocked)]
Result: [] -> [B]
Flow:
B
visited and allows dropping. Drop targets:[] -> [B]
A
visited and does not allow dropping
Scenario [] -> [C, B(blocked), A]
Result: [] -> [C, A]
- Going from no drop targets
[]
to three drop targets:[C, B, A]
(bubble ordered). C
andA
allow dropping, butB
has blocked dropping
Flow:
C
visited and allows dropping. Drop targets:[] -> [C]
B
visited and does not allow droppingA
visited and allows dropping. Drop targets[C] -> [C, A]
When you have nested drop targets all of them will have their onDrop()
callbacks executed on a
drop. Let's say we have two drop targets parent
and child
. You might want to perform one
operation if parent
is dropped on, and a different operation if child
is dropped on. In
parent
s onDrop()
how do you tell if child
was dropped on?
We can leverage the fact that location.current.dropTargets
is always bubbled ordered, so inner
drop targets come before outer ones
dropTargetForElements({
element: parent,
onDrop({ location, self }) {
// we know that if 'self' is in the first position, then no inner drop target was dropped on
if (location.current.dropTargets[0]?.element === self.element) {
action1();
return;
}
// at this point we know that an inner drop target was dropped on
action2();
},
});