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,
});
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 - the Element the drop target will be attached to
  • getData() - data to associate with the drop target
  • canDrop() - whether the drop target can be dropped on
  • getDropEffect() - control the cursor when over a drop target
  • getIsSticky() - whether the drop target will hold onto selection after no longer being dragged over
  • onGenerateDragPreview
  • onDragStart
  • onDrag
  • onDropTargetChange
  • onDrop

Required arguments

  • element: Element you want to attach the drop target to

Optional arguments

  • getData?: (args: GetFeedbackArgs) => Record<string, unknown>: a function that returns data you want to attach to the drop target. 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. 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' }),
});
  • canDrop?: (args: GetFeedbackArgs) => boolean is used to conditionally block dropping. When looking for valid drop targets, @atlaskit/pragmatic-drag-and-drop starts at 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. canDrop() being called repeatedly allows you to change your mind about whether a drop target can be dropped on after it has 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. 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.
dropTargetForLinks({
  getDropEffect: () => 'link',
});

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: 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';
  },
});

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]

Stickiness is not maintained when an old drop target states it cannot be dropped on

Scenario: [A(sticky)][] + A:canDrop() returns false Result: []

Stickiness is not maintained when an old drop target states it it is no longer sticky

Scenario: [A(sticky)][] + A:getIsSticky() returns false Result: []

Stickiness is not maintained when an old drop target is unmounted

Scenario: [A(sticky)][] + A is unmounted Result: []

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.