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