Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(edge-dropdown-menu): create edge component with a dropdown #596

Closed
wants to merge 3 commits into from
Closed
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
10 changes: 10 additions & 0 deletions apps/ui-components/app/components/edge-dropdown-menu/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import DemoWrapper from "@/components/demo-wrapper";
import Demo from "@/registry/components/edge-dropdown-menu/demo";

export default function DemoPage() {
return (
<DemoWrapper>
<Demo />
</DemoWrapper>
);
}
45 changes: 45 additions & 0 deletions apps/ui-components/registry/components/edge-dropdown-menu/demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import { Background, ReactFlow } from "@xyflow/react";
import { EdgeDropdownMenu } from "@/registry/components/edge-dropdown-menu";

const defaultNodes = [
{
id: "1",
position: { x: 200, y: 200 },
data: { label: "Node" },
},
{
id: "2",
position: { x: 500, y: 500 },
data: { label: "Node" },
},
];

const defaultEdges = [
{
id: "e1-2",
source: "1",
target: "2",
type: "edgeDropdownMenu",
},
];

const edgeTypes = {
edgeDropdownMenu: EdgeDropdownMenu,
};

export default function Demo() {
return (
<div className="h-full w-full">
<ReactFlow
defaultNodes={defaultNodes}
defaultEdges={defaultEdges}
edgeTypes={edgeTypes}
fitView
>
<Background />
</ReactFlow>
</div>
);
}
119 changes: 119 additions & 0 deletions apps/ui-components/registry/components/edge-dropdown-menu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React, { useEffect, useRef, useState } from "react";
import {
BaseEdge,
EdgeLabelRenderer,
EdgeProps,
getBezierPath,
} from "@xyflow/react";

import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";

export function EdgeDropdownMenu({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
}: EdgeProps) {
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);

const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});

const toggleDropdownVisibility = (
event: React.MouseEvent<HTMLButtonElement>,
) => {
event.stopPropagation();
setIsDropdownVisible((prev) => !prev);
};

const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setIsDropdownVisible(false);
}
};

useEffect(() => {
if (isDropdownVisible) {
document.addEventListener("click", handleClickOutside);
} else {
document.removeEventListener("click", handleClickOutside);
}
return () => {
document.removeEventListener("click", handleClickOutside);
};
}, [isDropdownVisible]);

return (
<>
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{ ...style, pointerEvents: "auto" }}
/>

<EdgeLabelRenderer>
<div
className="nodrag nopan pointer-events-auto absolute"
style={{
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
}}
>
<Button
onClick={toggleDropdownVisibility}
size="icon"
variant="secondary"
>
+
</Button>
</div>

{isDropdownVisible && (
<div
ref={menuRef}
style={{
position: "absolute",
top: `${labelY}px`,
left: `${labelX}px`,
zIndex: 1000,
transform: "translate(-50%, -50%)",
pointerEvents: "auto",
}}
>
<DropdownMenu open>
<DropdownMenuTrigger />
<DropdownMenuContent>
<DropdownMenuItem
onClick={() => window.alert("New Node Created")}
>
Create New Node
</DropdownMenuItem>
<DropdownMenuItem onClick={() => window.alert("Node Deleted")}>
Delete Node
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</EdgeLabelRenderer>
</>
);
}

EdgeDropdownMenu.displayName = "EdgeDropdownMenu";
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "edge-dropdown-menu",
"type": "registry:component",
"dependencies": ["@xyflow/react"],
"devDependencies": [],
"registryDependencies": ["dropdown-menu", "button"],
"files": [
{
"type": "registry:component",
"path": "edge-dropdown-menu.tsx"
}
],
"tailwind": {},
"cssVars": {},
"meta": {}
}
1 change: 1 addition & 0 deletions sites/reactflow.dev/src/pages/components/edges/_meta.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export default {
'button-edge': 'Edge with Button',
'edge-dropdown-menu': 'Edge with Dropdown Menu',
'data-edge': 'Edge with Node Data',
'animated-svg-edge': 'Animated SVG Edge',
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
title: Edge Dropdown Menu
description: A custom edge component with a dropdown menu.
---

import UiComponentViewer from '@/components/ui-component-viewer';
import fetchShadcnComponent from '@/utils/get-static-props/fetch-shadcn-component';

export const getStaticProps = fetchShadcnComponent('edge-dropdown-menu');

# Edge Dropdown Menu

A custom edge with a dropdown menu that appears when a button is clicked.

<UiComponentViewer />