diff --git a/examples/vite-app/src/examples/NodeToolbar/SelectedNodesToolbar.tsx b/examples/vite-app/src/examples/NodeToolbar/SelectedNodesToolbar.tsx
new file mode 100644
index 00000000..9c0db9cc
--- /dev/null
+++ b/examples/vite-app/src/examples/NodeToolbar/SelectedNodesToolbar.tsx
@@ -0,0 +1,13 @@
+import { NodeToolbar, useNodes } from 'reactflow';
+
+export default function SelectedNodesToolbar() {
+ const nodes = useNodes();
+ const selectedNodeIds = nodes.filter((node) => node.selected).map((node) => node.id);
+ const isVisible = selectedNodeIds.length > 1;
+
+ return (
+
+
+
+ );
+}
diff --git a/examples/vite-app/src/examples/NodeToolbar/index.tsx b/examples/vite-app/src/examples/NodeToolbar/index.tsx
index 9a5815df..601d8618 100644
--- a/examples/vite-app/src/examples/NodeToolbar/index.tsx
+++ b/examples/vite-app/src/examples/NodeToolbar/index.tsx
@@ -11,6 +11,7 @@ import ReactFlow, {
} from 'reactflow';
import CustomNode from './CustomNode';
+import SelectedNodesToolbar from './SelectedNodesToolbar';
const nodeTypes: NodeTypes = {
custom: CustomNode,
@@ -79,6 +80,7 @@ export default function NodeToolbarExample() {
+
);
}
diff --git a/packages/node-toolbar/src/NodeToolbar.tsx b/packages/node-toolbar/src/NodeToolbar.tsx
index caadf257..66ebd618 100644
--- a/packages/node-toolbar/src/NodeToolbar.tsx
+++ b/packages/node-toolbar/src/NodeToolbar.tsx
@@ -15,9 +15,7 @@ import shallow from 'zustand/shallow';
import NodeToolbarPortal from './NodeToolbarPortal';
import { NodeToolbarProps } from './types';
-type SelectedNode = Node | undefined;
-
-const nodeEqualityFn = (a: SelectedNode, b: SelectedNode) =>
+const nodeEqualityFn = (a: Node | undefined, b: Node | undefined) =>
a?.positionAbsolute?.x === b?.positionAbsolute?.x &&
a?.positionAbsolute?.y === b?.positionAbsolute?.y &&
a?.width === b?.width &&
@@ -25,6 +23,10 @@ const nodeEqualityFn = (a: SelectedNode, b: SelectedNode) =>
a?.selected === b?.selected &&
a?.[internalsSymbol]?.z === b?.[internalsSymbol]?.z;
+const nodesEqualityFn = (a: Node[], b: Node[]) => {
+ return a.length === b.length && a.every((node, i) => nodeEqualityFn(node, b[i]));
+};
+
const storeSelector = (state: ReactFlowState) => ({
transform: state.transform,
nodeOrigin: state.nodeOrigin,
@@ -70,21 +72,34 @@ function NodeToolbar({
offset = 10,
...rest
}: NodeToolbarProps) {
- const nodeSelector = useCallback((state: ReactFlowState): SelectedNode => state.nodeInternals.get(nodeId), [nodeId]);
- const node = useStore(nodeSelector, nodeEqualityFn);
+ const nodeIds: string[] = typeof nodeId === 'string' ? [nodeId] : nodeId;
+ const nodesSelector = useCallback(
+ (state: ReactFlowState): Node[] =>
+ nodeIds.reduce((acc, id) => {
+ const node = state.nodeInternals.get(id);
+ if (node) {
+ acc.push(node);
+ }
+ return acc;
+ }, [] as Node[]),
+ [nodeIds]
+ );
+ const nodes = useStore(nodesSelector, nodesEqualityFn);
const { transform, nodeOrigin, selectedNodesCount } = useStore(storeSelector, shallow);
- const isActive = typeof isVisible === 'boolean' ? isVisible : node?.selected && selectedNodesCount === 1;
+ const isActive =
+ typeof isVisible === 'boolean' ? isVisible : nodes.length === 1 && nodes[0].selected && selectedNodesCount === 1;
- if (!isActive || !node) {
+ if (!isActive || !nodes.length) {
return null;
}
- const nodeRect: Rect = getRectOfNodes([node], nodeOrigin);
+ const nodeRect: Rect = getRectOfNodes(nodes, nodeOrigin);
+ const zIndex: number = Math.max(...nodes.map((node) => (node[internalsSymbol]?.z || 1) + 1));
const wrapperStyle: CSSProperties = {
position: 'absolute',
transform: getTransform(nodeRect, transform, position, offset),
- zIndex: (node[internalsSymbol]?.z || 1) + 1,
+ zIndex,
...style,
};
diff --git a/packages/node-toolbar/src/types.ts b/packages/node-toolbar/src/types.ts
index 1e6576b6..a9f547ad 100644
--- a/packages/node-toolbar/src/types.ts
+++ b/packages/node-toolbar/src/types.ts
@@ -2,7 +2,7 @@ import { Position } from '@reactflow/core';
import type { HTMLAttributes } from 'react';
export type NodeToolbarProps = HTMLAttributes & {
- nodeId: string;
+ nodeId: string | string[];
isVisible?: boolean;
position?: Position;
offset?: number;