Skip to content

Commit f3ad20d

Browse files
authored
Refactor CollisionDetection to return an array of Collisions (#558)
1 parent f4fdd01 commit f3ad20d

File tree

24 files changed

+288
-123
lines changed

24 files changed

+288
-123
lines changed

.changeset/array-of-collisions.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
'@dnd-kit/core': major
3+
'@dnd-kit/sortable': minor
4+
---
5+
6+
Refactor of the `CollisionDetection` interface to return an array of `Collision`s:
7+
8+
```diff
9+
+export interface Collision {
10+
+ id: UniqueIdentifier;
11+
+ data?: Record<string, any>;
12+
+}
13+
14+
export type CollisionDetection = (args: {
15+
active: Active;
16+
collisionRect: ClientRect;
17+
droppableContainers: DroppableContainer[];
18+
pointerCoordinates: Coordinates | null;
19+
-}) => UniqueIdentifier;
20+
+}) => Collision[];
21+
```
22+
23+
This is a breaking change that requires all collision detection strategies to be updated to return an array of `Collision` rather than a single `UniqueIdentifier`
24+
25+
The `over` property remains a single `UniqueIdentifier`, and is set to the first item in returned in the collisions array.
26+
27+
Consumers can also access the `collisions` property which can be used to implement use-cases such as combining droppables in user-land.
28+
29+
The `onDragMove`, `onDragOver` and `onDragEnd` callbacks are also updated to receive the collisions array property.
30+
31+
Built-in collision detections such as rectIntersection, closestCenter, closestCorners and pointerWithin adhere to the CollisionDescriptor interface, which extends the Collision interface:
32+
33+
```ts
34+
export interface CollisionDescriptor extends Collision {
35+
data: {
36+
droppableContainer: DroppableContainer;
37+
value: number;
38+
[key: string]: any;
39+
};
40+
}
41+
```

packages/core/src/components/DndContext/DndContext.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import {
5959
defaultCoordinates,
6060
getAdjustedRect,
6161
getRectDelta,
62+
getFirstCollision,
6263
rectIntersection,
6364
} from '../../utilities';
6465
import {getTransformAgnosticClientRect} from '../../utilities/rect';
@@ -209,6 +210,7 @@ export const DndContext = memo(function DndContext({
209210
active: null,
210211
activeNode,
211212
collisionRect: null,
213+
collisions: null,
212214
droppableRects,
213215
draggableNodes,
214216
draggingNode: null,
@@ -282,7 +284,7 @@ export const DndContext = memo(function DndContext({
282284
? getAdjustedRect(draggingNodeRect, modifiedTranslate)
283285
: null;
284286

285-
const overId =
287+
const collisions =
286288
active && collisionRect
287289
? collisionDetection({
288290
active,
@@ -291,6 +293,7 @@ export const DndContext = memo(function DndContext({
291293
pointerCoordinates,
292294
})
293295
: null;
296+
const overId = getFirstCollision(collisions, 'id');
294297
const [over, setOver] = useState<Over | null>(null);
295298

296299
const transform = adjustScale(
@@ -368,14 +371,20 @@ export const DndContext = memo(function DndContext({
368371

369372
function createHandler(type: Action.DragEnd | Action.DragCancel) {
370373
return async function handler() {
371-
const {active, over, scrollAdjustedTranslate} = sensorContext.current;
374+
const {
375+
active,
376+
collisions,
377+
over,
378+
scrollAdjustedTranslate,
379+
} = sensorContext.current;
372380
let event: DragEndEvent | null = null;
373381

374382
if (active && scrollAdjustedTranslate) {
375383
const {cancelDrop} = latestProps.current;
376384

377385
event = {
378386
active: active,
387+
collisions,
379388
delta: scrollAdjustedTranslate,
380389
over,
381390
};
@@ -480,14 +489,15 @@ export const DndContext = memo(function DndContext({
480489

481490
useEffect(() => {
482491
const {onDragMove} = latestProps.current;
483-
const {active, over} = sensorContext.current;
492+
const {active, collisions, over} = sensorContext.current;
484493

485494
if (!active) {
486495
return;
487496
}
488497

489498
const event: DragMoveEvent = {
490499
active,
500+
collisions,
491501
delta: {
492502
x: scrollAdjustedTranslate.x,
493503
y: scrollAdjustedTranslate.y,
@@ -503,6 +513,7 @@ export const DndContext = memo(function DndContext({
503513
() => {
504514
const {
505515
active,
516+
collisions,
506517
droppableContainers,
507518
scrollAdjustedTranslate,
508519
} = sensorContext.current;
@@ -524,6 +535,7 @@ export const DndContext = memo(function DndContext({
524535
: null;
525536
const event: DragOverEvent = {
526537
active,
538+
collisions,
527539
delta: {
528540
x: scrollAdjustedTranslate.x,
529541
y: scrollAdjustedTranslate.y,
@@ -546,6 +558,7 @@ export const DndContext = memo(function DndContext({
546558
active,
547559
activeNode,
548560
collisionRect,
561+
collisions,
549562
droppableRects,
550563
draggableNodes,
551564
draggingNode,
@@ -563,6 +576,7 @@ export const DndContext = memo(function DndContext({
563576
}, [
564577
active,
565578
activeNode,
579+
collisions,
566580
collisionRect,
567581
draggableNodes,
568582
draggingNode,
@@ -592,6 +606,7 @@ export const DndContext = memo(function DndContext({
592606
ariaDescribedById: {
593607
draggable: draggableDescribedById,
594608
},
609+
collisions,
595610
containerNodeRect,
596611
dispatch,
597612
dragOverlay,
@@ -613,6 +628,7 @@ export const DndContext = memo(function DndContext({
613628
activeNodeRect,
614629
activatorEvent,
615630
activators,
631+
collisions,
616632
containerNodeRect,
617633
dragOverlay,
618634
dispatch,

packages/core/src/hooks/useDroppable.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function useDroppable({
2323
id,
2424
}: UseDroppableArguments) {
2525
const key = useUniqueId(ID_PREFIX);
26-
const {active, dispatch, over} = useContext(Context);
26+
const {active, collisions, dispatch, over} = useContext(Context);
2727
const rect = useRef<ClientRect | null>(null);
2828
const [nodeRef, setNodeRef] = useNodeRef();
2929
const dataRef = useData(data);
@@ -68,6 +68,7 @@ export function useDroppable({
6868

6969
return {
7070
active,
71+
collisions,
7172
rect,
7273
isOver: over?.id === id,
7374
node: nodeRef,

packages/core/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,15 @@ export type {
9494
export {
9595
defaultCoordinates,
9696
getClientRect,
97+
getFirstCollision,
9798
getScrollableAncestors,
9899
closestCenter,
99100
closestCorners,
100101
rectIntersection,
101102
pointerWithin,
102103
} from './utilities';
103-
export type {CollisionDetection} from './utilities';
104+
export type {
105+
Collision,
106+
CollisionDescriptor,
107+
CollisionDetection,
108+
} from './utilities';

packages/core/src/sensors/pointer/AbstractPointerSensor.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
getWindow,
66
} from '@dnd-kit/utilities';
77

8+
import {defaultCoordinates} from '../../utilities';
89
import {
910
getEventListenerTarget,
1011
hasExceededDistance,
@@ -80,7 +81,7 @@ export class AbstractPointerSensor implements SensorInstance {
8081
this.documentListeners = new Listeners(this.document);
8182
this.listeners = new Listeners(listenerTarget);
8283
this.windowListeners = new Listeners(getWindow(target));
83-
this.initialCoordinates = getEventCoordinates(event);
84+
this.initialCoordinates = getEventCoordinates(event) ?? defaultCoordinates;
8485
this.handleStart = this.handleStart.bind(this);
8586
this.handleMove = this.handleMove.bind(this);
8687
this.handleEnd = this.handleEnd.bind(this);
@@ -174,7 +175,7 @@ export class AbstractPointerSensor implements SensorInstance {
174175
return;
175176
}
176177

177-
const coordinates = getEventCoordinates(event);
178+
const coordinates = getEventCoordinates(event) ?? defaultCoordinates;
178179
const delta = getCoordinatesDelta(initialCoordinates, coordinates);
179180

180181
if (!activated && activationConstraint) {

packages/core/src/sensors/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
UniqueIdentifier,
1515
ClientRect,
1616
} from '../types';
17+
import type {Collision} from '../utilities/algorithms';
1718

1819
export enum Response {
1920
Start = 'start',
@@ -25,6 +26,7 @@ export type SensorContext = {
2526
active: Active | null;
2627
activeNode: HTMLElement | null;
2728
collisionRect: ClientRect | null;
29+
collisions: Collision[] | null;
2830
draggableNodes: DraggableNodes;
2931
draggingNode: HTMLElement | null;
3032
draggingNodeRect: ClientRect | null;

packages/core/src/store/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const Context = createContext<DndContextDescriptor>({
1313
ariaDescribedById: {
1414
draggable: '',
1515
},
16+
collisions: null,
1617
containerNodeRect: null,
1718
dispatch: noop,
1819
draggableNodes: {},

packages/core/src/store/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {MutableRefObject} from 'react';
22

33
import type {Coordinates, ClientRect, UniqueIdentifier} from '../types';
4+
import type {Collision} from '../utilities/algorithms';
45
import type {SyntheticListeners} from '../hooks/utilities';
56
import type {Actions} from './actions';
67
import type {DroppableContainersMap} from './constructors';
@@ -80,6 +81,7 @@ export interface DndContextDescriptor {
8081
ariaDescribedById: {
8182
draggable: UniqueIdentifier;
8283
};
84+
collisions: Collision[] | null;
8385
containerNodeRect: ClientRect | null;
8486
draggableNodes: DraggableNodes;
8587
droppableContainers: DroppableContainers;

packages/core/src/types/events.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import type {Active, Over} from '../store';
2+
import type {Collision} from '../utilities/algorithms';
3+
24
import type {Translate} from './coordinates';
35

46
interface DragEvent {
57
active: Active;
8+
collisions: Collision[] | null;
69
delta: Translate;
710
over: Over | null;
811
}

packages/core/src/utilities/algorithms/closestCenter.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import {distanceBetween} from '../coordinates';
2-
import type {Coordinates, ClientRect, UniqueIdentifier} from '../../types';
2+
import type {Coordinates, ClientRect} from '../../types';
33

4-
import type {CollisionDetection} from './types';
4+
import type {CollisionDescriptor, CollisionDetection} from './types';
5+
import {sortCollisionsAsc} from './helpers';
56

67
/**
78
* Returns the coordinates of the center of a given ClientRect
@@ -18,7 +19,7 @@ function centerOfRectangle(
1819
}
1920

2021
/**
21-
* Returns the closest rectangle from an array of rectangles to the center of a given
22+
* Returns the closest rectangles from an array of rectangles to the center of a given
2223
* rectangle.
2324
*/
2425
export const closestCenter: CollisionDetection = ({
@@ -30,23 +31,20 @@ export const closestCenter: CollisionDetection = ({
3031
collisionRect.left,
3132
collisionRect.top
3233
);
33-
let minDistanceToCenter = Infinity;
34-
let minDroppableContainer: UniqueIdentifier | null = null;
34+
const collisions: CollisionDescriptor[] = [];
3535

3636
for (const droppableContainer of droppableContainers) {
3737
const {
38+
id,
3839
rect: {current: rect},
3940
} = droppableContainer;
4041

4142
if (rect) {
4243
const distBetween = distanceBetween(centerOfRectangle(rect), centerRect);
4344

44-
if (distBetween < minDistanceToCenter) {
45-
minDistanceToCenter = distBetween;
46-
minDroppableContainer = droppableContainer.id;
47-
}
45+
collisions.push({id, data: {droppableContainer, value: distBetween}});
4846
}
4947
}
5048

51-
return minDroppableContainer;
49+
return collisions.sort(sortCollisionsAsc);
5250
};

0 commit comments

Comments
 (0)