From 9271035998b880b5bc9da5e6fa22e6df6e553199 Mon Sep 17 00:00:00 2001 From: Roman Pominov Date: Thu, 25 Aug 2022 10:23:53 +0300 Subject: [PATCH 1/7] more browser comp. solution for --- packages/design-system/src/components/text.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/design-system/src/components/text.tsx b/packages/design-system/src/components/text.tsx index 152019c8bc..d007036201 100644 --- a/packages/design-system/src/components/text.tsx +++ b/packages/design-system/src/components/text.tsx @@ -57,16 +57,17 @@ export const Text = styled("div", { true: { whiteSpace: "nowrap", textOverflow: "ellipsis", + overflow: "hidden", + + // To make sure text is not clipped vertically + pt: 10, + pb: 10, + mt: -10, + mb: -10, + flexBasis: 0, flexGrow: 1, - // We want to use overflow-x:clip to make sure the text is not clipped vertically. - // overflow:hidden is a fallback for when `clip` is not supported, - // the text may be clipped vertically in these browsers. - overflow: "hidden", - overflowX: "clip", - overflowY: "visible", - // For some reason flexBasis:0 is not enough // to stop it from growing past the container width: 0, From 11a9ffe59d93bae36990d4884c9612e187316794 Mon Sep 17 00:00:00 2001 From: Roman Pominov Date: Thu, 25 Aug 2022 10:28:05 +0300 Subject: [PATCH 2/7] remove a padding added by mistake --- packages/design-system/src/components/tree/tree.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/design-system/src/components/tree/tree.tsx b/packages/design-system/src/components/tree/tree.tsx index 71b0ad2a1c..bdb6e47256 100644 --- a/packages/design-system/src/components/tree/tree.tsx +++ b/packages/design-system/src/components/tree/tree.tsx @@ -246,8 +246,6 @@ export const Tree = ({ overflowX: "hidden", flexBasis: 0, flexGrow: 1, - pt: 2, - pb: 2, }} ref={(element) => { rootRef.current = element; From bc27de9f2c585a78fb6ad0545293aef44bfe8f05 Mon Sep 17 00:00:00 2001 From: Roman Pominov Date: Thu, 25 Aug 2022 10:47:51 +0300 Subject: [PATCH 3/7] use em to make sure it works with any font size --- packages/design-system/src/components/text.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/design-system/src/components/text.tsx b/packages/design-system/src/components/text.tsx index d007036201..065fdb33d3 100644 --- a/packages/design-system/src/components/text.tsx +++ b/packages/design-system/src/components/text.tsx @@ -60,10 +60,10 @@ export const Text = styled("div", { overflow: "hidden", // To make sure text is not clipped vertically - pt: 10, - pb: 10, - mt: -10, - mb: -10, + pt: "0.5em", + pb: "0.5em", + mt: "-0.5em", + mb: "-0.5em", flexBasis: 0, flexGrow: 1, From 6aada1505f24ddd09dbf0305cd6a592e41d7c553 Mon Sep 17 00:00:00 2001 From: Roman Pominov Date: Thu, 25 Aug 2022 12:15:34 +0300 Subject: [PATCH 4/7] add a readme --- .../src/components/primitives/dnd/README.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 packages/design-system/src/components/primitives/dnd/README.md diff --git a/packages/design-system/src/components/primitives/dnd/README.md b/packages/design-system/src/components/primitives/dnd/README.md new file mode 100644 index 0000000000..064fd1c7c4 --- /dev/null +++ b/packages/design-system/src/components/primitives/dnd/README.md @@ -0,0 +1,49 @@ +## Terminology + +- drop target - item accepting draggable element for drop +- drag item - item that has originally received a pointerdown event +- drop index - specific index at which item is going to drop relative to children +- drop target outline - frame that outlines a drop target +- placement indicator - a line that shows where the item is going to be placed +- edge detection - when pointer is near (~5px) an edge of a drop target + +## Principles + +- You can drop anywhere. At any point in time, there is a drop target and an index that would define the best possible position based on the current pointer position. There will never be an error state. If the best possible position is the original position, so be it. +- At any point in time, we can calculate the drop target outline, rect and placement indicator rect. + +## Architecture + +We provide two main React hooks: `useDrag` and `useDrop`, and secondary helpers: + +- `useAutoScroll` scrolls a scroll container when the pointer nears its edge, +- `useDragCursor` sets `cursor:grabbing` globally during the drag, +- `useHold` detects when a piece of data stays the same for a given amount of time. + +### `useDrag` + +This hook tracks a “drag” pointer interaction. It tells you: + +- when a drag starts, +- on which element, +- when pointer is moved during the drag, +- when the drag has ended. + +### `useDrop` + +This hook chooses the drop target. You give it the pointer coordinates received from `useDrag`, and it does its best to find the best position among potential drop targets. As a result, you get: + +- drop target element, +- coordinates of its bounding rect (drop target outline), +- index within children of the drop target (drop index), +- coordinates of the placement indicator line. + +This division of responsibilities between the two hooks allows to put them in different windows. For example, `useDrag` can be instantiated in the main browser window, while `useDrop` can live in an iframe. The information you’ll need to pass between them can be transferred via `postMessage()`. + +| Needs access to: | Drop targets DOM | Drag items DOM | Scroll container | +| ---------------- | ---------------- | -------------- | ---------------- | +| `useDrag` | | V | | +| `useDrop` | V | | V | +| `useAutoScroll` | | | V | +| `useHold` | | | | +| `useDragCursor` | | | | From 07c86260ab5cea6f99623976d4ae62c5799f4cab Mon Sep 17 00:00:00 2001 From: Roman Pominov Date: Thu, 25 Aug 2022 13:59:05 +0300 Subject: [PATCH 5/7] update test-cases --- apps/designer/docs/test-cases.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/apps/designer/docs/test-cases.md b/apps/designer/docs/test-cases.md index a4361e6d99..5accdaeeb5 100644 --- a/apps/designer/docs/test-cases.md +++ b/apps/designer/docs/test-cases.md @@ -6,13 +6,25 @@ 1. Create instances by drag&drop - Drag a component over an instance - check if its outline is correct - - Drag a box over a box, drop it - check if it was inserted in that box - - Drag a box and place it before a box - check if it was inserted before - - Drag a box and place it after a box - check if it was inserted after - - Select an instance, go to components, click a box - check if it was inserted at the end of the selected instance - - Drag a box exactly between 2 boxs - check if it was inserted between - - Drag a box to the bottom edge - check if parent box gets outlined and insertion happened after that dragged over box inside that outlined parent - - Drag a box to the top edge - check if parent box gets outlined and insertion happened before that dragged over box inside that outlined parent + - Drag a Box over a Box, drop it - check if it was inserted in that Box + - Drag a Box and place it before a Box - check if it was inserted before + - Drag a Box and place it after a Box - check if it was inserted after + - Drag a Box exactly between 2 Boxes - check if it was inserted between + - Drag a Box to the bottom edge - check if parent Box gets outlined and insertion happened after that dragged over Box inside that outlined parent + - Drag a Box to the top edge - check if parent Box gets outlined and insertion happened before that dragged over Box inside that outlined parent + - Create a Box with child Boxes, where children are aligned vertically — repeat the tests above with trying to insert inside the new Box + - Try to drop into a Paragraph - check that it's not allowed, and it drops next to the Paragraph instead + +1. Moving instances on canvas by drag&drop + + - Do the tests described in "Create instances by drag&drop", + but instead of moving a component from the panel to the canvas, + move a component that's already on the canvas + - Create a Paragraph, and make a word in it Bold + - drag & drop the Bold component - check that entire Paragraph have been moved + - Try to drop a Box inside itself - check that it's not allowed, and it drops next to it instead + - Create a Box that contains another Box, give the parent Box a padding + - drag the parent Box, and try to put it inside the child Box — check that it's not allowed, and it gets put next to the parent Box instead 1. Create instance by clicking on a component From 8a554fc1f093d898d05360326b6021d1d02b96a9 Mon Sep 17 00:00:00 2001 From: Roman Pominov Date: Thu, 25 Aug 2022 15:44:01 +0300 Subject: [PATCH 6/7] add some tests & minor fixes --- .../__snapshots__/geometry-utils.test.ts.snap | 451 ++++++++++++++++++ .../primitives/dnd/geometry-utils.test.ts | 211 +++++++- .../primitives/dnd/geometry-utils.ts | 2 +- .../src/components/primitives/dnd/index.ts | 12 +- .../primitives/dnd/use-auto-scroll.test.ts | 96 ++++ .../primitives/dnd/use-auto-scroll.ts | 6 +- .../src/components/primitives/dnd/use-drag.ts | 14 +- .../src/components/primitives/dnd/use-drop.ts | 90 ++-- .../src/components/primitives/dnd/use-hold.ts | 2 +- 9 files changed, 816 insertions(+), 68 deletions(-) create mode 100644 packages/design-system/src/components/primitives/dnd/__snapshots__/geometry-utils.test.ts.snap create mode 100644 packages/design-system/src/components/primitives/dnd/use-auto-scroll.test.ts diff --git a/packages/design-system/src/components/primitives/dnd/__snapshots__/geometry-utils.test.ts.snap b/packages/design-system/src/components/primitives/dnd/__snapshots__/geometry-utils.test.ts.snap new file mode 100644 index 0000000000..2cdb252ab3 --- /dev/null +++ b/packages/design-system/src/components/primitives/dnd/__snapshots__/geometry-utils.test.ts.snap @@ -0,0 +1,451 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getPlacementInside childrenOrientation={ type: 'horizontal', reverse: false } 1`] = ` +Object { + "direction": "vertical", + "length": 90, + "type": "inside-parent", + "x": 5, + "y": 5, +} +`; + +exports[`getPlacementInside childrenOrientation={ type: 'horizontal', reverse: true } 1`] = ` +Object { + "direction": "vertical", + "length": 90, + "type": "inside-parent", + "x": 5, + "y": 5, +} +`; + +exports[`getPlacementInside childrenOrientation={ type: 'mixed' } 1`] = ` +Object { + "direction": "horizontal", + "length": 90, + "type": "inside-parent", + "x": 5, + "y": 5, +} +`; + +exports[`getPlacementInside childrenOrientation={ type: 'vertical', reverse: false } 1`] = ` +Object { + "direction": "horizontal", + "length": 90, + "type": "inside-parent", + "x": 5, + "y": 5, +} +`; + +exports[`getPlacementInside childrenOrientation={ type: 'vertical', reverse: true } 1`] = ` +Object { + "direction": "horizontal", + "length": 90, + "type": "inside-parent", + "x": 5, + "y": 5, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'horizontal', reverse: false } direction=backward child almost same as parent 1`] = ` +Object { + "direction": "vertical", + "length": 292, + "type": "next-to-child", + "x": 2, + "y": 4, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'horizontal', reverse: false } direction=backward child bigger than parent 1`] = ` +Object { + "direction": "vertical", + "length": 310, + "type": "next-to-child", + "x": -10, + "y": -10, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'horizontal', reverse: false } direction=backward child same as parent 1`] = ` +Object { + "direction": "vertical", + "length": 300, + "type": "next-to-child", + "x": 0, + "y": 0, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'horizontal', reverse: false } direction=backward child smaller than parent 1`] = ` +Object { + "direction": "vertical", + "length": 100, + "type": "next-to-child", + "x": 95, + "y": 100, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'horizontal', reverse: false } direction=forward child almost same as parent 1`] = ` +Object { + "direction": "vertical", + "length": 292, + "type": "next-to-child", + "x": 298, + "y": 4, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'horizontal', reverse: false } direction=forward child bigger than parent 1`] = ` +Object { + "direction": "vertical", + "length": 310, + "type": "next-to-child", + "x": 300, + "y": -10, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'horizontal', reverse: false } direction=forward child same as parent 1`] = ` +Object { + "direction": "vertical", + "length": 300, + "type": "next-to-child", + "x": 300, + "y": 0, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'horizontal', reverse: false } direction=forward child smaller than parent 1`] = ` +Object { + "direction": "vertical", + "length": 100, + "type": "next-to-child", + "x": 205, + "y": 100, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'horizontal', reverse: true } direction=backward child almost same as parent 1`] = ` +Object { + "direction": "vertical", + "length": 292, + "type": "next-to-child", + "x": 298, + "y": 4, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'horizontal', reverse: true } direction=backward child bigger than parent 1`] = ` +Object { + "direction": "vertical", + "length": 310, + "type": "next-to-child", + "x": 300, + "y": -10, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'horizontal', reverse: true } direction=backward child same as parent 1`] = ` +Object { + "direction": "vertical", + "length": 300, + "type": "next-to-child", + "x": 300, + "y": 0, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'horizontal', reverse: true } direction=backward child smaller than parent 1`] = ` +Object { + "direction": "vertical", + "length": 100, + "type": "next-to-child", + "x": 205, + "y": 100, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'horizontal', reverse: true } direction=forward child almost same as parent 1`] = ` +Object { + "direction": "vertical", + "length": 292, + "type": "next-to-child", + "x": 2, + "y": 4, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'horizontal', reverse: true } direction=forward child bigger than parent 1`] = ` +Object { + "direction": "vertical", + "length": 310, + "type": "next-to-child", + "x": -10, + "y": -10, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'horizontal', reverse: true } direction=forward child same as parent 1`] = ` +Object { + "direction": "vertical", + "length": 300, + "type": "next-to-child", + "x": 0, + "y": 0, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'horizontal', reverse: true } direction=forward child smaller than parent 1`] = ` +Object { + "direction": "vertical", + "length": 100, + "type": "next-to-child", + "x": 95, + "y": 100, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'mixed' } direction=backward child almost same as parent 1`] = ` +Object { + "direction": "horizontal", + "length": 292, + "type": "next-to-child", + "x": 4, + "y": 2, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'mixed' } direction=backward child bigger than parent 1`] = ` +Object { + "direction": "horizontal", + "length": 310, + "type": "next-to-child", + "x": -10, + "y": -10, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'mixed' } direction=backward child same as parent 1`] = ` +Object { + "direction": "horizontal", + "length": 300, + "type": "next-to-child", + "x": 0, + "y": 0, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'mixed' } direction=backward child smaller than parent 1`] = ` +Object { + "direction": "horizontal", + "length": 100, + "type": "next-to-child", + "x": 100, + "y": 95, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'mixed' } direction=forward child almost same as parent 1`] = ` +Object { + "direction": "horizontal", + "length": 292, + "type": "next-to-child", + "x": 4, + "y": 298, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'mixed' } direction=forward child bigger than parent 1`] = ` +Object { + "direction": "horizontal", + "length": 310, + "type": "next-to-child", + "x": -10, + "y": 300, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'mixed' } direction=forward child same as parent 1`] = ` +Object { + "direction": "horizontal", + "length": 300, + "type": "next-to-child", + "x": 0, + "y": 300, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'mixed' } direction=forward child smaller than parent 1`] = ` +Object { + "direction": "horizontal", + "length": 100, + "type": "next-to-child", + "x": 100, + "y": 205, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'vertical', reverse: false } direction=backward child almost same as parent 1`] = ` +Object { + "direction": "horizontal", + "length": 292, + "type": "next-to-child", + "x": 4, + "y": 2, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'vertical', reverse: false } direction=backward child bigger than parent 1`] = ` +Object { + "direction": "horizontal", + "length": 310, + "type": "next-to-child", + "x": -10, + "y": -10, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'vertical', reverse: false } direction=backward child same as parent 1`] = ` +Object { + "direction": "horizontal", + "length": 300, + "type": "next-to-child", + "x": 0, + "y": 0, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'vertical', reverse: false } direction=backward child smaller than parent 1`] = ` +Object { + "direction": "horizontal", + "length": 100, + "type": "next-to-child", + "x": 100, + "y": 95, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'vertical', reverse: false } direction=forward child almost same as parent 1`] = ` +Object { + "direction": "horizontal", + "length": 292, + "type": "next-to-child", + "x": 4, + "y": 298, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'vertical', reverse: false } direction=forward child bigger than parent 1`] = ` +Object { + "direction": "horizontal", + "length": 310, + "type": "next-to-child", + "x": -10, + "y": 300, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'vertical', reverse: false } direction=forward child same as parent 1`] = ` +Object { + "direction": "horizontal", + "length": 300, + "type": "next-to-child", + "x": 0, + "y": 300, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'vertical', reverse: false } direction=forward child smaller than parent 1`] = ` +Object { + "direction": "horizontal", + "length": 100, + "type": "next-to-child", + "x": 100, + "y": 205, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'vertical', reverse: true } direction=backward child almost same as parent 1`] = ` +Object { + "direction": "horizontal", + "length": 292, + "type": "next-to-child", + "x": 4, + "y": 298, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'vertical', reverse: true } direction=backward child bigger than parent 1`] = ` +Object { + "direction": "horizontal", + "length": 310, + "type": "next-to-child", + "x": -10, + "y": 300, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'vertical', reverse: true } direction=backward child same as parent 1`] = ` +Object { + "direction": "horizontal", + "length": 300, + "type": "next-to-child", + "x": 0, + "y": 300, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'vertical', reverse: true } direction=backward child smaller than parent 1`] = ` +Object { + "direction": "horizontal", + "length": 100, + "type": "next-to-child", + "x": 100, + "y": 205, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'vertical', reverse: true } direction=forward child almost same as parent 1`] = ` +Object { + "direction": "horizontal", + "length": 292, + "type": "next-to-child", + "x": 4, + "y": 2, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'vertical', reverse: true } direction=forward child bigger than parent 1`] = ` +Object { + "direction": "horizontal", + "length": 310, + "type": "next-to-child", + "x": -10, + "y": -10, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'vertical', reverse: true } direction=forward child same as parent 1`] = ` +Object { + "direction": "horizontal", + "length": 300, + "type": "next-to-child", + "x": 0, + "y": 0, +} +`; + +exports[`getPlacementNextTo childrenOrientation={ type: 'vertical', reverse: true } direction=forward child smaller than parent 1`] = ` +Object { + "direction": "horizontal", + "length": 100, + "type": "next-to-child", + "x": 100, + "y": 95, +} +`; 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 11ceff2cae..c92f9fa873 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 @@ -1,8 +1,155 @@ -import { getPlacementBetween } from "./geometry-utils"; +import { + getPlacementBetween, + getDistanceToRect, + getArea, + getPlacementNextTo, + getPlacementInside, +} from "./geometry-utils"; -const size = { width: 100, height: 100 }; +describe("getDistanceToRect", () => { + const rect = { + top: 100, + left: 100, + width: 100, + height: 100, + }; + + test("inside", () => { + expect(getDistanceToRect(rect, { x: rect.left + 1, y: rect.top + 1 })).toBe( + 0 + ); + }); + + test("near top edge", () => { + expect(getDistanceToRect(rect, { x: rect.left + 1, y: rect.top - 2 })).toBe( + 2 + ); + }); + + test("near bottom edge", () => { + expect( + getDistanceToRect(rect, { + x: rect.left + 1, + y: rect.top + rect.height + 2, + }) + ).toBe(2); + }); + + test("near left edge", () => { + expect(getDistanceToRect(rect, { x: rect.left - 2, y: rect.top + 1 })).toBe( + 2 + ); + }); + + test("near right edge", () => { + expect( + getDistanceToRect(rect, { + x: rect.left + rect.width + 2, + y: rect.top + 1, + }) + ).toBe(2); + }); + + test("near top left corner", () => { + expect( + getDistanceToRect(rect, { + x: rect.left - 2, + y: rect.top - 2, + }) + ).toBe(Math.sqrt(8)); + }); + + test("near top right corner", () => { + expect( + getDistanceToRect(rect, { + x: rect.left + rect.width + 2, + y: rect.top - 2, + }) + ).toBe(Math.sqrt(8)); + }); + + test("near bottom left corner", () => { + expect( + getDistanceToRect(rect, { + x: rect.left - 2, + y: rect.top + rect.height + 2, + }) + ).toBe(Math.sqrt(8)); + }); + + test("near bottom right corner", () => { + expect( + getDistanceToRect(rect, { + x: rect.left + rect.width + 2, + y: rect.top + rect.height + 2, + }) + ).toBe(Math.sqrt(8)); + }); +}); + +describe("getArea", () => { + const rect = { + top: 100, + left: 100, + width: 100, + height: 100, + }; + const threshold = 10; + + test("top", () => { + expect( + getArea( + { x: rect.left + rect.width / 2, y: rect.top + 1 }, + threshold, + rect + ) + ).toBe("top"); + }); + + test("bottom", () => { + expect( + getArea( + { x: rect.left + rect.width / 2, y: rect.top + rect.height - 1 }, + threshold, + rect + ) + ).toBe("bottom"); + }); + + test("left", () => { + expect( + getArea( + { x: rect.left + 1, y: rect.top + rect.height / 2 }, + threshold, + rect + ) + ).toBe("left"); + }); + + test("right", () => { + expect( + getArea( + { x: rect.left + rect.width - 1, y: rect.top + rect.height / 2 }, + threshold, + rect + ) + ).toBe("right"); + }); + + test("center", () => { + expect( + getArea( + { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }, + threshold, + rect + ) + ).toBe("center"); + }); +}); describe("getPlacementBetween", () => { + const size = { width: 100, height: 100 }; + test.each([ [ // [a] | [b] @@ -67,3 +214,63 @@ describe("getPlacementBetween", () => { ); }); }); + +describe("getPlacementNextTo", () => { + describe.each([ + { type: "horizontal", reverse: false }, + { type: "horizontal", reverse: true }, + { type: "vertical", reverse: false }, + { type: "vertical", reverse: true }, + { type: "mixed" }, + ] as const)("childrenOrientation=%o", (childrenOrientation) => { + describe.each(["forward", "backward"] as const)( + "direction=%s", + (direction) => { + test.each([ + [ + "child smaller than parent", + { top: 100, left: 100, width: 100, height: 100 }, + ], + [ + "child almost same as parent", + { top: 4, left: 4, width: 292, height: 292 }, + ], + [ + "child same as parent", + { top: 0, left: 0, width: 300, height: 300 }, + ], + [ + "child bigger than parent", + { top: -10, left: -10, width: 310, height: 310 }, + ], + ])("%s", (_, child) => { + expect( + getPlacementNextTo( + { top: 0, left: 0, width: 300, height: 300 }, + child, + childrenOrientation, + direction + ) + ).toMatchSnapshot(); + }); + } + ); + }); +}); + +describe("getPlacementInside", () => { + test.each([ + { type: "horizontal", reverse: false }, + { type: "horizontal", reverse: true }, + { type: "vertical", reverse: false }, + { type: "vertical", reverse: true }, + { type: "mixed" }, + ] as const)("childrenOrientation=%o", (childrenOrientation) => { + expect( + getPlacementInside( + { top: 0, left: 0, width: 100, height: 100 }, + childrenOrientation + ) + ).toMatchSnapshot(); + }); +}); 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 6d8cfdab67..347a192131 100644 --- a/packages/design-system/src/components/primitives/dnd/geometry-utils.ts +++ b/packages/design-system/src/components/primitives/dnd/geometry-utils.ts @@ -22,7 +22,7 @@ export type Placement = { export type Area = "top" | "bottom" | "left" | "right" | "center"; // https://stackoverflow.com/a/18157551/478603 -const getDistanceToRect = (rect: Rect, { x, y }: Point) => { +export const getDistanceToRect = (rect: Rect, { x, y }: Point) => { const dx = Math.max(rect.left - x, 0, x - (rect.left + rect.width)); const dy = Math.max(rect.top - y, 0, y - (rect.top + rect.height)); return Math.sqrt(dx * dx + dy * dy); diff --git a/packages/design-system/src/components/primitives/dnd/index.ts b/packages/design-system/src/components/primitives/dnd/index.ts index 8cb8570565..b9f03e2785 100644 --- a/packages/design-system/src/components/primitives/dnd/index.ts +++ b/packages/design-system/src/components/primitives/dnd/index.ts @@ -1,7 +1,7 @@ -export * from "./use-drag"; -export * from "./use-drop"; -export * from "./use-auto-scroll"; -export * from "./placement-indicator"; -export * from "./use-hold"; -export * from "./use-drag-cursor"; +export { useDrag } from "./use-drag"; +export { useDrop, type DropTarget } from "./use-drop"; +export { useAutoScroll } from "./use-auto-scroll"; +export { PlacementIndicator } from "./placement-indicator"; +export { useHold } from "./use-hold"; +export { useDragCursor } from "./use-drag-cursor"; export { type Rect, type Point, type Placement } from "./geometry-utils"; diff --git a/packages/design-system/src/components/primitives/dnd/use-auto-scroll.test.ts b/packages/design-system/src/components/primitives/dnd/use-auto-scroll.test.ts new file mode 100644 index 0000000000..839f53ecec --- /dev/null +++ b/packages/design-system/src/components/primitives/dnd/use-auto-scroll.test.ts @@ -0,0 +1,96 @@ +import { getSpeed } from "./use-auto-scroll"; + +describe("getSpeed", () => { + const CONTAINER_START = 0; + const CONTAINER_END = 100; + const EDGE_DISTANCE_THRESHOLD = 10; + const MIN_SPEED = 0; + const MAX_SPEED = 10; + + test("speed is never over mod(maxSpeed)", () => { + expect( + getSpeed( + CONTAINER_END + 100, + CONTAINER_START, + CONTAINER_END, + EDGE_DISTANCE_THRESHOLD, + MIN_SPEED, + MAX_SPEED + ) + ).toBe(MAX_SPEED); + expect( + getSpeed( + CONTAINER_START - 100, + CONTAINER_START, + CONTAINER_END, + EDGE_DISTANCE_THRESHOLD, + MIN_SPEED, + MAX_SPEED + ) + ).toBe(-MAX_SPEED); + }); + + test("when at the middle of container, speed is minSpeed", () => { + expect( + getSpeed( + CONTAINER_START + (CONTAINER_END - CONTAINER_START) / 2, + CONTAINER_START, + CONTAINER_END, + EDGE_DISTANCE_THRESHOLD, + MIN_SPEED, + MAX_SPEED + ) + ).toBe(MIN_SPEED); + }); + + test("when within the edgeDistanceThreshold bounds, speed is minSpeed", () => { + expect( + getSpeed( + CONTAINER_END - EDGE_DISTANCE_THRESHOLD - 1, + CONTAINER_START, + CONTAINER_END, + EDGE_DISTANCE_THRESHOLD, + MIN_SPEED, + MAX_SPEED + ) + ).toBe(MIN_SPEED); + + expect( + getSpeed( + CONTAINER_START + EDGE_DISTANCE_THRESHOLD + 1, + CONTAINER_START, + CONTAINER_END, + EDGE_DISTANCE_THRESHOLD, + MIN_SPEED, + MAX_SPEED + ) + ).toBe(MIN_SPEED); + }); + + test.each([0.2, 0.3, 0.9])( + "speed is proportional to how close the pointer is to the edge (proportion=%d)", + (proportion) => { + expect( + getSpeed( + CONTAINER_END - EDGE_DISTANCE_THRESHOLD * (1 - proportion), + CONTAINER_START, + CONTAINER_END, + EDGE_DISTANCE_THRESHOLD, + MIN_SPEED, + MAX_SPEED + ) + ).toBe(MIN_SPEED + (MAX_SPEED - MIN_SPEED) * proportion); + + expect( + getSpeed( + CONTAINER_START + EDGE_DISTANCE_THRESHOLD * (1 - proportion), + CONTAINER_START, + CONTAINER_END, + EDGE_DISTANCE_THRESHOLD, + MIN_SPEED, + MAX_SPEED + ) + ).toBe((MIN_SPEED + (MAX_SPEED - MIN_SPEED) * proportion) * -1); + } + ); +}); diff --git a/packages/design-system/src/components/primitives/dnd/use-auto-scroll.ts b/packages/design-system/src/components/primitives/dnd/use-auto-scroll.ts index 1713cb02d3..ca543c1de3 100644 --- a/packages/design-system/src/components/primitives/dnd/use-auto-scroll.ts +++ b/packages/design-system/src/components/primitives/dnd/use-auto-scroll.ts @@ -4,7 +4,7 @@ import { type Point } from "./geometry-utils"; // Time between frames of scroll animation in milliseconds const FRAME_PERIOD = 30; -const getSpeed = ( +export const getSpeed = ( pointerPosition: number, containerStart: number, containerEnd: number, @@ -39,7 +39,7 @@ const getSpeed = ( return 0; }; -export type UseAutoScrollProps = { +type UseAutoScrollProps = { edgeDistanceThreshold?: number; // If set to true the entire document will be scrolled. @@ -51,7 +51,7 @@ export type UseAutoScrollProps = { maxSpeed?: number; }; -export type UseAutoScrollHandlers = { +type UseAutoScrollHandlers = { handleMove: (pointerCoordinate: Point) => void; setEnabled: (enabled: boolean) => void; targetRef: (element: HTMLElement | null) => void; diff --git a/packages/design-system/src/components/primitives/dnd/use-drag.ts b/packages/design-system/src/components/primitives/dnd/use-drag.ts index d15efdec28..e544e23e07 100644 --- a/packages/design-system/src/components/primitives/dnd/use-drag.ts +++ b/packages/design-system/src/components/primitives/dnd/use-drag.ts @@ -19,7 +19,7 @@ const initialState: State = { pointerId: undefined, }; -export type UseDragProps = { +type UseDragProps = { startDistanceThreashold?: number; shiftDistanceThreshold?: number; elementToData: (element: Element) => DragItemData | false; @@ -29,7 +29,7 @@ export type UseDragProps = { onEnd: (event: { isCanceled: boolean }) => void; }; -export type UseDragHandlers = { +type UseDragHandlers = { rootRef: (element: HTMLElement | null) => void; cancelCurrentDrag: () => void; }; @@ -129,9 +129,9 @@ export const useDrag = ( }; const end = (isCanceled: boolean) => { - // A drag is basically a very slow click. - // But we don't want it to register as a click. - if (isCanceled === false && state.current.status === "dragging") { + if (state.current.status === "dragging") { + // A drag is basically a very slow click. + // But we don't want it to register as a click. const addedAt = Date.now(); window.addEventListener( "click", @@ -145,12 +145,12 @@ export const useDrag = ( }, { capture: true, once: true } ); + + latestProps.current.onEnd({ isCanceled }); } state.current = { ...(initialState as State) }; - latestProps.current.onEnd({ isCanceled: isCanceled }); - window.removeEventListener("pointermove", handlePointerMove); window.removeEventListener("pointerup", handlePointerUp); window.removeEventListener("pointercancel", handlePointerCancel); 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 f0b6345e87..fa9745765f 100644 --- a/packages/design-system/src/components/primitives/dnd/use-drop.ts +++ b/packages/design-system/src/components/primitives/dnd/use-drop.ts @@ -16,7 +16,7 @@ import { getLocalChildrenOrientation, getChildrenRects } from "./dom-utils"; // Partial information about a drop target // used during the selection of a new drop target -export type PartialDropTarget = { +type PartialDropTarget = { data: Data; element: Element; }; @@ -30,7 +30,7 @@ export type DropTarget = PartialDropTarget & { // We pass around data, to avoid extra data lookups. // For example, data found in elementToData // doesn't have to be looked up again in swapDropTarget. -export type UseDropProps = { +type UseDropProps = { // To check that the element can qualify as a target elementToData: (target: Element) => Data | false; @@ -75,7 +75,7 @@ export type UseDropProps = { // the pointer always will be at least 2px away from any edge of the root. const PADDING_WHEN_EMULATING_POINTER_IN_BOUNDS = 2; -export type UseDropHandlers = { +type UseDropHandlers = { handleMove: (pointerCoordinates: Point) => void; handleScroll: () => void; handleStart: () => void; @@ -90,8 +90,9 @@ const getInitialState = () => { pointerCoordinates: undefined as Point | undefined, dropTarget: undefined as DropTarget | undefined, childrenRectsCache: new WeakMap(), - lastCandidateElement: undefined as Element | undefined, - lastCandidateArea: undefined as Area | undefined, + lastInitialCandidate: undefined as + | (PartialDropTarget & { area: Area }) + | undefined, }; }; @@ -219,40 +220,44 @@ export const useDrop = (props: UseDropProps): UseDropHandlers => { const { pointerCoordinates } = state.current; const root = rootRef.current; + const withArea = ( + candidate: PartialDropTarget | null | undefined + ) => { + if (candidate == null || pointerCoordinates === undefined) { + return undefined; + } + return { + ...candidate, + area: getArea( + pointerCoordinates, + edgeDistanceThreshold, + candidate.element.getBoundingClientRect() + ), + }; + }; + // @todo: Cache this? // Not expensive by itself, but it may call elementToData multiple times. - let candidate = + let candidate = withArea( root && - findClosestDropTarget({ - root, - initialElement: - pointerCoordinates && - document.elementFromPoint( - pointerCoordinates.x, - pointerCoordinates.y - ), - elementToData, - }); + findClosestDropTarget({ + root, + initialElement: + pointerCoordinates && + document.elementFromPoint( + pointerCoordinates.x, + pointerCoordinates.y + ), + elementToData, + }) + ); // To avoid calling swapDropTarget unnecessarily on every pointermove - const isNewCandidate = - candidate?.element !== state.current.lastCandidateElement; - state.current.lastCandidateElement = candidate?.element; - const candidateArea = - candidate && pointerCoordinates - ? getArea( - pointerCoordinates, - edgeDistanceThreshold, - candidate.element.getBoundingClientRect() - ) - : undefined; - const isNewArea = candidateArea !== state.current.lastCandidateArea; - state.current.lastCandidateArea = candidateArea; - if ( - isNewCandidate === false && - isNewArea === false && - state.current.dropTarget - ) { + const isNewInitialCandidate = + candidate?.element !== state.current.lastInitialCandidate?.element || + candidate?.area !== state.current.lastInitialCandidate?.area; + state.current.lastInitialCandidate = candidate; + if (isNewInitialCandidate === false && state.current.dropTarget) { // Still need to call setDropTarget to update rect and/or placement. // Because indexWithinChildren might have changed, // or parent coordinates might have moved in case of a scroll @@ -262,21 +267,10 @@ export const useDrop = (props: UseDropProps): UseDropHandlers => { let continueSwapping = true; while (continueSwapping || candidate == null) { - const swappedTo = swapDropTarget( - candidate && pointerCoordinates - ? { - ...candidate, - area: getArea( - pointerCoordinates, - edgeDistanceThreshold, - candidate.element.getBoundingClientRect() - ), - } - : undefined - ); + const swappedTo = swapDropTarget(candidate); continueSwapping = swappedTo.element !== candidate?.element && swappedTo.final !== true; - candidate = swappedTo; + candidate = withArea(swappedTo); } setDropTarget(candidate); @@ -324,7 +318,7 @@ export const useDrop = (props: UseDropProps): UseDropHandlers => { handleDomMutation() { state.current.childrenRectsCache = new WeakMap(); - state.current.lastCandidateElement = undefined; + state.current.lastInitialCandidate = undefined; detectTarget(); }, }; diff --git a/packages/design-system/src/components/primitives/dnd/use-hold.ts b/packages/design-system/src/components/primitives/dnd/use-hold.ts index e1e3acb530..5097baf4d2 100644 --- a/packages/design-system/src/components/primitives/dnd/use-hold.ts +++ b/packages/design-system/src/components/primitives/dnd/use-hold.ts @@ -1,6 +1,6 @@ import { useMemo, useRef } from "react"; -export type UseHoldProps = { +type UseHoldProps = { isEqual: (a: Data, b: Data) => boolean; holdTimeThreshold: number; onHold: (data: Data) => void; From 88f8d3991f19ca987fb5c721e58a599384907fc2 Mon Sep 17 00:00:00 2001 From: Roman Pominov Date: Thu, 25 Aug 2022 17:27:10 +0300 Subject: [PATCH 7/7] more tests --- apps/designer/docs/test-cases.md | 4 +- .../primitives/dnd/geometry-utils.test.ts | 261 ++++++++++++++++++ .../primitives/dnd/geometry-utils.ts | 2 +- 3 files changed, 264 insertions(+), 3 deletions(-) diff --git a/apps/designer/docs/test-cases.md b/apps/designer/docs/test-cases.md index 5accdaeeb5..8904eeca3c 100644 --- a/apps/designer/docs/test-cases.md +++ b/apps/designer/docs/test-cases.md @@ -12,7 +12,7 @@ - Drag a Box exactly between 2 Boxes - check if it was inserted between - Drag a Box to the bottom edge - check if parent Box gets outlined and insertion happened after that dragged over Box inside that outlined parent - Drag a Box to the top edge - check if parent Box gets outlined and insertion happened before that dragged over Box inside that outlined parent - - Create a Box with child Boxes, where children are aligned vertically — repeat the tests above with trying to insert inside the new Box + - Create a Box with child Boxes, where children are aligned vertically - repeat the tests above with trying to insert inside the new Box - Try to drop into a Paragraph - check that it's not allowed, and it drops next to the Paragraph instead 1. Moving instances on canvas by drag&drop @@ -24,7 +24,7 @@ - drag & drop the Bold component - check that entire Paragraph have been moved - Try to drop a Box inside itself - check that it's not allowed, and it drops next to it instead - Create a Box that contains another Box, give the parent Box a padding - - drag the parent Box, and try to put it inside the child Box — check that it's not allowed, and it gets put next to the parent Box instead + - drag the parent Box, and try to put it inside the child Box - check that it's not allowed, and it drops next to the parent Box instead 1. Create instance by clicking on a component 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 c92f9fa873..5f3c6fd2c7 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 @@ -4,6 +4,9 @@ import { getArea, getPlacementNextTo, getPlacementInside, + getTwoRectsOrientation, + getRectsOrientation, + getIndexAdjustment, } from "./geometry-utils"; describe("getDistanceToRect", () => { @@ -274,3 +277,261 @@ describe("getPlacementInside", () => { ).toMatchSnapshot(); }); }); + +describe("getTwoRectsOrientation", () => { + test.each([ + [ + "horizontal", + { top: 0, left: 0, width: 100, height: 100 }, + { top: 0, left: 100, width: 100, height: 100 }, + ], + [ + "horizontal", + { top: 10, left: 0, width: 80, height: 80 }, + { top: 0, left: 100, width: 100, height: 100 }, + ], + [ + "vertical", + { top: 0, left: 0, width: 100, height: 100 }, + { top: 100, left: 0, width: 100, height: 100 }, + ], + [ + "vertical", + { top: 0, left: 10, width: 80, height: 80 }, + { top: 100, left: 0, width: 100, height: 100 }, + ], + ])("%s %o %o", (type, rectA, rectB) => { + expect(getTwoRectsOrientation(rectA, rectB)).toEqual({ + reverse: false, + type, + }); + expect(getTwoRectsOrientation(rectB, rectA)).toEqual({ + reverse: true, + type, + }); + }); + + test.each([ + [ + { top: 0, left: 0, width: 100, height: 100 }, + { top: 0, left: 0, width: 100, height: 100 }, + ], + [ + { top: 10, left: 10, width: 80, height: 80 }, + { top: 0, left: 0, width: 100, height: 100 }, + ], + [ + { top: 0, left: 0, width: 100, height: 100 }, + { top: 50, left: 50, width: 100, height: 100 }, + ], + [ + { top: 0, left: 0, width: 100, height: 100 }, + { top: 100, left: 100, width: 100, height: 100 }, + ], + ])("mixed %o %o", (rectA, rectB) => { + expect(getTwoRectsOrientation(rectA, rectB)).toEqual({ + type: "mixed", + }); + expect(getTwoRectsOrientation(rectB, rectA)).toEqual({ + type: "mixed", + }); + }); +}); + +describe("getRectsOrientation", () => { + const vertical = [ + { top: 0, left: 100, width: 100, height: 100 }, + { top: 100, left: 100, width: 100, height: 100 }, + { top: 200, left: 100, width: 100, height: 100 }, + ] as const; + + const horizontal = [ + { top: 100, left: 0, width: 100, height: 100 }, + { top: 100, left: 100, width: 100, height: 100 }, + { top: 100, left: 200, width: 100, height: 100 }, + ] as const; + + test("if all reversed, result is reversed", () => { + expect( + getRectsOrientation(vertical[2], vertical[1], vertical[0]).reverse + ).toBe(true); + }); + + test("if only one reversed, result is not reversed", () => { + expect( + getRectsOrientation(vertical[0], vertical[1], vertical[0]).reverse + ).toBe(false); + }); + + test("if all vertical result is vertical", () => { + expect( + getRectsOrientation(vertical[0], vertical[1], vertical[2]).type + ).toBe("vertical"); + expect(getRectsOrientation(undefined, vertical[1], vertical[2]).type).toBe( + "vertical" + ); + expect(getRectsOrientation(vertical[0], vertical[1], undefined).type).toBe( + "vertical" + ); + }); + + test("if all horizontal result is horizontal", () => { + expect( + getRectsOrientation(horizontal[0], horizontal[1], horizontal[2]).type + ).toBe("horizontal"); + expect( + getRectsOrientation(undefined, horizontal[1], horizontal[2]).type + ).toBe("horizontal"); + expect( + getRectsOrientation(horizontal[0], horizontal[1], undefined).type + ).toBe("horizontal"); + }); + + test("if mixed result is mixed", () => { + expect( + getRectsOrientation(horizontal[0], vertical[1], vertical[2]).type + ).toBe("mixed"); + }); +}); + +describe("getIndexAdjustment", () => { + const rect = { top: 100, left: 100, width: 200, height: 200 }; + + describe.each([true, false])("orientation.reverse=%s", (reverse) => { + describe("orientation.type=vertical", () => { + test("above", () => { + expect( + getIndexAdjustment( + { x: rect.left + rect.width / 2, y: rect.top - 10 }, + rect, + { type: "vertical", reverse } + ) + ).toBe(reverse ? 1 : 0); + }); + test("below", () => { + expect( + getIndexAdjustment( + { + x: rect.left + rect.width / 2, + y: rect.top + rect.width + 10, + }, + rect, + { type: "vertical", reverse } + ) + ).toBe(reverse ? 0 : 1); + }); + test("inside, above middle", () => { + expect( + getIndexAdjustment( + { + x: rect.left + rect.width / 2, + y: rect.top + rect.height / 3, + }, + rect, + { type: "vertical", reverse } + ) + ).toBe(reverse ? 1 : 0); + }); + test("inside, below middle", () => { + expect( + getIndexAdjustment( + { + x: rect.left + rect.width / 2, + y: rect.top + rect.height * (2 / 3), + }, + rect, + { type: "vertical", reverse } + ) + ).toBe(reverse ? 0 : 1); + }); + }); + describe("orientation.type=horizontal", () => { + test("to the left", () => { + expect( + getIndexAdjustment( + { x: rect.left - 10, y: rect.top + rect.height / 2 }, + rect, + { type: "horizontal", reverse } + ) + ).toBe(reverse ? 1 : 0); + }); + test("to the right", () => { + expect( + getIndexAdjustment( + { + x: rect.left + rect.width + 10, + y: rect.top + rect.height / 2, + }, + rect, + { type: "horizontal", reverse } + ) + ).toBe(reverse ? 0 : 1); + }); + test("inside, to the left of middle", () => { + expect( + getIndexAdjustment( + { + x: rect.left + rect.width / 3, + y: rect.top + rect.height / 2, + }, + rect, + { type: "horizontal", reverse } + ) + ).toBe(reverse ? 1 : 0); + }); + test("inside, to the right of middle", () => { + expect( + getIndexAdjustment( + { + x: rect.left + rect.width * (2 / 3), + y: rect.top + rect.height / 2, + }, + rect, + { type: "horizontal", reverse } + ) + ).toBe(reverse ? 0 : 1); + }); + }); + }); + describe("orientation.type=mixed", () => { + test.each([ + ["above", rect.left + rect.width / 2, rect.top - 10, 0], + ["below", rect.left + rect.width / 2, rect.top + rect.height + 10, 1], + ["to the left", rect.left - 10, rect.top + rect.height / 2, 0], + [ + "to the right", + rect.left + rect.width + 10, + rect.top + rect.height / 2, + 1, + ], + [ + "inside, above middle", + rect.left + rect.width / 2, + rect.top + rect.height / 3, + 0, + ], + [ + "inside, below middle", + rect.left + rect.width / 2, + rect.top + rect.height * (2 / 3), + 1, + ], + [ + "inside, to the left of middle", + rect.left + rect.width / 3, + rect.top + rect.height / 2, + 0, + ], + [ + "inside, to the right of middle", + rect.left + rect.width * (2 / 3), + rect.top + rect.height / 2, + 1, + ], + ])("%s", (_, x, y, expected) => { + expect(getIndexAdjustment({ x, y }, rect, { type: "mixed" })).toBe( + expected + ); + }); + }); +}); 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 347a192131..65497d1394 100644 --- a/packages/design-system/src/components/primitives/dnd/geometry-utils.ts +++ b/packages/design-system/src/components/primitives/dnd/geometry-utils.ts @@ -228,7 +228,7 @@ const getSegmentsOrder = ( return "overlap"; }; -const getTwoRectsOrientation = ( +export const getTwoRectsOrientation = ( first: Rect, second: Rect ): ChildrenOrientation => {