diff --git a/examples/react/src/App/routes.ts b/examples/react/src/App/routes.ts
index 024a22ac..6dffe3b0 100644
--- a/examples/react/src/App/routes.ts
+++ b/examples/react/src/App/routes.ts
@@ -1,6 +1,7 @@
import Basic from '../examples/Basic';
import Backgrounds from '../examples/Backgrounds';
import ColorMode from '../examples/ColorMode';
+import ClickDistance from '../examples/ClickDistance';
import ControlledUncontrolled from '../examples/ControlledUncontrolled';
import ControlledViewport from '../examples/ControlledViewport';
import CustomConnectionLine from '../examples/CustomConnectionLine';
@@ -85,6 +86,11 @@ const routes: IRoute[] = [
path: 'cancel-connection',
component: CancelConnection,
},
+ {
+ name: 'Click Distance',
+ path: 'click-distance',
+ component: ClickDistance,
+ },
{
name: 'Controlled/Uncontrolled',
path: 'controlled-uncontrolled',
diff --git a/examples/react/src/examples/ClickDistance/index.tsx b/examples/react/src/examples/ClickDistance/index.tsx
new file mode 100644
index 00000000..97fa015b
--- /dev/null
+++ b/examples/react/src/examples/ClickDistance/index.tsx
@@ -0,0 +1,72 @@
+import { useCallback, useState } from 'react';
+import { ReactFlow, addEdge, Node, Connection, Edge, useNodesState, useEdgesState, Panel } from '@xyflow/react';
+
+const initNodes: Node[] = [
+ {
+ id: '1a',
+ type: 'input',
+ data: { label: 'Node 1' },
+ position: { x: 250, y: 5 },
+ className: 'light',
+ ariaLabel: 'Input Node 1',
+ },
+ {
+ id: '2a',
+ data: { label: 'Node 2' },
+ position: { x: 100, y: 100 },
+ className: 'light',
+ ariaLabel: 'Default Node 2',
+ },
+ {
+ id: '3a',
+ data: { label: 'Node 3' },
+ position: { x: 400, y: 100 },
+ className: 'light',
+ },
+ {
+ id: '4a',
+ data: { label: 'Node 4' },
+ position: { x: 400, y: 200 },
+ className: 'light',
+ },
+];
+
+const initEdges: Edge[] = [
+ { id: 'e1-2', source: '1a', target: '2a', ariaLabel: undefined },
+ { id: 'e1-3', source: '1a', target: '3a' },
+];
+
+const onPaneClick = () => console.log('pane click');
+
+const BasicFlow = () => {
+ const [nodes, setNodes, onNodesChange] = useNodesState(initNodes);
+ const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
+ const [paneClickDistance, setPaneClickDistance] = useState(0);
+
+ const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
+
+ return (
+
+
+ setPaneClickDistance(+evt.target.value)}
+ />
+ click distance: {paneClickDistance}
+
+
+ );
+};
+
+export default BasicFlow;
diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx
index 674eebd1..120db624 100644
--- a/packages/react/src/components/StoreUpdater/index.tsx
+++ b/packages/react/src/components/StoreUpdater/index.tsx
@@ -67,6 +67,7 @@ const reactFlowFieldsToTrack = [
'onBeforeDelete',
'debug',
'autoPanSpeed',
+ 'paneClickDistance',
] as const;
type ReactFlowFieldsToTrack = (typeof reactFlowFieldsToTrack)[number];
@@ -89,6 +90,7 @@ const selector = (s: ReactFlowState) => ({
setNodeExtent: s.setNodeExtent,
reset: s.reset,
setDefaultNodesAndEdges: s.setDefaultNodesAndEdges,
+ setPaneClickDistance: s.setPaneClickDistance,
});
const initPrevValues = {
@@ -102,6 +104,7 @@ const initPrevValues = {
elementsSelectable: true,
noPanClassName: 'nopan',
rfId: '1',
+ paneClickDistance: 0,
};
export function StoreUpdater(
@@ -116,6 +119,7 @@ export function StoreUpdater();
@@ -146,6 +150,7 @@ export function StoreUpdater({
onPaneMouseLeave,
onPaneContextMenu,
onPaneScroll,
+ paneClickDistance,
deleteKeyCode,
selectionKeyCode,
selectionOnDrag,
@@ -101,6 +102,7 @@ function FlowRendererComponent({
noPanClassName={noPanClassName}
onViewportChange={onViewportChange}
isControlledViewport={isControlledViewport}
+ paneClickDistance={paneClickDistance}
>
> & {
rfId: string;
@@ -84,6 +85,7 @@ function GraphViewComponent(
onPaneMouseLeave,
onPaneScroll,
onPaneContextMenu,
+ paneClickDistance = 0,
children,
onReconnect,
onReconnectStart,
@@ -200,6 +201,7 @@ function ReactFlow(
onPaneMouseLeave={onPaneMouseLeave}
onPaneScroll={onPaneScroll}
onPaneContextMenu={onPaneContextMenu}
+ paneClickDistance={paneClickDistance}
onSelectionContextMenu={onSelectionContextMenu}
onSelectionStart={onSelectionStart}
onSelectionEnd={onSelectionEnd}
@@ -277,6 +279,7 @@ function ReactFlow(
selectNodesOnDrag={selectNodesOnDrag}
nodeDragThreshold={nodeDragThreshold}
onBeforeDelete={onBeforeDelete}
+ paneClickDistance={paneClickDistance}
debug={debug}
/>
diff --git a/packages/react/src/container/ZoomPane/index.tsx b/packages/react/src/container/ZoomPane/index.tsx
index f6e7a79c..3d4003e0 100644
--- a/packages/react/src/container/ZoomPane/index.tsx
+++ b/packages/react/src/container/ZoomPane/index.tsx
@@ -47,6 +47,7 @@ export function ZoomPane({
noPanClassName,
onViewportChange,
isControlledViewport,
+ paneClickDistance,
}: ZoomPaneProps) {
const store = useStoreApi();
const zoomPane = useRef(null);
@@ -64,6 +65,7 @@ export function ZoomPane({
maxZoom,
translateExtent,
viewport: defaultViewport,
+ paneClickDistance,
onTransformChange: (transform: Transform) => {
onViewportChange?.({ x: transform[0], y: transform[1], zoom: transform[2] });
diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts
index 062340b7..3d3eab93 100644
--- a/packages/react/src/store/index.ts
+++ b/packages/react/src/store/index.ts
@@ -255,6 +255,9 @@ const createStore = ({
set({ translateExtent });
},
+ setPaneClickDistance: (clickDistance) => {
+ get().panZoom?.setClickDistance(clickDistance);
+ },
resetSelectedElements: () => {
const { edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get();
diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts
index cd46ac22..29919931 100644
--- a/packages/react/src/types/component-props.ts
+++ b/packages/react/src/types/component-props.ts
@@ -223,6 +223,10 @@ export interface ReactFlowProps void;
/** This event handler gets called when mouse leaves the pane */
onPaneMouseLeave?: (event: ReactMouseEvent) => void;
+ /** Distance that the mouse can move between mousedown/up that will trigger a click
+ * @default 0
+ */
+ paneClickDistance?: number;
/** This handler gets called before the user deletes nodes or edges and provides a way to abort the deletion by returning false. */
onBeforeDelete?: OnBeforeDelete;
/** Custom node types to be available in a flow.
diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts
index 0e89d5c6..66bf3c44 100644
--- a/packages/react/src/types/store.ts
+++ b/packages/react/src/types/store.ts
@@ -170,6 +170,7 @@ export type ReactFlowActions = {
panBy: PanBy;
fitView: (options?: FitViewOptions) => Promise;
fitViewSync: (options?: FitViewOptions) => boolean;
+ setPaneClickDistance: (distance: number) => void;
};
export type ReactFlowState = ReactFlowStore<
diff --git a/packages/svelte/src/lib/actions/zoom/index.ts b/packages/svelte/src/lib/actions/zoom/index.ts
index b96e012f..a0c83c31 100644
--- a/packages/svelte/src/lib/actions/zoom/index.ts
+++ b/packages/svelte/src/lib/actions/zoom/index.ts
@@ -35,11 +35,20 @@ type ZoomParams = {
noWheelClassName: string;
userSelectionActive: boolean;
lib: string;
+ paneClickDistance: number;
};
export default function zoom(domNode: Element, params: ZoomParams) {
- const { panZoom, minZoom, maxZoom, initialViewport, viewport, dragging, translateExtent } =
- params;
+ const {
+ panZoom,
+ minZoom,
+ maxZoom,
+ initialViewport,
+ viewport,
+ dragging,
+ translateExtent,
+ paneClickDistance
+ } = params;
const panZoomInstance = XYPanZoom({
domNode,
@@ -47,6 +56,7 @@ export default function zoom(domNode: Element, params: ZoomParams) {
maxZoom,
translateExtent,
viewport: initialViewport,
+ paneClickDistance,
onTransformChange: (transform) =>
viewport.set({ x: transform[0], y: transform[1], zoom: transform[2] }),
onDraggingChange: dragging.set
diff --git a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte
index 74709680..f19e003c 100644
--- a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte
+++ b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte
@@ -79,6 +79,7 @@
export let onbeforedelete: $$Props['onbeforedelete'] = undefined;
export let oninit: $$Props['oninit'] = undefined;
export let nodeOrigin: $$Props['nodeOrigin'] = undefined;
+ export let paneClickDistance: $$Props['paneClickDistance'] = 0;
export let defaultMarkerColor = '#b1b1b7';
@@ -125,7 +126,8 @@
edgeTypes,
minZoom,
maxZoom,
- translateExtent
+ translateExtent,
+ paneClickDistance
});
return () => {
@@ -188,7 +190,8 @@
edgeTypes,
minZoom,
maxZoom,
- translateExtent
+ translateExtent,
+ paneClickDistance
});
$: colorModeClass = useColorModeClass(colorMode);
@@ -225,6 +228,7 @@
zoomOnPinch={zoomOnPinch === undefined ? true : zoomOnPinch}
panOnScroll={panOnScroll === undefined ? false : panOnScroll}
panOnDrag={panOnDrag === undefined ? true : panOnDrag}
+ paneClickDistance={paneClickDistance === undefined ? 0 : paneClickDistance}
>
& {
* @default 1
*/
nodeDragThreshold?: number;
+ /** Distance that the mouse can move between mousedown/up that will trigger a click
+ * @default 0
+ */
+ paneClickDistance?: number;
/** Minimum zoom level
* @default 0.5
*/
diff --git a/packages/svelte/src/lib/container/SvelteFlow/utils.ts b/packages/svelte/src/lib/container/SvelteFlow/utils.ts
index c7166b0c..d7a71a7c 100644
--- a/packages/svelte/src/lib/container/SvelteFlow/utils.ts
+++ b/packages/svelte/src/lib/container/SvelteFlow/utils.ts
@@ -1,7 +1,8 @@
+import type { Writable } from 'svelte/store';
+import type { CoordinateExtent } from '@xyflow/system';
+
import type { SvelteFlowStore } from '$lib/store/types';
import type { EdgeTypes, NodeTypes } from '$lib/types';
-import type { CoordinateExtent } from '@xyflow/system';
-import type { Writable } from 'svelte/store';
// this is helper function for updating the store
// for props where we need to call a specific store action
@@ -12,13 +13,15 @@ export function updateStore(
edgeTypes,
minZoom,
maxZoom,
- translateExtent
+ translateExtent,
+ paneClickDistance
}: {
nodeTypes?: NodeTypes;
edgeTypes?: EdgeTypes;
minZoom?: number;
maxZoom?: number;
translateExtent?: CoordinateExtent;
+ paneClickDistance?: number;
}
) {
if (nodeTypes !== undefined) {
@@ -40,6 +43,10 @@ export function updateStore(
if (translateExtent !== undefined) {
store.setTranslateExtent(translateExtent);
}
+
+ if (paneClickDistance !== undefined) {
+ store.setPaneClickDistance(paneClickDistance);
+ }
}
const getKeys = (obj: T) => Object.keys(obj) as Array;
diff --git a/packages/svelte/src/lib/container/Zoom/Zoom.svelte b/packages/svelte/src/lib/container/Zoom/Zoom.svelte
index 205f0f2d..adb4463d 100644
--- a/packages/svelte/src/lib/container/Zoom/Zoom.svelte
+++ b/packages/svelte/src/lib/container/Zoom/Zoom.svelte
@@ -19,6 +19,7 @@
export let zoomOnPinch: $$Props['zoomOnPinch'];
export let panOnDrag: $$Props['panOnDrag'];
export let panOnScroll: $$Props['panOnScroll'];
+ export let paneClickDistance: $$Props['paneClickDistance'];
const {
viewport,
@@ -68,7 +69,8 @@
noWheelClassName: 'nowheel',
userSelectionActive: !!$selectionRect,
translateExtent: $translateExtent,
- lib: $lib
+ lib: $lib,
+ paneClickDistance
}}
>
diff --git a/packages/svelte/src/lib/container/Zoom/types.ts b/packages/svelte/src/lib/container/Zoom/types.ts
index cf2e15cd..b7b4f29b 100644
--- a/packages/svelte/src/lib/container/Zoom/types.ts
+++ b/packages/svelte/src/lib/container/Zoom/types.ts
@@ -12,4 +12,5 @@ export type ZoomProps = {
zoomOnPinch: boolean;
panOnScroll: boolean;
panOnDrag: boolean | number[];
+ paneClickDistance: number;
};
diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts
index bae08a24..9ed41bc0 100644
--- a/packages/svelte/src/lib/store/index.ts
+++ b/packages/svelte/src/lib/store/index.ts
@@ -248,6 +248,10 @@ export function createStore({
return elementsChanged;
}
+ function setPaneClickDistance(distance: number) {
+ get(store.panZoom)?.setClickDistance(distance);
+ }
+
function unselectNodesAndEdges(params?: { nodes?: Node[]; edges?: Edge[] }) {
const resetNodes = resetSelectedElements(params?.nodes || get(store.nodes));
if (resetNodes) store.nodes.set(get(store.nodes));
@@ -448,6 +452,7 @@ export function createStore({
setMinZoom,
setMaxZoom,
setTranslateExtent,
+ setPaneClickDistance,
unselectNodesAndEdges,
addSelectedNodes,
addSelectedEdges,
diff --git a/packages/svelte/src/lib/store/types.ts b/packages/svelte/src/lib/store/types.ts
index 0d8c01bf..88ee8be0 100644
--- a/packages/svelte/src/lib/store/types.ts
+++ b/packages/svelte/src/lib/store/types.ts
@@ -25,6 +25,7 @@ export type SvelteFlowStoreActions = {
setMinZoom: (minZoom: number) => void;
setMaxZoom: (maxZoom: number) => void;
setTranslateExtent: (extent: CoordinateExtent) => void;
+ setPaneClickDistance: (distance: number) => void;
fitView: (options?: FitViewOptions) => Promise;
updateNodePositions: UpdateNodePositions;
updateNodeInternals: (updates: Map) => void;
diff --git a/packages/system/src/types/panzoom.ts b/packages/system/src/types/panzoom.ts
index 4a126a98..561714b4 100644
--- a/packages/system/src/types/panzoom.ts
+++ b/packages/system/src/types/panzoom.ts
@@ -9,6 +9,7 @@ export type PanZoomParams = {
domNode: Element;
minZoom: number;
maxZoom: number;
+ paneClickDistance: number;
viewport: Viewport;
translateExtent: CoordinateExtent;
onTransformChange: OnTransformChange;
@@ -56,4 +57,5 @@ export type PanZoomInstance = {
scaleTo: (scale: number, options?: PanZoomTransformOptions) => Promise;
scaleBy: (factor: number, options?: PanZoomTransformOptions) => Promise;
syncViewport: (viewport: Viewport) => void;
+ setClickDistance: (distance: number) => void;
};
diff --git a/packages/system/src/xypanzoom/XYPanZoom.ts b/packages/system/src/xypanzoom/XYPanZoom.ts
index 2db56b10..3266b8e7 100644
--- a/packages/system/src/xypanzoom/XYPanZoom.ts
+++ b/packages/system/src/xypanzoom/XYPanZoom.ts
@@ -9,7 +9,7 @@ import {
PanZoomParams,
PanZoomInstance,
} from '../types';
-import { clamp } from '../utils';
+import { clamp, isNumeric } from '../utils';
import { getD3Transition, viewportToTransform, wheelDelta } from './utils';
import {
createPanOnScrollHandler,
@@ -34,6 +34,7 @@ export function XYPanZoom({
domNode,
minZoom,
maxZoom,
+ paneClickDistance,
translateExtent,
viewport,
onPanZoom,
@@ -52,7 +53,10 @@ export function XYPanZoom({
isPanScrolling: false,
};
const bbox = domNode.getBoundingClientRect();
- const d3ZoomInstance = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent);
+ const d3ZoomInstance = zoom()
+ .clickDistance(paneClickDistance)
+ .scaleExtent([minZoom, maxZoom])
+ .translateExtent(translateExtent);
const d3Selection = select(domNode).call(d3ZoomInstance);
setViewportConstrained(
@@ -267,6 +271,20 @@ export function XYPanZoom({
d3ZoomInstance?.translateExtent(translateExtent);
}
+ function setClickDistance(distance: number) {
+ let validNumber = distance;
+
+ if (!isNumeric(distance)) {
+ validNumber = 0;
+ }
+
+ if (validNumber < 0) {
+ validNumber = 0;
+ }
+
+ d3ZoomInstance?.clickDistance(validNumber);
+ }
+
return {
update,
destroy,
@@ -278,5 +296,6 @@ export function XYPanZoom({
setScaleExtent,
setTranslateExtent,
syncViewport,
+ setClickDistance,
};
}