fix(canvas): restore visible handle glow during drag
This commit is contained in:
@@ -10,7 +10,13 @@ import {
|
||||
useCanvasConnectionMagnetism,
|
||||
} from "@/components/canvas/canvas-connection-magnetism-context";
|
||||
|
||||
const connectionStateRef: { current: { inProgress: boolean } } = {
|
||||
const connectionStateRef: {
|
||||
current: {
|
||||
inProgress?: boolean;
|
||||
fromNode?: { id: string };
|
||||
fromHandle?: { id?: string; type?: "source" | "target" };
|
||||
};
|
||||
} = {
|
||||
current: { inProgress: false },
|
||||
};
|
||||
|
||||
@@ -77,7 +83,11 @@ describe("CanvasHandle", () => {
|
||||
});
|
||||
|
||||
async function renderHandle(args?: {
|
||||
connectionState?: {
|
||||
inProgress?: boolean;
|
||||
fromNode?: { id: string };
|
||||
fromHandle?: { id?: string; type?: "source" | "target" };
|
||||
};
|
||||
activeTarget?: {
|
||||
nodeId: string;
|
||||
handleId?: string;
|
||||
@@ -88,7 +98,7 @@ describe("CanvasHandle", () => {
|
||||
} | null;
|
||||
props?: Partial<React.ComponentProps<typeof CanvasHandle>>;
|
||||
}) {
|
||||
connectionStateRef.current = { inProgress: args?.inProgress ?? false };
|
||||
connectionStateRef.current = args?.connectionState ?? { inProgress: false };
|
||||
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
@@ -98,7 +108,7 @@ describe("CanvasHandle", () => {
|
||||
nodeId="node-1"
|
||||
nodeType="image"
|
||||
type="target"
|
||||
position="left"
|
||||
position={"left" as React.ComponentProps<typeof CanvasHandle>["position"]}
|
||||
id="image-in"
|
||||
{...args?.props}
|
||||
/>
|
||||
@@ -128,7 +138,7 @@ describe("CanvasHandle", () => {
|
||||
|
||||
it("turns on near-target glow when this handle is active target", async () => {
|
||||
await renderHandle({
|
||||
inProgress: true,
|
||||
connectionState: { inProgress: true },
|
||||
activeTarget: {
|
||||
nodeId: "node-1",
|
||||
handleId: "image-in",
|
||||
@@ -145,7 +155,7 @@ describe("CanvasHandle", () => {
|
||||
|
||||
it("renders a stronger glow in snapped state than near state", async () => {
|
||||
await renderHandle({
|
||||
inProgress: true,
|
||||
connectionState: { inProgress: true },
|
||||
activeTarget: {
|
||||
nodeId: "node-1",
|
||||
handleId: "image-in",
|
||||
@@ -160,7 +170,7 @@ describe("CanvasHandle", () => {
|
||||
const nearGlow = nearHandle.style.boxShadow;
|
||||
|
||||
await renderHandle({
|
||||
inProgress: true,
|
||||
connectionState: { inProgress: true },
|
||||
activeTarget: {
|
||||
nodeId: "node-1",
|
||||
handleId: "image-in",
|
||||
@@ -178,7 +188,7 @@ describe("CanvasHandle", () => {
|
||||
|
||||
it("does not glow for non-target handles during the same drag", async () => {
|
||||
await renderHandle({
|
||||
inProgress: true,
|
||||
connectionState: { inProgress: true },
|
||||
activeTarget: {
|
||||
nodeId: "other-node",
|
||||
handleId: "image-in",
|
||||
@@ -193,13 +203,33 @@ describe("CanvasHandle", () => {
|
||||
expect(handle.getAttribute("data-glow-state")).toBe("idle");
|
||||
});
|
||||
|
||||
it("shows glow while dragging when connection payload exists without inProgress", async () => {
|
||||
await renderHandle({
|
||||
connectionState: {
|
||||
fromNode: { id: "source-node" },
|
||||
fromHandle: { id: "image-out", type: "source" },
|
||||
},
|
||||
activeTarget: {
|
||||
nodeId: "node-1",
|
||||
handleId: "image-in",
|
||||
handleType: "target",
|
||||
centerX: 120,
|
||||
centerY: 80,
|
||||
distancePx: HANDLE_SNAP_RADIUS_PX + 2,
|
||||
},
|
||||
});
|
||||
|
||||
const handle = getHandleElement();
|
||||
expect(handle.getAttribute("data-glow-state")).toBe("near");
|
||||
});
|
||||
|
||||
it("emits stable handle geometry data attributes", async () => {
|
||||
await renderHandle({
|
||||
props: {
|
||||
nodeId: "node-2",
|
||||
id: undefined,
|
||||
type: "source",
|
||||
position: "right",
|
||||
position: "right" as React.ComponentProps<typeof CanvasHandle>["position"],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -27,6 +27,16 @@ const reactFlowStateRef: {
|
||||
},
|
||||
};
|
||||
|
||||
const connectionStateRef: {
|
||||
current: {
|
||||
fromHandle?: { type?: "source" | "target" };
|
||||
};
|
||||
} = {
|
||||
current: {
|
||||
fromHandle: { type: "source" },
|
||||
},
|
||||
};
|
||||
|
||||
vi.mock("@xyflow/react", async () => {
|
||||
const actual = await vi.importActual<typeof import("@xyflow/react")>("@xyflow/react");
|
||||
|
||||
@@ -36,6 +46,7 @@ vi.mock("@xyflow/react", async () => {
|
||||
getNodes: () => reactFlowStateRef.current.nodes,
|
||||
getEdges: () => reactFlowStateRef.current.edges,
|
||||
}),
|
||||
useConnection: () => connectionStateRef.current,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -93,6 +104,7 @@ describe("CustomConnectionLine", () => {
|
||||
function renderLine(args?: {
|
||||
withMagnetHandle?: boolean;
|
||||
connectionStatus?: ConnectionLineComponentProps["connectionStatus"];
|
||||
omitFromHandleType?: boolean;
|
||||
}) {
|
||||
document
|
||||
.querySelectorAll("[data-testid='custom-line-magnet-handle']")
|
||||
@@ -106,6 +118,10 @@ describe("CustomConnectionLine", () => {
|
||||
edges: [],
|
||||
};
|
||||
|
||||
connectionStateRef.current = {
|
||||
fromHandle: { type: "source" },
|
||||
};
|
||||
|
||||
if (args?.withMagnetHandle && container) {
|
||||
const handleEl = document.createElement("div");
|
||||
handleEl.setAttribute("data-testid", "custom-line-magnet-handle");
|
||||
@@ -128,11 +144,19 @@ describe("CustomConnectionLine", () => {
|
||||
}
|
||||
|
||||
act(() => {
|
||||
const lineProps = {
|
||||
...baseProps,
|
||||
fromHandle: {
|
||||
...baseProps.fromHandle,
|
||||
...(args?.omitFromHandleType ? { type: undefined } : null),
|
||||
},
|
||||
} as ConnectionLineComponentProps;
|
||||
|
||||
root?.render(
|
||||
<CanvasConnectionMagnetismProvider>
|
||||
<svg>
|
||||
<CustomConnectionLine
|
||||
{...baseProps}
|
||||
{...lineProps}
|
||||
connectionStatus={args?.connectionStatus ?? "valid"}
|
||||
/>
|
||||
</svg>
|
||||
@@ -170,6 +194,17 @@ describe("CustomConnectionLine", () => {
|
||||
expect(path.getAttribute("d")).toContain("220");
|
||||
});
|
||||
|
||||
it("still resolves magnet target when fromHandle.type is missing", () => {
|
||||
renderLine({
|
||||
withMagnetHandle: true,
|
||||
omitFromHandleType: true,
|
||||
});
|
||||
|
||||
const path = getPath();
|
||||
expect(path.getAttribute("d")).toContain("300");
|
||||
expect(path.getAttribute("d")).toContain("220");
|
||||
});
|
||||
|
||||
it("strengthens stroke visual feedback while snapped", () => {
|
||||
renderLine();
|
||||
const idlePath = getPath();
|
||||
|
||||
@@ -34,10 +34,26 @@ export default function CanvasHandle({
|
||||
const connection = useConnection();
|
||||
const { activeTarget } = useCanvasConnectionMagnetism();
|
||||
|
||||
const connectionState = connection as {
|
||||
inProgress?: boolean;
|
||||
fromNode?: unknown;
|
||||
toNode?: unknown;
|
||||
fromHandle?: unknown;
|
||||
toHandle?: unknown;
|
||||
};
|
||||
const hasConnectionPayload =
|
||||
connectionState.fromNode !== undefined ||
|
||||
connectionState.toNode !== undefined ||
|
||||
connectionState.fromHandle !== undefined ||
|
||||
connectionState.toHandle !== undefined;
|
||||
const isConnectionDragActive =
|
||||
connectionState.inProgress === true ||
|
||||
(connectionState.inProgress === undefined && hasConnectionPayload);
|
||||
|
||||
const handleId = normalizeHandleId(id);
|
||||
const targetHandleId = normalizeHandleId(activeTarget?.handleId);
|
||||
const isActiveTarget =
|
||||
connection.inProgress &&
|
||||
isConnectionDragActive &&
|
||||
activeTarget !== null &&
|
||||
activeTarget.nodeId === nodeId &&
|
||||
activeTarget.handleType === type &&
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getSmoothStepPath,
|
||||
getStraightPath,
|
||||
type ConnectionLineComponentProps,
|
||||
useConnection,
|
||||
useReactFlow,
|
||||
} from "@xyflow/react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
@@ -52,14 +53,20 @@ export default function CustomConnectionLine({
|
||||
connectionStatus,
|
||||
}: ConnectionLineComponentProps) {
|
||||
const { getNodes, getEdges } = useReactFlow();
|
||||
const connection = useConnection();
|
||||
const { activeTarget, setActiveTarget } = useCanvasConnectionMagnetism();
|
||||
const fromHandleId = fromHandle?.id;
|
||||
const fromNodeId = fromNode?.id;
|
||||
|
||||
const connectionFromHandleType =
|
||||
connection.fromHandle?.type === "source" || connection.fromHandle?.type === "target"
|
||||
? connection.fromHandle.type
|
||||
: null;
|
||||
|
||||
const fromHandleType =
|
||||
fromHandle?.type === "source" || fromHandle?.type === "target"
|
||||
? fromHandle.type
|
||||
: null;
|
||||
: connectionFromHandleType ?? "source";
|
||||
|
||||
const resolvedMagnetTarget = useMemo(() => {
|
||||
if (!fromHandleType || !fromNodeId) {
|
||||
|
||||
Reference in New Issue
Block a user