From ad53dcdcf375cb1041f0c61abfa9213693a1b075 Mon Sep 17 00:00:00 2001 From: Bogdan Chadkin Date: Thu, 16 Mar 2023 12:40:59 +0300 Subject: [PATCH] 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. --- .../components/tree/horizontal-shift.test.ts | 20 ++++++--- .../src/components/tree/horizontal-shift.ts | 33 ++++++++------ .../src/components/tree/item-utils.ts | 25 +++++++++++ .../src/components/tree/tree.tsx | 45 ++++++++++++------- 4 files changed, 88 insertions(+), 35 deletions(-) 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,