Refactor tree drag item and drop target with selectors (#1253)

Before supporting selector in callbacks need to upport selectors in
data. Here internal drag item and drop target supported as selectors.

In the future these states will be exposed so for drop target added
custom representation without element to allow sending it via pubsub
between realms.
This commit is contained in:
Bogdan Chadkin authored and GitHub committed 2023-03-16 12:40:59 +03:00
1 parent 6f6a57c0e1
commit ad53dcdcf3
4 files changed
+88 -35

No files matched your search

@@ -1,7 +1,8 @@
import { describe, test, expect } from "@jest/globals";
import { renderHook, act } from "@testing-library/react-hooks";
import type { DropTarget, Placement } from "../primitives/dnd";
import type { Placement } from "../primitives/dnd";
import { useHorizontalShift } from "./horizontal-shift";
import type { ItemDropTarget, ItemId } from "./item-utils";
import {
canAcceptChild,
getItemChildren,
@@ -80,30 +81,35 @@ const makeDrop = ({
into: Item;
after?: Item;
placement?: Placement;
}): DropTarget<Item> => ({
}): ItemDropTarget<Item> => ({
itemSelector: getItemSelector(into.id),
data: into,
indexWithinChildren:
after === undefined ? 0 : into.children.indexOf(after) + 1,
placement,
// not used
element: null as unknown as HTMLElement,
rect: null as unknown as DOMRect,
});
const getItemSelector = (itemId: ItemId) => {
return getItemPath(tree, itemId)
.reverse()
.map((item) => item.id);
};
const render = (
{
dragItem,
dropTarget,
}: {
dragItem: Item | undefined;
dropTarget: DropTarget<Item> | undefined;
dropTarget: ItemDropTarget<Item> | undefined;
},
shift = -1
) => {
const { result } = renderHook((props) => useHorizontalShift<Item>(props), {
initialProps: {
dragItem,
dragItemSelector:
dragItem === undefined ? undefined : getItemSelector(dragItem.id),
dropTarget,
root: tree,
getIsExpanded: (item: Item) => item.children.length > 0,
@@ -1,5 +1,6 @@
import { useMemo, useState } from "react";
import type { DropTarget, Placement } from "../primitives/dnd";
import type { Placement } from "../primitives/dnd";
import type { ItemDropTarget, ItemSelector } from "./item-utils";
import { getPlacementIndicatorAlignment } from "./tree-node";
export type ShiftedDropTarget<Data> = {
@@ -9,7 +10,7 @@ export type ShiftedDropTarget<Data> = {
};
export const useHorizontalShift = <Data extends { id: string }>({
dragItem,
dragItemSelector,
dropTarget,
root,
getIsExpanded,
@@ -20,8 +21,8 @@ export const useHorizontalShift = <Data extends { id: string }>({
getItemChildren: (item: Data) => Data[];
canAcceptChild: (item: Data) => boolean;
getItemPath: (root: Data, id: string) => Data[];
dragItem: Data | undefined;
dropTarget: DropTarget<Data> | undefined;
dragItemSelector: undefined | ItemSelector;
dropTarget: ItemDropTarget<Data> | undefined;
root: Data;
getIsExpanded: (item: Data) => boolean;
}) => {
@@ -30,13 +31,18 @@ export const useHorizontalShift = <Data extends { id: string }>({
// Here we want to allow user to shift placement line horizontally
// but only if that corresponds to a meaningful position in the tree
const shiftedDropTarget = useMemo<ShiftedDropTarget<Data> | undefined>(() => {
if (dropTarget === undefined || dragItem === undefined) {
if (dropTarget === undefined || dragItemSelector === undefined) {
return undefined;
}
const dragItemDepth = getItemPath(root, dragItem.id).length - 1;
const dragItemDepth = dragItemSelector.length - 1;
const { data, placement, indexWithinChildren } = dropTarget;
const {
itemSelector: dropItemSelector,
data,
placement,
indexWithinChildren,
} = dropTarget;
const shiftPlacement = (depth: number) => {
const shift = getPlacementIndicatorAlignment(depth);
@@ -54,10 +60,7 @@ export const useHorizontalShift = <Data extends { id: string }>({
return { item: data, position: "end" };
}
const dropTargetPath = getItemPath(root, data.id);
dropTargetPath.reverse();
const currentDepth = dropTargetPath.length;
const currentDepth = dropItemSelector.length;
const desiredDepth = dragItemDepth + horizontalShift;
const withoutShift = {
@@ -66,7 +69,11 @@ export const useHorizontalShift = <Data extends { id: string }>({
placement: shiftPlacement(currentDepth),
} as const;
const isDragItem = (item: Data | undefined) => item?.id === dragItem.id;
const [dragItemId] = dragItemSelector;
const isDragItem = (item: Data | undefined) => item?.id === dragItemId;
const dropTargetPath = getItemPath(root, data.id);
dropTargetPath.reverse();
if (desiredDepth < currentDepth) {
let shifted = 0;
@@ -158,7 +165,7 @@ export const useHorizontalShift = <Data extends { id: string }>({
return withoutShift;
}, [
dropTarget,
dragItem,
dragItemSelector,
root,
horizontalShift,
getItemPath,
@@ -1,7 +1,17 @@
import type { Placement } from "../primitives/dnd";
export type ItemId = string;
export type ItemSelector = string[];
export type ItemDropTarget<Item> = {
itemSelector: ItemSelector;
data: Item;
rect: DOMRect;
indexWithinChildren: number;
placement: Placement;
};
export const getElementByItemSelector = (
root: undefined | Element,
itemSelector: ItemSelector
@@ -18,6 +28,21 @@ export const getElementByItemSelector = (
);
};
export const getItemSelectorFromElement = (element: Element) => {
const itemSelector: ItemSelector = [];
let matched: undefined | Element =
element.closest(`[data-drop-target-id]`) ?? undefined;
while (matched) {
const itemId = matched.getAttribute("data-drop-target-id") ?? undefined;
if (itemId !== undefined) {
itemSelector.push(itemId);
}
matched =
matched.parentElement?.closest(`[data-drop-target-id]`) ?? undefined;
}
return itemSelector;
};
export const areItemSelectorsEqual = (
left?: ItemSelector,
right?: ItemSelector
@@ -4,7 +4,6 @@ import { createPortal } from "react-dom";
import { ListPositionIndicator } from "../list-position-indicator";
import { TreeNode, INDENT, TreeItemRenderProps } from "./tree-node";
import {
type DropTarget,
useHold,
useDrop,
useDrag,
@@ -15,8 +14,10 @@ import { Box } from "../box";
import { useHorizontalShift } from "./horizontal-shift";
import {
type ItemSelector,
type ItemDropTarget,
areItemSelectorsEqual,
getElementByItemSelector,
getItemSelectorFromElement,
} from "./item-utils";
export type TreeProps<Data extends { id: string }> = {
@@ -61,8 +62,10 @@ export const Tree = <Data extends { id: string }>({
const rootRef = useRef<HTMLElement | null>(null);
const [dragItem, setDragItem] = useState<Data>();
const [dropTarget, setDropTarget] = useState<DropTarget<Data>>();
const [dragItemSelector, setDragItemSelector] = useState<
undefined | ItemSelector
>();
const [dropTarget, setDropTarget] = useState<ItemDropTarget<Data>>();
const getDropTargetElement = useCallback(
(id: string): HTMLElement | null | undefined =>
@@ -71,7 +74,7 @@ export const Tree = <Data extends { id: string }>({
);
const [shiftedDropTarget, setHorizontalShift] = useHorizontalShift({
dragItem,
dragItemSelector,
dropTarget,
root,
getIsExpanded,
@@ -88,8 +91,8 @@ export const Tree = <Data extends { id: string }>({
};
};
const useHoldHandler = useHold<DropTarget<Data>>({
isEqual: (a, b) => a.data.id === b.data.id,
const useHoldHandler = useHold<ItemDropTarget<Data>>({
isEqual: (a, b) => areItemSelectorsEqual(a.itemSelector, b.itemSelector),
holdTimeThreshold: 600,
onHold: (dropTarget) => {
if (
@@ -113,7 +116,7 @@ export const Tree = <Data extends { id: string }>({
},
swapDropTarget: (dropTarget) => {
if (dragItem === undefined || dropTarget === undefined) {
if (dragItemSelector === undefined || dropTarget === undefined) {
return getFallbackDropTarget();
}
@@ -129,8 +132,9 @@ export const Tree = <Data extends { id: string }>({
}
// Don't allow to drop inside drag item or any of its children
const [dragItemId] = dragItemSelector;
const dragItemIndex = path.findIndex(
(instance) => instance.id === dragItem.id
(instance) => instance.id === dragItemId
);
if (dragItemIndex !== -1) {
path.splice(0, dragItemIndex + 1);
@@ -152,8 +156,15 @@ export const Tree = <Data extends { id: string }>({
},
onDropTargetChange: (dropTarget) => {
useHoldHandler.setData(dropTarget);
setDropTarget(dropTarget);
const itemDropTarget = {
itemSelector: getItemSelectorFromElement(dropTarget.element),
data: dropTarget.data,
rect: dropTarget.rect,
indexWithinChildren: dropTarget.indexWithinChildren,
placement: dropTarget.placement,
};
useHoldHandler.setData(itemDropTarget);
setDropTarget(itemDropTarget);
},
getValidChildren: (element) => {
@@ -195,7 +206,11 @@ export const Tree = <Data extends { id: string }>({
},
onStart: ({ data }) => {
onSelect?.(data.id);
setDragItem(data);
setDragItemSelector(
getItemPath(root, data.id)
.reverse()
.map((item) => item.id)
);
dropHandlers.handleStart();
autoScrollHandlers.setEnabled(true);
},
@@ -207,9 +222,9 @@ export const Tree = <Data extends { id: string }>({
setHorizontalShift(shifts);
},
onEnd: ({ isCanceled }) => {
if (shiftedDropTarget && dragItem && isCanceled === false) {
if (shiftedDropTarget && dragItemSelector && isCanceled === false) {
onDragEnd({
itemId: dragItem.id,
itemId: dragItemSelector[0],
dropTarget: {
itemId: shiftedDropTarget.item.id,
position: shiftedDropTarget.position,
@@ -219,7 +234,7 @@ export const Tree = <Data extends { id: string }>({
autoScrollHandlers.setEnabled(false);
setHorizontalShift(0);
setDragItem(undefined);
setDragItemSelector(undefined);
setDropTarget(undefined);
dropHandlers.handleEnd({ isCanceled });
useHoldHandler.reset();
@@ -228,7 +243,7 @@ export const Tree = <Data extends { id: string }>({
const autoScrollHandlers = useAutoScroll();
useDragCursor(dragItem !== undefined);
useDragCursor(dragItemSelector !== undefined);
const keyboardNavigation = useKeyboardNavigation({
root,