diff --git a/packages/design-system/src/components/tree/horizontal-shift.test.ts b/packages/design-system/src/components/tree/horizontal-shift.test.ts index 9582288646..31ac702838 100644 --- a/packages/design-system/src/components/tree/horizontal-shift.test.ts +++ b/packages/design-system/src/components/tree/horizontal-shift.test.ts @@ -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 => ({ +}): ItemDropTarget => ({ + 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 | undefined; + dropTarget: ItemDropTarget | undefined; }, shift = -1 ) => { const { result } = renderHook((props) => useHorizontalShift(props), { initialProps: { - dragItem, + dragItemSelector: + dragItem === undefined ? undefined : getItemSelector(dragItem.id), dropTarget, root: tree, getIsExpanded: (item: Item) => item.children.length > 0, diff --git a/packages/design-system/src/components/tree/horizontal-shift.ts b/packages/design-system/src/components/tree/horizontal-shift.ts index 09a60c093f..bfe8e1648b 100644 --- a/packages/design-system/src/components/tree/horizontal-shift.ts +++ b/packages/design-system/src/components/tree/horizontal-shift.ts @@ -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 = { @@ -9,7 +10,7 @@ export type ShiftedDropTarget = { }; export const useHorizontalShift = ({ - dragItem, + dragItemSelector, dropTarget, root, getIsExpanded, @@ -20,8 +21,8 @@ export const useHorizontalShift = ({ getItemChildren: (item: Data) => Data[]; canAcceptChild: (item: Data) => boolean; getItemPath: (root: Data, id: string) => Data[]; - dragItem: Data | undefined; - dropTarget: DropTarget | undefined; + dragItemSelector: undefined | ItemSelector; + dropTarget: ItemDropTarget | undefined; root: Data; getIsExpanded: (item: Data) => boolean; }) => { @@ -30,13 +31,18 @@ export const useHorizontalShift = ({ // 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 | 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 = ({ 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 = ({ 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 = ({ return withoutShift; }, [ dropTarget, - dragItem, + dragItemSelector, root, horizontalShift, getItemPath, diff --git a/packages/design-system/src/components/tree/item-utils.ts b/packages/design-system/src/components/tree/item-utils.ts index 95dbc6d656..219e7772f2 100644 --- a/packages/design-system/src/components/tree/item-utils.ts +++ b/packages/design-system/src/components/tree/item-utils.ts @@ -1,7 +1,17 @@ +import type { Placement } from "../primitives/dnd"; + export type ItemId = string; export type ItemSelector = string[]; +export type ItemDropTarget = { + 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 diff --git a/packages/design-system/src/components/tree/tree.tsx b/packages/design-system/src/components/tree/tree.tsx index 84a8d6f6a2..3a1d010768 100644 --- a/packages/design-system/src/components/tree/tree.tsx +++ b/packages/design-system/src/components/tree/tree.tsx @@ -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 = { @@ -61,8 +62,10 @@ export const Tree = ({ const rootRef = useRef(null); - const [dragItem, setDragItem] = useState(); - const [dropTarget, setDropTarget] = useState>(); + const [dragItemSelector, setDragItemSelector] = useState< + undefined | ItemSelector + >(); + const [dropTarget, setDropTarget] = useState>(); const getDropTargetElement = useCallback( (id: string): HTMLElement | null | undefined => @@ -71,7 +74,7 @@ export const Tree = ({ ); const [shiftedDropTarget, setHorizontalShift] = useHorizontalShift({ - dragItem, + dragItemSelector, dropTarget, root, getIsExpanded, @@ -88,8 +91,8 @@ export const Tree = ({ }; }; - const useHoldHandler = useHold>({ - isEqual: (a, b) => a.data.id === b.data.id, + const useHoldHandler = useHold>({ + isEqual: (a, b) => areItemSelectorsEqual(a.itemSelector, b.itemSelector), holdTimeThreshold: 600, onHold: (dropTarget) => { if ( @@ -113,7 +116,7 @@ export const Tree = ({ }, swapDropTarget: (dropTarget) => { - if (dragItem === undefined || dropTarget === undefined) { + if (dragItemSelector === undefined || dropTarget === undefined) { return getFallbackDropTarget(); } @@ -129,8 +132,9 @@ export const Tree = ({ } // 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 = ({ }, 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 = ({ }, 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 = ({ 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 = ({ autoScrollHandlers.setEnabled(false); setHorizontalShift(0); - setDragItem(undefined); + setDragItemSelector(undefined); setDropTarget(undefined); dropHandlers.handleEnd({ isCanceled }); useHoldHandler.reset(); @@ -228,7 +243,7 @@ export const Tree = ({ const autoScrollHandlers = useAutoScroll(); - useDragCursor(dragItem !== undefined); + useDragCursor(dragItemSelector !== undefined); const keyboardNavigation = useKeyboardNavigation({ root,