Skip to content

Commit

Permalink
PositionObserver rework
Browse files Browse the repository at this point in the history
  • Loading branch information
clauderic committed Sep 12, 2024
1 parent 8053e4b commit 5ca3a71
Show file tree
Hide file tree
Showing 16 changed files with 527 additions and 266 deletions.
3 changes: 2 additions & 1 deletion .changeset/collision-observer.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
'@dnd-kit/abstract': patch
'@dnd-kit/dom': patch
---

Reduce the how frequently collisions are re-computed when the position of the drag operation does not change.
Rework how collisions are detected and how the position of elements is observed using a new `PositionObserver`.
22 changes: 3 additions & 19 deletions packages/abstract/src/core/collision/observer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {batch, signal, untracked, type Signal, effects} from '@dnd-kit/state';
import {signal, untracked, type Signal, effects} from '@dnd-kit/state';
import type {Coordinates} from '@dnd-kit/geometry';

import type {DragDropManager} from '../manager/index.ts';
Expand Down Expand Up @@ -49,25 +49,9 @@ export class CollisionObserver<
);
}

forceUpdateCount = signal(0);

public forceUpdate(refresh = true) {
public forceUpdate() {
untracked(() => {
const {source} = this.manager.dragOperation;

batch(() => {
if (refresh) {
for (const droppable of this.manager.registry.droppables) {
if (source && !droppable.accepts(source)) {
continue;
}

droppable.refreshShape();
}
}

this.#collisions.value = this.computeCollisions();
});
this.#collisions.value = this.computeCollisions();
});
}

Expand Down
4 changes: 0 additions & 4 deletions packages/abstract/src/core/entities/droppable/droppable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,4 @@ export class Droppable<
public get isDropTarget() {
return this.manager?.dragOperation.target?.id === this.id;
}

public refreshShape() {
// To be implemented by subclasses
}
}
150 changes: 36 additions & 114 deletions packages/dom/src/core/entities/droppable/droppable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,9 @@ import type {
} from '@dnd-kit/abstract';
import {defaultCollisionDetection} from '@dnd-kit/collision';
import type {CollisionDetector} from '@dnd-kit/collision';
import {Signal, reactive, signal, untracked} from '@dnd-kit/state';
import {reactive, untracked} from '@dnd-kit/state';
import type {Shape} from '@dnd-kit/geometry';
import {
DOMRectangle,
getDocument,
scheduler,
getFirstScrollableAncestor,
} from '@dnd-kit/dom/utilities';
import {DOMRectangle, PositionObserver} from '@dnd-kit/dom/utilities';

import type {DragDropManager} from '../../manager/manager.ts';

Expand All @@ -33,6 +28,25 @@ export class Droppable<T extends Data = Data> extends AbstractDroppable<
manager: DragDropManager | undefined
) {
const {collisionDetector = defaultCollisionDetection} = input;
const updateShape = (boundingClientRect?: DOMRectReadOnly | null) => {
const {element} = this;

if (!element || boundingClientRect === null) {
this.shape = undefined;
return undefined;
}

const updatedShape = new DOMRectangle(element);

const shape = untracked(() => this.shape);
if (updatedShape && shape?.equals(updatedShape)) {
return shape;
}

this.shape = updatedShape;

return updatedShape;
};

super(
{
Expand All @@ -45,74 +59,25 @@ export class Droppable<T extends Data = Data> extends AbstractDroppable<
if (!manager) return;

const {dragOperation} = manager;

if (element && dragOperation.status.initialized) {
const scrollableAncestor = getFirstScrollableAncestor(element);
const doc = getDocument(element);
const root =
scrollableAncestor === doc.scrollingElement
? doc
: scrollableAncestor;
const intersectionObserver = new IntersectionObserver(
(entries) => {
const [entry] = entries.slice(-1);
const {width, height} = entry.boundingClientRect;

if (!width && !height) {
return;
}

this.visible = entry.isIntersecting;
},
{
root: root ?? doc,
rootMargin: '40%',
}
);

const mutationObserver = new MutationObserver(() =>
scheduler.schedule(this.refreshShape)
const {source} = dragOperation;
const observePosition =
source &&
dragOperation.status.initialized &&
element &&
this.accepts(source);

if (observePosition) {
const positionObserver = new PositionObserver(
element,
updateShape
);

const resizeObserver = new ResizeObserver(() =>
scheduler.schedule(this.refreshShape)
);

if (element.parentElement) {
mutationObserver.observe(element.parentElement, {
childList: true,
});
}

resizeObserver.observe(element);
intersectionObserver.observe(element);

return () => {
positionObserver.disconnect();
this.shape = undefined;
this.visible = undefined;
resizeObserver.disconnect();
mutationObserver.disconnect();
intersectionObserver.disconnect();
};
}
},
() => {
const {manager} = this;

if (!manager) return;

const {dragOperation} = manager;
const {status} = dragOperation;
const source = untracked(() => dragOperation.source);

if (status.initialized) {
if (source?.type != null && !this.accepts(source)) {
return;
}

scheduler.schedule(this.refreshShape);
}
},
() => {
if (this.manager?.dragOperation.status.initialized) {
return () => {
Expand All @@ -125,54 +90,11 @@ export class Droppable<T extends Data = Data> extends AbstractDroppable<
manager
);

this.internal = {
element: signal(element),
};
this.refreshShape = this.refreshShape.bind(this);

/*
* If a droppable target mounts during a drag operation, assume it is visible
* so that we can update its shape immediately.
*/
if (this.manager?.dragOperation.status.initialized) {
this.visible = true;
}
this.refreshShape = () => updateShape();
}

@reactive
public accessor visible: Boolean | undefined;

@reactive
public accessor placeholder: Element | undefined;

public set element(value: Element | undefined) {
this.internal.element.value = value;
}

public get element() {
return this.placeholder ?? this.internal?.element.value;
}

public refreshShape(ignoreTransform?: boolean): Shape | undefined {
const {element, shape} = this;

if (!element || this.visible === false) {
this.shape = undefined;
return undefined;
}

const updatedShape = new DOMRectangle(element, ignoreTransform);

if (updatedShape && shape?.equals(updatedShape)) {
return shape;
}

this.shape = updatedShape;

return updatedShape;
}
public accessor element: Element | undefined;

internal: {
element: Signal<Element | undefined>;
};
public refreshShape: () => Shape | undefined;
}
Loading

0 comments on commit 5ca3a71

Please sign in to comment.