mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-08 04:55:06 +09:00
Merge pull request #243 from webstudio-is/dnd-improvements
Drag&drop improvements & some minor fixes
This commit is contained in:
13 files changed
+1154
-85
No files matched your search
@@ -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 drops next to the parent Box instead
|
||||
|
||||
1. Create instance by clicking on a component
|
||||
|
||||
|
||||
@@ -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` | | | |
|
||||
+451
@@ -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,
|
||||
}
|
||||
`;
|
||||
@@ -1,8 +1,158 @@
|
||||
import { getPlacementBetween } from "./geometry-utils";
|
||||
import {
|
||||
getPlacementBetween,
|
||||
getDistanceToRect,
|
||||
getArea,
|
||||
getPlacementNextTo,
|
||||
getPlacementInside,
|
||||
getTwoRectsOrientation,
|
||||
getRectsOrientation,
|
||||
getIndexAdjustment,
|
||||
} 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 +217,321 @@ 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();
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -228,7 +228,7 @@ const getSegmentsOrder = (
|
||||
return "overlap";
|
||||
};
|
||||
|
||||
const getTwoRectsOrientation = (
|
||||
export const getTwoRectsOrientation = (
|
||||
first: Rect,
|
||||
second: Rect
|
||||
): ChildrenOrientation => {
|
||||
|
||||
@@ -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";
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -19,7 +19,7 @@ const initialState: State<unknown> = {
|
||||
pointerId: undefined,
|
||||
};
|
||||
|
||||
export type UseDragProps<DragItemData> = {
|
||||
type UseDragProps<DragItemData> = {
|
||||
startDistanceThreashold?: number;
|
||||
shiftDistanceThreshold?: number;
|
||||
elementToData: (element: Element) => DragItemData | false;
|
||||
@@ -29,7 +29,7 @@ export type UseDragProps<DragItemData> = {
|
||||
onEnd: (event: { isCanceled: boolean }) => void;
|
||||
};
|
||||
|
||||
export type UseDragHandlers = {
|
||||
type UseDragHandlers = {
|
||||
rootRef: (element: HTMLElement | null) => void;
|
||||
cancelCurrentDrag: () => void;
|
||||
};
|
||||
@@ -129,9 +129,9 @@ export const useDrag = <DragItemData>(
|
||||
};
|
||||
|
||||
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 = <DragItemData>(
|
||||
},
|
||||
{ capture: true, once: true }
|
||||
);
|
||||
|
||||
latestProps.current.onEnd({ isCanceled });
|
||||
}
|
||||
|
||||
state.current = { ...(initialState as State<DragItemData>) };
|
||||
|
||||
latestProps.current.onEnd({ isCanceled: isCanceled });
|
||||
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", handlePointerUp);
|
||||
window.removeEventListener("pointercancel", handlePointerCancel);
|
||||
|
||||
@@ -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<Data> = {
|
||||
type PartialDropTarget<Data> = {
|
||||
data: Data;
|
||||
element: Element;
|
||||
};
|
||||
@@ -30,7 +30,7 @@ export type DropTarget<Data> = PartialDropTarget<Data> & {
|
||||
// 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<Data> = {
|
||||
type UseDropProps<Data> = {
|
||||
// To check that the element can qualify as a target
|
||||
elementToData: (target: Element) => Data | false;
|
||||
|
||||
@@ -75,7 +75,7 @@ export type UseDropProps<Data> = {
|
||||
// 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 = <Data>() => {
|
||||
pointerCoordinates: undefined as Point | undefined,
|
||||
dropTarget: undefined as DropTarget<Data> | undefined,
|
||||
childrenRectsCache: new WeakMap<Element, Rect[]>(),
|
||||
lastCandidateElement: undefined as Element | undefined,
|
||||
lastCandidateArea: undefined as Area | undefined,
|
||||
lastInitialCandidate: undefined as
|
||||
| (PartialDropTarget<Data> & { area: Area })
|
||||
| undefined,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -219,40 +220,44 @@ export const useDrop = <Data>(props: UseDropProps<Data>): UseDropHandlers => {
|
||||
const { pointerCoordinates } = state.current;
|
||||
const root = rootRef.current;
|
||||
|
||||
const withArea = (
|
||||
candidate: PartialDropTarget<Data> | 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 = <Data>(props: UseDropProps<Data>): 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 = <Data>(props: UseDropProps<Data>): UseDropHandlers => {
|
||||
|
||||
handleDomMutation() {
|
||||
state.current.childrenRectsCache = new WeakMap();
|
||||
state.current.lastCandidateElement = undefined;
|
||||
state.current.lastInitialCandidate = undefined;
|
||||
detectTarget();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useRef } from "react";
|
||||
|
||||
export type UseHoldProps<Data> = {
|
||||
type UseHoldProps<Data> = {
|
||||
isEqual: (a: Data, b: Data) => boolean;
|
||||
holdTimeThreshold: number;
|
||||
onHold: (data: Data) => void;
|
||||
|
||||
@@ -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: "0.5em",
|
||||
pb: "0.5em",
|
||||
mt: "-0.5em",
|
||||
mb: "-0.5em",
|
||||
|
||||
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,
|
||||
|
||||
@@ -246,8 +246,6 @@ export const Tree = <Data extends { id: string }>({
|
||||
overflowX: "hidden",
|
||||
flexBasis: 0,
|
||||
flexGrow: 1,
|
||||
pt: 2,
|
||||
pb: 2,
|
||||
}}
|
||||
ref={(element) => {
|
||||
rootRef.current = element;
|
||||
|
||||
Reference in new issue
Block a user