Skip to content

Commit d52f668

Browse files
Merge pull request xyflow#129 from wbkd/guide/context-menu
✨ Create a new node context menu example.
2 parents e87b0b3 + bf0d0d0 commit d52f668

File tree

6 files changed

+152
-15
lines changed

6 files changed

+152
-15
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
title: Node Context Menus
3+
hide_table_of_contents: true
4+
---
5+
6+
import CodeViewer from '/src/components/CodeViewer';
7+
8+
The [`onNodeContextMenu`](/docs/api/react-flow-props/#onnodecontextmenu) event
9+
can be used to show a custom menu when right-clicking a node. This example shows
10+
a simple menu with buttons to duplicate or delete the clicked node.
11+
12+
<CodeViewer
13+
codePath="example-flows/ContextMenu"
14+
additionalFiles={['ContextMenu.js', 'nodes-edges.js', 'style.css']}
15+
/>

src/components/CodeViewer/example-flows/BaseStyle/index.js

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,15 @@ import ReactFlow, {
66
useNodesState,
77
useEdgesState,
88
addEdge,
9-
Position,
109
} from 'reactflow';
1110

1211
import 'reactflow/dist/base.css';
1312

14-
const nodeDefaults = {
15-
sourcePosition: Position.Right,
16-
targetPosition: Position.Left,
17-
};
18-
1913
const initialNodes = [
20-
{
21-
id: '1',
22-
position: { x: 0, y: 150 },
23-
data: { label: 'base style 1' },
24-
...nodeDefaults,
25-
},
26-
{ id: '2', position: { x: 250, y: 0 }, data: { label: 'base style 2' }, ...nodeDefaults },
27-
{ id: '3', position: { x: 250, y: 150 }, data: { label: 'base style 3' }, ...nodeDefaults },
28-
{ id: '4', position: { x: 250, y: 300 }, data: { label: 'base style 4' }, ...nodeDefaults },
14+
{ id: '1', position: { x: 0, y: 150 }, data: { label: 'base style 1' } },
15+
{ id: '2', position: { x: 250, y: 0 }, data: { label: 'base style 2' } },
16+
{ id: '3', position: { x: 250, y: 150 }, data: { label: 'base style 3' } },
17+
{ id: '4', position: { x: 250, y: 300 }, data: { label: 'base style 4' } },
2918
];
3019

3120
const initialEdges = [
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React, { useCallback } from 'react';
2+
import { useReactFlow } from 'reactflow';
3+
4+
export default function ContextMenu({ id, top, left, right, bottom, ...props }) {
5+
const { getNode, setNodes, addNodes, setEdges } = useReactFlow();
6+
const duplicateNode = useCallback(() => {
7+
const node = getNode(id);
8+
const position = {
9+
x: node.position.x + 50,
10+
y: node.position.y + 50,
11+
};
12+
13+
addNodes({ ...node, id: `${node.id}-copy`, position });
14+
}, [id, getNode, addNodes]);
15+
16+
const deleteNode = useCallback(() => {
17+
setNodes((nodes) => nodes.filter((node) => node.id !== id));
18+
setEdges((edges) => edges.filter((edge) => edge.source !== id));
19+
}, [id, setNodes, setEdges]);
20+
21+
return (
22+
<div style={{ top, left, right, bottom }} className="context-menu" {...props}>
23+
<p style={{ margin: '0.5em' }}>
24+
<small>node: {id}</small>
25+
</p>
26+
<button onClick={duplicateNode}>duplicate</button>
27+
<button onClick={deleteNode}>delete</button>
28+
</div>
29+
);
30+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React, { useCallback, useRef, useState } from 'react';
2+
import ReactFlow, { Background, useNodesState, useEdgesState, addEdge } from 'reactflow';
3+
4+
import { initialNodes, initialEdges } from './nodes-edges';
5+
import ContextMenu from './ContextMenu';
6+
7+
import 'reactflow/dist/style.css';
8+
import './style.css';
9+
10+
const Flow = () => {
11+
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
12+
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
13+
const [menu, setMenu] = useState(null);
14+
const ref = useRef(null);
15+
16+
const onConnect = useCallback((params) => setEdges((els) => addEdge(params, els)), [setEdges]);
17+
18+
const onNodeContextMenu = useCallback(
19+
(event, node) => {
20+
// Prevent native context menu from showing
21+
event.preventDefault();
22+
23+
// Calculate position of the context menu. We want to make sure it
24+
// doesn't get positioned off-screen.
25+
const pane = ref.current.getBoundingClientRect();
26+
setMenu({
27+
id: node.id,
28+
top: event.clientY < pane.height - 200 && event.clientY,
29+
left: event.clientX < pane.width - 200 && event.clientX,
30+
right: event.clientX >= pane.width - 200 && pane.width - event.clientX,
31+
bottom: event.clientY >= pane.height - 200 && pane.height - event.clientY,
32+
});
33+
},
34+
[setMenu]
35+
);
36+
37+
// Close the context menu if it's open whenever the window is clicked.
38+
const onPaneClick = useCallback(() => setMenu(null), [setMenu]);
39+
40+
return (
41+
<ReactFlow
42+
ref={ref}
43+
nodes={nodes}
44+
edges={edges}
45+
onNodesChange={onNodesChange}
46+
onEdgesChange={onEdgesChange}
47+
onConnect={onConnect}
48+
onPaneClick={onPaneClick}
49+
onNodeContextMenu={onNodeContextMenu}
50+
fitView
51+
>
52+
<Background />
53+
{menu && <ContextMenu onClick={onPaneClick} {...menu} />}
54+
</ReactFlow>
55+
);
56+
};
57+
58+
export default Flow;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export const initialNodes = [
2+
{ id: '1', position: { x: 175, y: 0 }, data: { label: 'a' } },
3+
{ id: '2', position: { x: 0, y: 250 }, data: { label: 'b' } },
4+
{ id: '3', position: { x: 175, y: 250 }, data: { label: 'c' } },
5+
{ id: '4', position: { x: 350, y: 250 }, data: { label: 'd' } },
6+
];
7+
8+
export const initialEdges = [
9+
{
10+
id: 'e1-2',
11+
source: '1',
12+
target: '2',
13+
},
14+
{
15+
id: 'e1-3',
16+
source: '1',
17+
target: '3',
18+
},
19+
{
20+
id: 'e1-4',
21+
source: '1',
22+
target: '4',
23+
},
24+
];
25+
26+
export default { initialNodes, initialEdges };
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.context-menu {
2+
background: white;
3+
border-style: solid;
4+
box-shadow: 10px 19px 20px rgba(0, 0, 0, 10%);
5+
position: absolute;
6+
z-index: 10;
7+
}
8+
9+
.context-menu button {
10+
border: none;
11+
display: block;
12+
padding: 0.5em;
13+
text-align: left;
14+
width: 100%;
15+
}
16+
17+
.context-menu button:hover {
18+
background: white;
19+
}

0 commit comments

Comments
 (0)