Filter invalid nested drag targets before collision selection
Quality / quality (ubuntu-latest) (push) Failing after 9m59s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
Clintchiz committed 2026-09-05 16:46:01 +05:30
1 parent 88207cdd48
commit ca2fcb4016
3 files changed
+27 -4

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dnd",
"version": "0.1.3",
"version": "0.1.4",
"type": "module",
"description": "Stable accessible drag-and-drop primitives for WRNexus.",
"exports": {
+9 -1
View File
@@ -121,7 +121,15 @@ export function createDndManager(options: DndManagerOptions): DndManager {
element: registration.element,
handle: registration.handle,
type: registration.type,
accept: registration.accepts,
accept: (source) => {
const origin = entries.get(source.id)?.current.element;
// A nested ancestor overlaps its own descendants and must not steal
// their next keyboard/pointer target just because its box is larger.
if (source.id !== registration.id && origin && registration.element.contains?.(origin)) return false;
if (registration.accepts && !registration.accepts.some(type => type === source.type)) return false;
const target = entries.get(registration.id)?.sortable;
return options.accept?.([source.id], { group: target?.group ?? registration.group, index: target?.index ?? registration.index }) ?? true;
},
disabled: registration.disabled,
transition: reducedMotion ? null : undefined,
data: { ...registration.data, direction },
+17 -2
View File
@@ -12,7 +12,7 @@ const compiled = new Bun.Transpiler({ loader: "ts" })
function fixture(overrides: Partial<DndManagerOptions> = {}) {
const listeners: Record<string, (event: any) => any> = {};
const announcements: DndAnnouncement[] = [];
const instances: Array<{ destroyed: number }> = [];
const instances: Array<{ destroyed: number; input: any }> = [];
let managerDestructions = 0;
class Backend {
monitor = {
@@ -26,7 +26,7 @@ function fixture(overrides: Partial<DndManagerOptions> = {}) {
}
class Sortable {
destroyed = 0;
constructor() {
constructor(public input: any) {
instances.push(this);
}
destroy() {
@@ -94,6 +94,21 @@ test("late drop resolution cannot clear or announce a newer gesture", async () =
]);
});
test("collision candidates exclude overlapping ancestors and invalid destinations before sorting", () => {
const f = fixture({ accept: (_ids, to) => to.group !== "blocked" });
const child = {} as HTMLElement;
f.manager.registerSortable({ id: "child", group: "content", index: 0, element: child, type: "text" });
f.manager.registerSortable({ id: "parent", group: "root", index: 0, element: { contains: (element: unknown) => element === child } as HTMLElement });
f.manager.registerSortable({ id: "sibling", group: "content", index: 1, element: {} as HTMLElement, accepts: ["text"] });
f.manager.registerSortable({ id: "invalid", group: "blocked", index: 0, element: {} as HTMLElement });
const source = { id: "child", type: "text" };
expect(f.instances[1]!.input.accept(source)).toBe(false);
expect(f.instances[2]!.input.accept(source)).toBe(true);
expect(f.instances[2]!.input.accept({ ...source, type: "image" })).toBe(false);
expect(f.instances[3]!.input.accept(source)).toBe(false);
f.manager.destroy();
});
test("empty drop zones use explicit locations for acceptance and commit", async () => {
const commits: unknown[] = [];
const f = fixture({ accept: (_ids, to) => to.group === "empty", onMoveIntent: intent => { commits.push(intent.to); } });