Open realing29 opened 1 year ago
I have if the height of an element has changed, for example the text is smaller than others, then when you drag it, it changes position twice, instead of once to move to its position. And all identical elements work without any problems when dragging them.
@realing29 Hi! Experiencing same issue, have you found any solution?
Hi, the following two steps were the workaround for us.
1. Switch objects not immediately on collision, but create a smaller region the user must drag into. The red lines are computed relatively to the object corner and cursor position (it is not an invisible element or anything like this):
The collision detection for square-ish target:
type Inset = 0 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 1;
/**
* Ignores X pixels (defined as percentage) from the edge of the element.
* The ignored area takes into account element asymmetry, so it is derived from the shortest side.
* @param targetBoundingRect Droppable element rect
* @param pointer User's pointer coordinates
* @param insetRelativeToShorterSide Percentage of ignored pixels from the edge of the droppable element
*/
export function isWithinTargetInset(
targetBoundingRect: DOMRectLike,
pointer: XYCoord,
insetRelativeToShorterSide: Inset,
): boolean {
/**
* targetA ----------------
* ¦ insetA ----------- ¦
* ¦ ¦ ¦ ¦
* ¦ ¦ ¦ ¦
* ¦ ----------- insetC ¦
* ---------------- targetC
*/
const targetA = { x: targetBoundingRect.left, y: targetBoundingRect.top };
const targetC = { x: targetBoundingRect.right, y: targetBoundingRect.bottom };
const lengthOfShorterSide = Math.min(targetC.x - targetA.x, targetC.y - targetA.y);
const ignoredPixelsPerSide = (lengthOfShorterSide * insetRelativeToShorterSide) / 2;
const insetA = { x: targetA.x + ignoredPixelsPerSide, y: targetA.y + ignoredPixelsPerSide };
const insetC = { x: targetC.x - ignoredPixelsPerSide, y: targetC.y - ignoredPixelsPerSide };
return (
pointer.x > insetA.x && pointer.x < insetC.x && pointer.y > insetA.y && pointer.y < insetC.y
);
}
Usage:
// dropTargetRefObject is refObject set to the drop target element
// targetId is our identifier of the target entity that the drop target element represents
const [, connectDropTarget] = useDrop<DragObject, never, CollectedProps>({
hover: (dragObject: DragObject, monitor: DropTargetMonitor) => {
const targetBoundingRect = dropTargetRefObject.current?.getBoundingClientRect();
const pointer = monitor.getClientOffset();
if (isWithinTargetInset(targetBoundingRect, pointer, 0.2)) {
onMoveItem(dragObject.sourceId, targetId);
}
},
// ...other props
});
Different strategies must be used based on the target element dimensions.
2. We stopped using custom isDragging callback. Using default one helped, from the docs: https://react-dnd.github.io/react-dnd/docs/api/use-drag
isDragging(monitor): Optional. By default, only the drag source that initiated the drag operation is considered to be dragging. You can override this behavior by defining a custom isDragging method. It might return something like props.id === monitor.getItem().id. Do this if the original component may be unmounted during the dragging and later “resurrected” with a different parent. For example, when moving a card across the lists in a Kanban board, you want it to retain the dragged appearance—even though technically, the component gets unmounted and a different one gets mounted every time you move it to another list. Note: You may not call monitor.isDragging() inside this method.
Adding collision detection for elongated rectangles:
enum Direction {
Forward = 'Forward',
Backward = 'Backward',
}
function crossedHalfTargetHeight(
targetBoundingRect: DOMRectLike,
clientOffset: XYCoord,
dragDirection: Direction,
): boolean {
const middleY = (targetBoundingRect.bottom - targetBoundingRect.top) / 2;
const offsetY = clientOffset.y - targetBoundingRect.top;
return (
(dragDirection === Direction.Forward && offsetY >= middleY) ||
(dragDirection === Direction.Backward && offsetY <= middleY)
);
}
const getItemsDistance = <TItem, TKey>(
items: ReadonlyArray<TItem> | Immutable.List<TItem>,
fromKey: TKey,
toKey: TKey,
getKey: (item: TItem) => TKey,
): number | null => {
const fromIndex = items.findIndex((item: TItem) => getKey(item) === fromKey);
const toIndex = items.findIndex((item: TItem) => getKey(item) === toKey);
if (fromIndex < 0 || toIndex < 0) {
return null;
}
return toIndex - fromIndex;
};
const getItemsDirection = <TItem, TKey>(
items: Immutable.List<TItem> | ReadonlyArray<TItem>,
fromKey: TKey,
toKey: TKey,
getKey: (item: TItem) => TKey,
): Direction | null => {
const itemsDistance = getItemsDistance(items, fromKey, toKey, getKey);
if (!itemsDistance) {
return null;
}
return itemsDistance > 0 ? Direction.Forward : Direction.Backward;
};
// Usage in hover function:
...
hover: (dragObject: DragObject, monitor: DropTargetMonitor) => {
const targetBoundingRect = dropTargetRefObject.current?.getBoundingClientRect();
const pointer = monitor.getClientOffset();
const dragDirection = getItemsDirection(
items, // an array of all entities
sourceId, // tragged entity id
targetId, // hovered entity id
getId, // a function that creates an id from an entity, for example: (item) => item.id
);
if (
!!dragDirection &&
crossedHalfTargetHeight(args.targetBoundingRect, args.pointer, dragDirection)
) {
onMoveItem(dragObject.sourceId, targetId);
}
},
Describe the bug The elements twitch when swapping places
Reproduction
https://codesandbox.io/s/github/react-dnd/react-dnd/tree/gh-pages/examples_js/04-sortable/stress-test?from-embed=&file=/src/Card.js:784-792 link on codesandbox from documentation - https://react-dnd.github.io/react-dnd/docs/api/use-drag
Steps to reproduce the behavior:
movement element
Expected behavior A clear and concise description of what you expected to happen.
Screenshots https://drive.google.com/file/d/1A9CmzmiAQILg8zOvw0XrGUAdHqxsNfNJ/view?usp=sharing
Desktop (please complete the following information):