Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/brown-rooms-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@dnd-kit/abstract': patch
---

Fix `dragstart` events so `operation.target` includes the initial droppable target while preserving `dragstart` before `dragover` ordering and queued `setDropTarget()` completion semantics.
124 changes: 115 additions & 9 deletions packages/abstract/src/core/manager/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,19 @@ import type {
} from '../entities/index.ts';

import type {DragDropManager} from './manager.ts';
import {defaultPreventable} from './events.ts';
import {defaultPreventable, type DragOverEvent} from './events.ts';
import {StatusValue} from './status.ts';

type DeferredDragOverEvent<
T extends Draggable,
U extends Droppable,
V extends DragDropManager<T, U>,
> = {
event: DragOverEvent<T, U, V>;
resolve(value: boolean): void;
reject(reason?: unknown): void;
};

/**
* Provides actions for controlling drag and drop operations.
*
Expand All @@ -30,6 +40,75 @@ export class DragActions<
*/
constructor(private readonly manager: V) {}

#deferredDragOverEvents: DeferredDragOverEvent<T, U, V>[] | null = null;

#dispatchDragOver(event: DragOverEvent<T, U, V>): Promise<boolean> {
const deferredEvents = this.#deferredDragOverEvents;

if (deferredEvents) {
return new Promise<boolean>((resolve, reject) => {
deferredEvents.push({event, resolve, reject});
});
}

return this.#dispatchDragOverNow(event);
}

#dispatchDragOverNow(event: DragOverEvent<T, U, V>): Promise<boolean> {
this.manager.monitor.dispatch('dragover', event);
return this.manager.renderer.rendering.then(() => event.defaultPrevented);
}

#flushDeferredDragOverEvents() {
const events = this.#deferredDragOverEvents;
this.#deferredDragOverEvents = null;

if (!events) {
return;
}

let error: unknown;

for (const {event, resolve, reject} of events) {
try {
this.#dispatchDragOverNow(event).then(resolve, reject);
} catch (reason) {
error ??= reason;
reject(reason);
}
}

if (error) {
throw error;
}
}

#resolveDeferredDragOverEvents(value: boolean) {
const events = this.#deferredDragOverEvents;
this.#deferredDragOverEvents = null;

if (!events) {
return;
}

for (const {resolve} of events) {
resolve(value);
}
}

#rejectDeferredDragOverEvents(reason: unknown) {
const events = this.#deferredDragOverEvents;
this.#deferredDragOverEvents = null;

if (!events) {
return;
}

for (const {reject} of events) {
reject(reason);
}
}

/**
* Sets the source of the drag operation.
*
Expand Down Expand Up @@ -67,7 +146,7 @@ export class DragActions<
});

if (dragOperation.status.dragging) {
this.manager.monitor.dispatch('dragover', event);
return this.#dispatchDragOver(event);
}

return this.manager.renderer.rendering.then(() => event.defaultPrevented);
Expand Down Expand Up @@ -144,14 +223,41 @@ export class DragActions<
const {status} = dragOperation;
if (status.current !== StatusValue.Initializing) return;

batch(() => {
dragOperation.status.set(StatusValue.Dragging);
this.#deferredDragOverEvents = [];

this.manager.monitor.dispatch('dragstart', {
nativeEvent,
operation: dragOperation.snapshot(),
cancelable: false,
});
dragOperation.status.set(StatusValue.Dragging);

// Let start-time collision effects populate the initial target before
// dragstart, while keeping dragover ordered after dragstart.
queueMicrotask(() => {
try {
if (
controller.signal.aborted ||
dragOperation.status.current !== StatusValue.Dragging
) {
this.#resolveDeferredDragOverEvents(false);
return;
}

this.manager.monitor.dispatch('dragstart', {
nativeEvent,
operation: dragOperation.snapshot(),
cancelable: false,
});

if (
controller.signal.aborted ||
dragOperation.status.current !== StatusValue.Dragging
) {
this.#resolveDeferredDragOverEvents(false);
return;
}

this.#flushDeferredDragOverEvents();
} catch (error) {
this.#rejectDeferredDragOverEvents(error);
throw error;
}
});
});

Expand Down
Loading
Loading