Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/flow-node-id-prop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@cloudflare/kumo': minor
---

Add optional `id` prop to `Flow.Node` for stable node identification and connector test IDs

2 changes: 2 additions & 0 deletions packages/kumo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@types/svg-path-parser": "^1.1.6",
"@vitejs/plugin-react": "^5.1.4",
"@vitest/browser-playwright": "^4.0.18",
"@vitest/ui": "catalog:",
Expand All @@ -461,6 +462,7 @@
"playwright": "catalog:",
"plop": "4.0.4",
"rollup-plugin-preserve-directives": "0.4.0",
"svg-path-parser": "^1.1.0",
"tailwindcss": "catalog:",
"ts-json-schema-generator": "2.4.0",
"tsx": "catalog:",
Expand Down
9 changes: 9 additions & 0 deletions packages/kumo/src/components/flow/connectors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export interface Connector {
isBottom?: boolean;
disabled?: boolean;
single?: boolean;
/** Id of the source node this connector originates from. */
fromId?: string;
/** Id of the target node this connector points to. */
toId?: string;
}

type ConnectorsProps = {
Expand Down Expand Up @@ -200,6 +204,11 @@ export const Connectors = forwardRef<SVGSVGElement, ConnectorsProps>(
strokeWidth="2"
markerEnd={`url(#${id})`}
data-index={index}
data-testid={
connector.fromId && connector.toId
? `${connector.fromId}-${connector.toId}`
: undefined
}
/>
</g>
);
Expand Down
16 changes: 12 additions & 4 deletions packages/kumo/src/components/flow/diagram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,12 @@ export function FlowDiagram({
onPan={handlePan}
onPanEnd={handlePanEnd}
>
<motion.div ref={contentRef} className="w-max mx-auto" style={{ x, y }}>
<motion.div
data-testid="flow-contents"
ref={contentRef}
className="w-max mx-auto"
style={{ x, y }}
>
<FlowNodeList>{children}</FlowNodeList>
</motion.div>

Expand Down Expand Up @@ -317,7 +322,8 @@ export type NodeData = {

export const useNodeGroup = () => useDescendants<NodeData>();

export const useNode = (props: NodeData) => useDescendantIndex<NodeData>(props);
export const useNode = (props: NodeData, id?: string) =>
useDescendantIndex<NodeData>(props, id);

/**
* Hook to optionally register as a node if within a parent descendants context.
Expand Down Expand Up @@ -347,7 +353,7 @@ export const useOptionalNode = (props: NodeData) => {
unregisterRef.current = null;
}
};
}, [id, renderOrder, props, parentContext]);
}, [id, renderOrder, props, parentContext?.register]);

if (!parentContext) return null;

Expand Down Expand Up @@ -395,6 +401,8 @@ export function FlowNodeList({ children }: { children: ReactNode }) {
y2: nextRect.top - offsetY + nextRect.height / 2,
disabled: isDisabled,
single: true,
fromId: currentNode.id,
toId: nextNode.id,
});
}
}
Expand All @@ -418,7 +426,7 @@ export function FlowNodeList({ children }: { children: ReactNode }) {
start: startAnchor,
end: endAnchor,
}),
[startAnchor, endAnchor],
[JSON.stringify(startAnchor), JSON.stringify(endAnchor)],
);

// Register with parent context if we're nested (e.g., inside Flow.Parallel)
Expand Down
Loading