I am trying to add a context menu component to my React flow app so when you right click on a node a context menu shows up next to it. I am using React Flow and have been trying to convert the
example they give in JavaScript to typescript.
the context component works with no errors, but after I add this logic for positioning the menu, I am getting this error message Type 'void' is not assignable to type 'string | number | undefined'. The expected type comes from this index signature.
this is my App.tsx
import { ReactFlow, type Node, useNodesState, addEdge, useEdgesState, type Edge } from '@xyflow/react';
import {Controls, Background, BackgroundVariant} from '@xyflow/react'
import ContextMenu, { type Menu } from './ContextMenu';
import React from 'react'
import { useCallback, useRef, useState} from 'react';
import ExtendableNode from './extendable-node'
const nodeTypes = {
extendableNode: ExtendableNode
};
const initialNodes: Node[] = [
{
id: '1',
position: {x: 10, y: 10},
data: {label: "default label"
},
type: 'extendableNode',
}
];
const initialEdges: Edge[] = []
//const NewNodeId = () => `randomnode_${new Date()}`
function Flow() {
const [nodes, setNodes , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const [menu, setMenu] = useState<Menu | null>(null);
const ref = useRef<HTMLDivElement | null>(null);
const onNodeContextMenu = useCallback(
(event: React.MouseEvent, node: { id: string }) => {
// Prevent native context menu from showing
event.preventDefault();
// Calculate position of the context menu so it doesn't get rendered off screen
const pane = ref.current?.getBoundingClientRect();
if (pane) {
setMenu({
id: node.id,
top: event.clientY < pane.height - 200 ? event.clientY : false,
left: event.clientX < pane.width - 200 ? event.clientX : false,
right: event.clientX >= pane.width - 200 ? pane.width - event.clientX : false,
bottom: event.clientY >= pane.height - 200 ? pane.height - event.clientY : false,
});
}
},
[setMenu],
);
// Close the context menu if it's open whenever the window is clicked.
const onPaneClick = useCallback(() => setMenu(null), [setMenu]);
return (
<div className="h-screen w-screen p-8 bg-gray-50 rounded-xl">
<ReactFlow
nodes={nodes}
nodeTypes={nodeTypes}
onNodesChange={onNodesChange}
onPaneClick={onPaneClick}
onNodeContextMenu={onNodeContextMenu}
fitView>
{menu && <ContextMenu onClick={onPaneClick} {...menu} />}
<Background color="#ccc" variant={BackgroundVariant.Cross} />
<Controls/>
</ReactFlow>
</div>
);
}
export function App() {
return <Flow />;
}
This is the interface
import {type FC, useCallback } from 'react';
import { useReactFlow, type Node } from '@xyflow/react';
interface ContextMenuProps {
id: string;
top: number;
left: number;
right: number;
bottom: number;
[key: string]: string | number | undefined ;
}
const ContextMenu: FC<ContextMenuProps> = ({
id,
top,
left,
right,
bottom,
...props
}) =>
{
const { getNode, setNodes, addNodes, setEdges } = useReactFlow();
const duplicateNode = useCallback(() => {
const node: Node | undefined = getNode(id)!;
if(node) {
const position = {
x: node.position.x + 50,
y: node.position.y + 50,
};
addNodes({
...node,
selected: false,
dragging: false,
id: `${node.id}-copy`,
position,
});
}
}, [id, getNode, addNodes]);
const deleteNode = useCallback(() => {
setNodes((nodes) => nodes.filter((node) => node.id !== id));
setEdges((edges) => edges.filter((edge) => edge.source !== id));
}, [id, setNodes, setEdges]);
return (
<div
style={{ top, left, right, bottom }}
className="context-menu"
{...props}
>
<p style={{ margin: '0.5em' }}>
<small>node: {id}</small>
</p>
<button onClick={duplicateNode}>duplicate</button>
<button onClick={deleteNode}>delete</button>
</div>
);
}
export default ContextMenu;
export type Menu = {
id: string;
top: number | boolean;
left: number | boolean;
right: number | boolean;
bottom: number | boolean;
};
I know typescript has strict expectations around being explicit with return types so I tried to add the onclick to the index signature.
interface ContextMenuProps {
id: string;
top: number;
left: number;
right: number;
bottom: number;
onClick: ()=> void;
[key: string]: string | number | undefined | (()=>void);
}
that then lead to another error type '() => void'.The expected type comes from property 'onClick' which is declared here on type 'IntrinsicAttributes & ContextMenuProps'*
I feel I must have made a glaring mistake that I am missing.