Repository: adobe/react-spectrum
File: packages/react-aria-components/src/DragAndDrop.tsx (TypeScript)

Code before:
      if (renderDropIndicator) {
        let keys = dragState?.draggingKeys ?? undefined;
        let draggedKey = dragState?.draggedKey ?? undefined;
        return renderDropIndicator(target, keys, draggedKey);
      } else {
        return <DropIndicator target={target} />;
      }
    }
    // We invalidate whenever the target changes.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [dropState?.target, isVirtualDragging, renderDropIndicator, dragState?.draggingKeys, dragState?.draggedKey]);
  return dragAndDropHooks?.useDropIndicator ? fn : undefined;
}

export function useDndPersistedKeys(selectionManager: MultipleSelectionManager, dragAndDropHooks?: DragAndDropHooks, dropState?: DroppableCollectionState): Set<Key> {
  // Persist the focused key and the drop target key.
  let focusedKey = selectionManager.focusedKey;
  let dropTargetKey: Key | null | undefined = null;
  if (dragAndDropHooks?.isVirtualDragging?.() && dropState?.target?.type === 'item') {
    dropTargetKey = dropState.target.key;
    if (dropState.target.dropPosition === 'after') {
      // Normalize to the "before" drop position since we only render those to the DOM.
      let nextKey = dropState.collection.getKeyAfter(dropTargetKey);
      if (nextKey != null) {
        let targetLevel = dropState.collection.getItem(dropTargetKey)?.level ?? 0;
        // Skip over any rows that are descendants of the target (can't drop an item into itself)
        while (nextKey) {
          let node = dropState.collection.getItem(nextKey);
          // eslint-disable-next-line max-depth
          if (!node) {
            break;
          }
          // Stop once we find a node at the same level or higher
          // eslint-disable-next-line max-depth
          if ((node.level ?? 0) <= targetLevel) {
            break;
          }
          nextKey = dropState.collection.getKeyAfter(nextKey);
        }
      }

      dropTargetKey = nextKey ?? dropTargetKey;
    }
  }

  return useMemo(() => {
    return new Set([focusedKey, dropTargetKey].filter(k => k != null));
  }, [focusedKey, dropTargetKey]);
}


Review comment (suggestion):
The logic seems right here, but I don't think the comment is. `dropTargetKey` is based on the current drop target, not the dragged item. So the problem isn't that you can't drop an item on itself, but that the "after" drop position occurs after all of the children of an item, not immediately after the item itself.

Code after:
      if (renderDropIndicator) {
        let keys = dragState?.draggingKeys ?? undefined;
        let draggedKey = dragState?.draggedKey ?? undefined;
        return renderDropIndicator(target, keys, draggedKey);
      } else {
        return <DropIndicator target={target} />;
      }
    }
    // We invalidate whenever the target changes.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [dropState?.target, isVirtualDragging, renderDropIndicator, dragState?.draggingKeys, dragState?.draggedKey]);
  return dragAndDropHooks?.useDropIndicator ? fn : undefined;
}

export function useDndPersistedKeys(selectionManager: MultipleSelectionManager, dragAndDropHooks?: DragAndDropHooks, dropState?: DroppableCollectionState): Set<Key> {
  // Persist the focused key and the drop target key.
  let focusedKey = selectionManager.focusedKey;
  let dropTargetKey: Key | null | undefined = null;
  if (dragAndDropHooks?.isVirtualDragging?.() && dropState?.target?.type === 'item') {
    dropTargetKey = dropState.target.key;
    if (dropState.target.dropPosition === 'after') {
      // Normalize to the "before" drop position since we only render those to the DOM.
      let nextKey = dropState.collection.getKeyAfter(dropTargetKey);
      if (nextKey != null) {
        let targetLevel = dropState.collection.getItem(dropTargetKey)?.level ?? 0;
        // Skip over any rows that are descendants of the target ("after" position should be after all children)
        while (nextKey) {
          let node = dropState.collection.getItem(nextKey);
          // eslint-disable-next-line max-depth
          if (!node) {
            break;
          }
          // Stop once we find a node at the same level or higher
          // eslint-disable-next-line max-depth
          if ((node.level ?? 0) <= targetLevel) {
            break;
          }
          nextKey = dropState.collection.getKeyAfter(nextKey);
        }
      }

      dropTargetKey = nextKey ?? dropTargetKey;
    }
  }

  return useMemo(() => {
    return new Set([focusedKey, dropTargetKey].filter(k => k != null));
  }, [focusedKey, dropTargetKey]);
}