diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 12a50dfb..b53feaad 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/block-editor", - "version": "0.1.2", + "version": "0.1.3", "type": "module", "description": "Hydrated accessible editor runtime for WRNexus block documents.", "exports": { diff --git a/packages/block-editor/src/runtime.ts b/packages/block-editor/src/runtime.ts index 5f9e194b..cdf83cb4 100644 --- a/packages/block-editor/src/runtime.ts +++ b/packages/block-editor/src/runtime.ts @@ -7,6 +7,10 @@ export interface MountedEditor { export interface MountEditorOptions { direction?: "ltr" | "rtl"; announce?(announcement: DndAnnouncement): string; + canMove?(intent: { + nodeIds: string[]; + to: { parentId: string; slot: string; index: number }; + }): boolean; onMove(intent: { nodeIds: string[]; to: { parentId: string; slot: string; index: number }; @@ -37,6 +41,19 @@ export function mountBlockEditor(root: HTMLElement, options: MountEditorOptions) let gesture = 0; const groups = new Map(); manager = createDndManager({ + accept(ids, location) { + const destination = groups.get(String(location.group)); + return ( + !destroyed && + generation === currentGeneration && + !!destination && + (options.canMove?.({ + nodeIds: ids.map(String), + to: { ...destination, index: location.index }, + }) ?? + true) + ); + }, direction: options.direction ?? (view.getComputedStyle(root).direction === "rtl" ? "rtl" : "ltr"), reducedMotion: view.matchMedia("(prefers-reduced-motion: reduce)").matches, diff --git a/packages/dnd/package.json b/packages/dnd/package.json index 18b6c3b2..5f4db0bd 100644 --- a/packages/dnd/package.json +++ b/packages/dnd/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dnd", - "version": "0.1.1", + "version": "0.1.2", "type": "module", "description": "Stable accessible drag-and-drop primitives for WRNexus.", "exports": { diff --git a/packages/dnd/src/manager.ts b/packages/dnd/src/manager.ts index db5b49b4..de0e6ce8 100644 --- a/packages/dnd/src/manager.ts +++ b/packages/dnd/src/manager.ts @@ -36,7 +36,10 @@ export function createDndManager(options: DndManagerOptions): DndManager { const source = event.operation.source as (typeof event.operation.source & { group?: string | number; index?: number }) | null; if (!source) return; - const destination = { group: source.group ?? "default", index: source.index ?? 0 }; + const target = event.operation.target as + (typeof event.operation.target & { group?: string | number; index?: number }) | null; + if (!target) return; + const destination = { group: target.group ?? "default", index: target.index ?? 0 }; if (options.accept && !options.accept(activeIds, destination)) { event.preventDefault(); options.announce?.({ type: "invalid", ids: activeIds, location: destination }); diff --git a/packages/dnd/test/manager-lifecycle.test.ts b/packages/dnd/test/manager-lifecycle.test.ts index 5594c860..82349999 100644 --- a/packages/dnd/test/manager-lifecycle.test.ts +++ b/packages/dnd/test/manager-lifecycle.test.ts @@ -55,6 +55,17 @@ function fixture(overrides: Partial = {}) { instances, start: (id: string) => listeners.dragstart!(event(id)), end: (id: string) => listeners.dragend!(event(id)), + over: (target: { group: string; index: number }) => { + let prevented = false; + listeners.dragover!({ + ...event("first"), + operation: { ...event("first").operation, target }, + preventDefault: () => { + prevented = true; + }, + }); + return prevented; + }, get managerDestructions() { return managerDestructions; }, @@ -82,6 +93,14 @@ test("late drop resolution cannot clear or announce a newer gesture", async () = ]); }); +test("hover acceptance checks the proposed target instead of the source's old group", () => { + const f = fixture({ accept: (_ids, to) => to.group === "allowed" }); + f.start("first"); + expect(f.over({ group: "blocked", index: 2 })).toBe(true); + expect(f.announcements.at(-1)?.location).toEqual({ group: "blocked", index: 2 }); + expect(f.over({ group: "allowed", index: 0 })).toBe(false); +}); + test("drop commit rejection reports cancellation and its original intent", async () => { const failure = new Error("save failed"); const errors: unknown[] = [];