Drop targets

Drop targets are elements that items can be dropped on.

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.

Basic usage

// 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>;
}

Rules

  • Drop targets are scoped to a particular entity type. For example, dropTargetForElements is a drop target for elements, and dropTargetForExternal 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 your console 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,
});
Dark theme
Drag me 👋
Grandparent 👵
Parent 1 👩
Child 1 🧒
Child 2 👧
Parent 2 👨
Child 3 🧑‍🦱
Child 4 👶
  • 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

Drop target arguments

High level:

element (required)

element: Element;

The Element you want to attach the drop target to.

getData

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

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

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

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() returns true AND
  • The drop target getIsSticky() returns true 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 with getDropEffect(). The last data and dropEffect created when a drop target is actively being dragged over is preserved.

Nested drop targets

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 and A allow dropping, but B has blocked dropping

Flow:

  • C visited and allows dropping. Drop targets: [] -> [C]
  • B visited and does not allow dropping
  • A visited and allows dropping. Drop targets [C] -> [C, A]

Handling onDrop() for nested drop targets

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 parents 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();
    },
});

Was this page helpful?
We use this feedback to improve our documentation.