fix(web): keep architecture library drops on the canvas

Keep the shape library open while dragging, accept drops on the
scroller surface, and use a text/plain fallback so dragged
components stay at the drop point instead of snapping back.
This commit is contained in:
tianma-if committed 2026-09-08 09:59:29 +08:00
1 parent a62b263520
commit f98101d734
2 files changed
+49 -13

No files matched your search

@@ -246,6 +246,9 @@ describe("diagram editor canvas surface", () => {
expect(source).toContain('<DropdownMenu modal={false}');
expect(source).toContain("draggable");
expect(source).toContain("event.dataTransfer.setData(ARCHITECTURE_LIBRARY_DRAG_TYPE");
expect(source).toContain('event.dataTransfer.setData("text/plain", icon)');
expect(source).toContain("if (!nextOpen && draggingRef.current) return");
expect(source).toContain("onPointerDownOutside={(event) => {");
expect(source).toContain("onDragOver={handleArchitectureDragOver}");
expect(source).toContain("onDrop={handleArchitectureDrop}");
expect(source).toContain("graph.clientToLocal({ x: event.clientX, y: event.clientY })");
@@ -253,6 +256,7 @@ describe("diagram editor canvas surface", () => {
expect(source).toContain("x: options.position.x - authoredSize.width / 2");
expect(source).toContain("onPick={setPendingArchitectureItem}");
expect(source).toContain("onPointerDownCapture={handlePendingArchitecturePlacement}");
expect(source).toContain("ref={canvasSurfaceRef}");
expect(source).toContain('t("diagram.placeShapeHint"');
});
+45 -13
View File
@@ -312,6 +312,16 @@ const inferArchitectureResourceIcon = (
return item ? architectureResourceIcon(item) : undefined;
};
const isArchitectureLibraryDrag = (event: ReactDragEvent) => {
const types = Array.from(event.dataTransfer.types).map((type) => type.toLowerCase());
return types.includes(ARCHITECTURE_LIBRARY_DRAG_TYPE) || types.includes("text/plain");
};
const architectureLibraryDragIcon = (event: ReactDragEvent) => (
event.dataTransfer.getData(ARCHITECTURE_LIBRARY_DRAG_TYPE)
|| event.dataTransfer.getData("text/plain")
) as ArchitectureResourceIcon;
const ArchitectureComponentLibrary = ({
onPick,
t,
@@ -321,6 +331,7 @@ const ArchitectureComponentLibrary = ({
}) => {
const [open, setOpen] = useState(false);
const [query, setQuery] = useState("");
const draggingRef = useRef(false);
const normalizedQuery = query.trim().toLocaleLowerCase();
const categories = ARCHITECTURE_LIBRARY_CATEGORIES.map((category) => ({
...category,
@@ -329,11 +340,21 @@ const ArchitectureComponentLibrary = ({
return (
<DropdownMenu modal={false} open={open} onOpenChange={(nextOpen) => {
if (!nextOpen && draggingRef.current) return;
setOpen(nextOpen);
if (!nextOpen) setQuery("");
}}>
<DiagramToolbarAddTrigger onPointerEnter={() => setOpen(true)} />
<DropdownMenuContent align="start" className="max-h-[min(36rem,calc(100vh-8rem))] w-[min(30rem,calc(100vw-2rem))] overflow-y-auto p-0">
<DropdownMenuContent
align="start"
className="max-h-[min(36rem,calc(100vh-8rem))] w-[min(30rem,calc(100vw-2rem))] overflow-y-auto p-0"
onPointerDownOutside={(event) => {
if (draggingRef.current) event.preventDefault();
}}
onFocusOutside={(event) => {
if (draggingRef.current) event.preventDefault();
}}
>
<div className="sticky top-0 z-10 border-b border-slate-200 bg-white p-2.5">
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
@@ -369,11 +390,19 @@ const ArchitectureComponentLibrary = ({
aria-label={label}
className={cn("flex h-10 w-10 cursor-grab justify-center rounded-lg p-0 hover:bg-current/10 focus:bg-current/10 active:cursor-grabbing", category.tone)}
draggable
onPointerDown={(event) => event.stopPropagation()}
onDragStart={(event) => {
draggingRef.current = true;
event.stopPropagation();
const icon = architectureResourceIcon(item);
event.dataTransfer.effectAllowed = "copy";
event.dataTransfer.setData(ARCHITECTURE_LIBRARY_DRAG_TYPE, architectureResourceIcon(item));
event.dataTransfer.setData("text/plain", icon);
event.dataTransfer.setData(ARCHITECTURE_LIBRARY_DRAG_TYPE, icon);
}}
onDragEnd={() => {
draggingRef.current = false;
setOpen(false);
}}
onDragEnd={() => setOpen(false)}
onSelect={() => {
onPick(item);
setOpen(false);
@@ -1897,26 +1926,26 @@ export const DiagramEditorPane = ({
}, [addNode, t]);
const handleArchitectureDragOver = (event: ReactDragEvent<HTMLDivElement>) => {
if (document?.kind !== "architecture" || readOnly || !Array.from(event.dataTransfer.types).includes(ARCHITECTURE_LIBRARY_DRAG_TYPE)) return;
if (document?.kind !== "architecture" || readOnly || !isArchitectureLibraryDrag(event)) return;
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
};
const handleArchitectureDrop = (event: ReactDragEvent<HTMLDivElement>) => {
if (document?.kind !== "architecture" || readOnly) return;
const resourceIcon = event.dataTransfer.getData(ARCHITECTURE_LIBRARY_DRAG_TYPE) as ArchitectureResourceIcon;
if (document?.kind !== "architecture" || readOnly || !isArchitectureLibraryDrag(event)) return;
event.preventDefault();
event.stopPropagation();
const resourceIcon = architectureLibraryDragIcon(event);
const item = ARCHITECTURE_LIBRARY_ITEMS.find((candidate) => architectureResourceIcon(candidate) === resourceIcon);
const graph = graphRef.current;
if (!item || !graph) return;
event.preventDefault();
event.stopPropagation();
placeArchitectureItem(item, graph.clientToLocal({ x: event.clientX, y: event.clientY }));
};
const handlePendingArchitecturePlacement = (event: ReactPointerEvent<HTMLDivElement>) => {
if (!pendingArchitectureItem || document?.kind !== "architecture" || readOnly || event.button !== 0) return;
const target = event.target instanceof Element ? event.target : null;
if (target?.closest(".x6-node, .x6-edge")) return;
if (target?.closest(".x6-node, .x6-edge, input, textarea, [role='dialog']")) return;
const graph = graphRef.current;
if (!graph) return;
event.preventDefault();
@@ -2544,7 +2573,13 @@ export const DiagramEditorPane = ({
)}
theme={theme}
/>
<div ref={canvasSurfaceRef} className="relative min-h-0 flex-1">
<div
ref={canvasSurfaceRef}
className={cn("relative min-h-0 flex-1", pendingArchitectureItem && "cursor-crosshair")}
onDragOver={handleArchitectureDragOver}
onDrop={handleArchitectureDrop}
onPointerDownCapture={handlePendingArchitecturePlacement}
>
<div
ref={containerRef}
className={cn("edgeever-diagram-canvas absolute inset-0 touch-none outline-none", pendingArchitectureItem && "cursor-crosshair")}
@@ -2556,9 +2591,6 @@ export const DiagramEditorPane = ({
data-space-pan={spacePanActive ? "active" : undefined}
tabIndex={0}
aria-label={t("diagram.canvas", { type: kindLabel })}
onDragOver={handleArchitectureDragOver}
onDrop={handleArchitectureDrop}
onPointerDownCapture={handlePendingArchitecturePlacement}
/>
{!readOnly && (
<div