diff --git a/packages/design-system/src/components/primitives/dnd/dom-utils.ts b/packages/design-system/src/components/primitives/dnd/dom-utils.ts index 3b14d30feb..b24f0dfb12 100644 --- a/packages/design-system/src/components/primitives/dnd/dom-utils.ts +++ b/packages/design-system/src/components/primitives/dnd/dom-utils.ts @@ -6,6 +6,10 @@ import { export type UsableElement = HTMLElement | SVGElement; +// Many DOM APIs produce values of type Element, +// which lacks certain methods that we often need. +// It's also usually safe to assume that the value is in fact HTMLElement or SVGElement. +// We use this helper to convince TypeScript that the value is a UsableElement. export const toUseableElement = ( element: EventTarget | Element | undefined | null ): UsableElement | undefined => { @@ -36,9 +40,10 @@ export const getLocalChildrenOrientation = ( const probeRect = probe.getBoundingClientRect(); parent.removeChild(probe); - return probeRect.width === 0 && probeRect.height !== 0 - ? "horizontal" - : "vertical"; + return { + type: probeRect.width === 0 ? "horizontal" : "vertical", + reverse: false, + }; } return getRectsOrientation(previous, current, next); diff --git a/packages/design-system/src/components/primitives/dnd/geometry-utils.test.ts b/packages/design-system/src/components/primitives/dnd/geometry-utils.test.ts index fb684ff796..5a963473a1 100644 --- a/packages/design-system/src/components/primitives/dnd/geometry-utils.test.ts +++ b/packages/design-system/src/components/primitives/dnd/geometry-utils.test.ts @@ -21,8 +21,7 @@ describe("getPlacementBetween", () => { ], [ - // [a] - // ----------- + // [a] // [b] { top: 0, left: 600 }, { top: 200, left: 0 }, @@ -30,10 +29,9 @@ describe("getPlacementBetween", () => { ], [ - // [a] | - // | - // | - // | [b] + // [a] + // + // [b] { top: 0, left: 0 }, { top: 600, left: 200 }, undefined, diff --git a/packages/design-system/src/components/primitives/dnd/geometry-utils.ts b/packages/design-system/src/components/primitives/dnd/geometry-utils.ts index b3b8089531..d96221804d 100644 --- a/packages/design-system/src/components/primitives/dnd/geometry-utils.ts +++ b/packages/design-system/src/components/primitives/dnd/geometry-utils.ts @@ -1,6 +1,8 @@ export type Rect = Pick; -export type ChildrenOrientation = "vertical" | "horizontal" | "mixed"; +export type ChildrenOrientation = + | { type: "horizontal" | "vertical"; reverse: boolean } + | { type: "mixed"; reverse?: boolean }; export type Placement = { x: number; @@ -75,7 +77,7 @@ export const getPlacementBetween = ( return undefined; } - if (distanceX < 0 || (distanceY >= 0 && distanceY < distanceX)) { + if (distanceX < 0) { const minX = Math.min(a.left, b.left); const maxX = Math.max(a.left + a.width, b.left + b.width); return { @@ -99,12 +101,26 @@ export const getPlacementBetween = ( export const getPlacementNextTo = ( parentRect: Rect, rect: Rect | undefined, - side: "top" | "bottom" | "left" | "right" + childrenOrientation: ChildrenOrientation, + direction: "forward" | "backward" ): Placement | undefined => { if (rect === undefined) { return undefined; } + const isForward = childrenOrientation.reverse + ? direction === "backward" + : direction === "forward"; + + const side = + childrenOrientation.type === "horizontal" + ? isForward + ? "right" + : "left" + : isForward + ? "bottom" + : "top"; + const margin = 5; if (side === "top") { @@ -154,7 +170,7 @@ export const getPlacementInside = ( ): Placement => { const padding = 5; - if (childrenOrientation === "horizontal") { + if (childrenOrientation.type === "horizontal") { const safePadding = Math.min(parentRect.width / 2, padding); return { y: parentRect.top + safePadding, @@ -178,12 +194,12 @@ const getSegmentsOrder = ( aEnd: number, bStart: number, bEnd: number -): "a-after-b" | "b-after-a" | "overlap" => { +): "b-first" | "a-first" | "overlap" => { if (aStart >= bEnd) { - return "a-after-b"; + return "b-first"; } if (bStart >= aEnd) { - return "b-after-a"; + return "a-first"; } return "overlap"; }; @@ -205,12 +221,13 @@ const getTwoRectsOrientation = ( second.top + second.height ); if (xOrder !== "overlap" && yOrder === "overlap") { - return "horizontal"; + return { type: "horizontal", reverse: xOrder === "b-first" }; } if (xOrder === "overlap" && yOrder !== "overlap") { - return "vertical"; + return { type: "vertical", reverse: yOrder === "b-first" }; } - return "mixed"; + + return { type: "mixed" }; }; export const getRectsOrientation = ( @@ -218,25 +235,29 @@ export const getRectsOrientation = ( second: Rect, third: Rect | undefined ): ChildrenOrientation => { - // @todo: add "vertical-reversed", "horizontal-reversed", "mixed-reversed" - const orientations = [ first && getTwoRectsOrientation(first, second), third && getTwoRectsOrientation(second, third), ]; - const includesVertical = orientations.includes("vertical"); - const includesHorizontal = orientations.includes("horizontal"); + // @todo: Maybe we should check that at least one is reversed. + // Need to test what works better, but keep an eye on false positives. + const allReverse = orientations.every((x) => !x || x.reverse !== false); + + const types = orientations.map((x) => x && x.type); + + const includesVertical = types.includes("vertical"); + const includesHorizontal = types.includes("horizontal"); if (includesVertical && !includesHorizontal) { - return "vertical"; + return { type: "vertical", reverse: allReverse }; } if (includesHorizontal && !includesVertical) { - return "horizontal"; + return { type: "horizontal", reverse: allReverse }; } - return "mixed"; + return { type: "mixed" }; }; // Determines whether we should place the item before or after the closest child. @@ -244,7 +265,7 @@ export const getRectsOrientation = ( export const getIndexAdjustment = ( pointer: { x: number; y: number }, closestChildRect: Rect | undefined, - childrenOrientation: ChildrenOrientation + { type: orientationType, reverse }: ChildrenOrientation ) => { if (closestChildRect === undefined) { return 0; @@ -252,14 +273,14 @@ export const getIndexAdjustment = ( const { top, left, width, height } = closestChildRect; - if (childrenOrientation === "vertical") { + if (orientationType === "vertical") { const middleY = top + height / 2; - return pointer.y < middleY ? 0 : 1; + return pointer.y < middleY ? (reverse ? 1 : 0) : reverse ? 0 : 1; } - if (childrenOrientation === "horizontal") { + if (orientationType === "horizontal") { const middleX = left + width / 2; - return pointer.x < middleX ? 0 : 1; + return pointer.x < middleX ? (reverse ? 1 : 0) : reverse ? 0 : 1; } // For the "mixed" orientation, diff --git a/packages/design-system/src/components/primitives/dnd/placement-indicator.tsx b/packages/design-system/src/components/primitives/dnd/placement-indicator.tsx index edcaf8db4c..be06d6805c 100644 --- a/packages/design-system/src/components/primitives/dnd/placement-indicator.tsx +++ b/packages/design-system/src/components/primitives/dnd/placement-indicator.tsx @@ -9,12 +9,20 @@ const placementStyle = { }; export const PlacementIndicator = ({ placement }: { placement: Placement }) => { - const style = { - top: placement.y - (placement.direction === "horizontal" ? 1 : 0), - left: placement.x - (placement.direction === "vertical" ? 1 : 0), - width: placement.direction === "horizontal" ? placement.length : 2, - height: placement.direction === "vertical" ? placement.length : 2, - }; + const style = + placement.direction === "horizontal" + ? { + top: placement.y - 1, + left: placement.x, + width: placement.length, + height: 2, + } + : { + top: placement.y, + left: placement.x - 1, + width: 2, + height: placement.length, + }; return ; }; diff --git a/packages/design-system/src/components/primitives/dnd/sortable-list.stories.tsx b/packages/design-system/src/components/primitives/dnd/sortable-list.stories.tsx index 333bbc4ccd..bfca46e3f6 100644 --- a/packages/design-system/src/components/primitives/dnd/sortable-list.stories.tsx +++ b/packages/design-system/src/components/primitives/dnd/sortable-list.stories.tsx @@ -12,7 +12,7 @@ type ItemData = { id: string; text: string }; const ListItem = styled("li", { display: "block", margin: 10, - background: "$mint12", + background: "$mint5", padding: 10, userSelect: "none", }); @@ -39,8 +39,10 @@ const Item = ({ export const SortableList = ({ direction = "vertical", + reversed = false, }: { direction?: "horizontal" | "vertical" | "wrap"; + reversed?: boolean; }) => { const [data, setData] = useState([ { id: "0", text: "First" }, @@ -132,11 +134,15 @@ export const SortableList = ({ @@ -170,10 +183,18 @@ export const SortableList = ({ ); }; -export const SortableListHorizontal = () => ( - +export const SortableListHorizontal = (args: Args) => ( + ); -export const SortableListWrap = () => ; +export const SortableListWrap = (args: Args) => ( + +); -export default {} as ComponentMeta; +type Args = { reversed: boolean }; + +export default { + args: { + reversed: false, + }, +} as ComponentMeta; diff --git a/packages/design-system/src/components/primitives/dnd/use-drop.ts b/packages/design-system/src/components/primitives/dnd/use-drop.ts index 3f7f68a992..4cb3f2ef27 100644 --- a/packages/design-system/src/components/primitives/dnd/use-drop.ts +++ b/packages/design-system/src/components/primitives/dnd/use-drop.ts @@ -134,15 +134,6 @@ export const useDrop = (props: UseDropProps): UseDropHandlers => { current.indexWithinChildren !== indexWithinChildren || !isEqualRect(current.rect, parentRect) ) { - const side = - childrenOrientation === "horizontal" - ? indexAdjustment > 0 - ? "right" - : "left" - : indexAdjustment > 0 - ? "bottom" - : "top"; - const toGlobalCoordinates = (placement: Placement | undefined) => placement && { ...placement, @@ -159,7 +150,12 @@ export const useDrop = (props: UseDropProps): UseDropHandlers => { getPlacementBetween(closestChildRect, neighbourRect) ) || toGlobalCoordinates( - getPlacementNextTo(parentRect, closestChildRect, side) + getPlacementNextTo( + parentRect, + closestChildRect, + childrenOrientation, + indexAdjustment > 0 ? "forward" : "backward" + ) ) || getPlacementInside(parentRect, childrenOrientation);