From e1f864f270a81e2d289c203981b862c7b1658ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 18 Nov 2021 16:29:06 +0100 Subject: [PATCH 1/3] feat(flow): add properties for configuring no-interaction classes --- src/container/EdgeRenderer/index.tsx | 7 ++++- src/container/FlowRenderer/index.tsx | 6 ++++ src/container/GraphView/index.tsx | 8 ++++++ src/container/NodeRenderer/index.tsx | 4 ++- src/container/ReactFlow/index.tsx | 9 ++++++ src/container/ZoomPane/index.tsx | 41 ++++++++++++++++++---------- 6 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 7d6329fc..9d242090 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -1,5 +1,6 @@ import React, { memo, CSSProperties, useCallback } from 'react'; import shallow from 'zustand/shallow'; +import cc from 'classcat'; import { useStore } from '../../store'; import ConnectionLine from '../../components/ConnectionLine/index'; @@ -35,6 +36,7 @@ interface EdgeRendererProps { onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge) => void; onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void; edgeUpdaterRadius?: number; + noPanClassName?: string; } interface EdgeWrapperProps { @@ -63,6 +65,7 @@ interface EdgeWrapperProps { targetNodeX?: number; targetNodeY?: number; targetNodeHandleBounds?: NodeHandleBounds; + noPanClassName?: string; } const Edge = memo( @@ -92,6 +95,7 @@ const Edge = memo( targetNodeX, targetNodeY, targetNodeHandleBounds, + noPanClassName, }: EdgeWrapperProps) => { const sourceHandleId = edge.sourceHandle || null; const targetHandleId = edge.targetHandle || null; @@ -163,7 +167,7 @@ const Edge = memo( { onEdgeUpdate={props.onEdgeUpdate} edgeTypes={props.edgeTypes} connectionMode={connectionMode} + noPanClassName={props.noPanClassName} /> ); })} diff --git a/src/container/FlowRenderer/index.tsx b/src/container/FlowRenderer/index.tsx index 9cfc3216..9a34403d 100644 --- a/src/container/FlowRenderer/index.tsx +++ b/src/container/FlowRenderer/index.tsx @@ -59,6 +59,9 @@ const FlowRenderer = ({ onSelectionDrag, onSelectionDragStop, onSelectionContextMenu, + noDragClassName, + noZoomClassName, + noPanClassName, }: FlowRendererProps) => { const { unsetNodesSelection, resetSelectedElements, nodesSelectionActive } = useStore(selector, shallow); @@ -107,6 +110,9 @@ const FlowRenderer = ({ defaultZoom={defaultZoom} zoomActivationKeyCode={zoomActivationKeyCode} preventScrolling={preventScrolling} + noDragClassName={noDragClassName} + noZoomClassName={noZoomClassName} + noPanClassName={noPanClassName} > {children} diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 34c7c668..e08dd5ee 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -76,6 +76,9 @@ const GraphView = ({ edgeUpdaterRadius, onEdgeUpdateStart, onEdgeUpdateEnd, + noDragClassName, + noZoomClassName, + noPanClassName, }: GraphViewProps) => { useOnLoadHandler(onLoad); @@ -106,6 +109,9 @@ const GraphView = ({ onSelectionDragStop={onSelectionDragStop} onSelectionContextMenu={onSelectionContextMenu} preventScrolling={preventScrolling} + noDragClassName={noDragClassName} + noZoomClassName={noZoomClassName} + noPanClassName={noPanClassName} > diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 3482db92..94c74c0e 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -1,5 +1,6 @@ import React, { memo, useMemo, ComponentType, MouseEvent } from 'react'; import shallow from 'zustand/shallow'; +import cc from 'classcat'; import { useStore } from '../../store'; import { Node, NodeTypesType, ReactFlowState, WrapNodeProps } from '../../types'; @@ -19,6 +20,7 @@ interface NodeRendererProps { onNodeDrag?: (event: MouseEvent, node: Node) => void; onNodeDragStop?: (event: MouseEvent, node: Node) => void; onlyRenderVisibleElements: boolean; + noPanClassName?: string; } const selector = (s: ReactFlowState) => ({ @@ -76,7 +78,7 @@ const NodeRenderer = (props: NodeRendererProps) => { , 'on onEdgeUpdateStart?: (event: ReactMouseEvent, edge: Edge) => void; onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void; edgeUpdaterRadius?: number; + noDragClassName?: string; + noZoomClassName?: string; + noPanClassName?: string; } export type ReactFlowRefType = HTMLDivElement; @@ -212,6 +215,9 @@ const ReactFlow = forwardRef( edgeUpdaterRadius = 10, onNodesChange, onEdgesChange, + noDragClassName = 'nodrag', + noZoomClassName = 'nowheel', + noPanClassName = 'nopan', ...rest }, ref @@ -276,6 +282,9 @@ const ReactFlow = forwardRef( onEdgeUpdateEnd={onEdgeUpdateEnd} edgeUpdaterRadius={edgeUpdaterRadius} defaultMarkerColor={defaultMarkerColor} + noDragClassName={noDragClassName} + noZoomClassName={noZoomClassName} + noPanClassName={noPanClassName} /> @@ -40,7 +43,8 @@ const eventToFlowTransform = (eventTransform: any): FlowTransform => ({ zoom: eventTransform.k, }); -const hasNoWheelClass = (event: any) => event.target.closest('.nowheel'); +const isWrappedWithClass = (event: any, className: string | undefined) => event.target.closest(`.${className}`); +// const hasNoWheelClass = (event: any) => event.target.closest('.nowheel'); const selector = (s: ReactFlowState) => ({ d3Zoom: s.d3Zoom, @@ -69,6 +73,8 @@ const ZoomPane = ({ zoomActivationKeyCode, preventScrolling = true, children, + noZoomClassName, + noPanClassName, }: ZoomPaneProps) => { const store = useStoreApi(); const zoomPane = useRef(null); @@ -106,7 +112,7 @@ const ZoomPane = ({ if (panOnScroll && !zoomActivationKeyPressed) { d3Selection .on('wheel', (event: any) => { - if (hasNoWheelClass(event)) { + if (isWrappedWithClass(event, noZoomClassName)) { return false; } event.preventDefault(); @@ -140,7 +146,7 @@ const ZoomPane = ({ } else if (typeof d3ZoomHandler !== 'undefined') { d3Selection .on('wheel', (event: any) => { - if (!preventScrolling || hasNoWheelClass(event)) { + if (!preventScrolling || isWrappedWithClass(event, noZoomClassName)) { return null; } @@ -158,6 +164,7 @@ const ZoomPane = ({ zoomActivationKeyPressed, zoomOnPinch, preventScrolling, + noZoomClassName, ]); useEffect(() => { @@ -230,22 +237,28 @@ const ZoomPane = ({ return false; } - if (hasNoWheelClass(event) && event.type === 'wheel') { + // if the target element is inside the nowheel class, we prevent zooming + if (isWrappedWithClass(event, noZoomClassName) && event.type === 'wheel') { + return false; + } + + // if the target element is inside the nopan class, we prevent panning + if (isWrappedWithClass(event, noPanClassName) && event.type !== 'wheel') { return false; } // when the target element is a node, we still allow zooming - if ( - (event.target.closest('.react-flow__node') || event.target.closest('.react-flow__edge')) && - event.type !== 'wheel' - ) { - return false; - } + // if ( + // (event.target.closest('.react-flow__node') || event.target.closest('.react-flow__edge')) && + // event.type !== 'wheel' + // ) { + // return false; + // } - // when the target element is a node selection, we still allow zooming - if (event.target.closest('.react-flow__nodesselection') && event.type !== 'wheel') { - return false; - } + // // when the target element is a node selection, we still allow zooming + // if (event.target.closest('.react-flow__nodesselection') && event.type !== 'wheel') { + // return false; + // } if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') { return false; From 004dcc652895f1023430877519a5b8785b511073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 18 Nov 2021 16:53:41 +0100 Subject: [PATCH 2/3] feat(selection): disable pane movement when selection is dragged --- package-lock.json | 4 ++-- package.json | 2 +- src/components/NodesSelection/index.tsx | 5 ++++- src/container/FlowRenderer/index.tsx | 1 + 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1756d2e2..9299f831 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.14", + "version": "10.0.0-next.17", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-flow-renderer", - "version": "10.0.0-next.14", + "version": "10.0.0-next.17", "license": "MIT", "dependencies": { "@babel/runtime": "^7.15.4", diff --git a/package.json b/package.json index b2fdbe7a..1133fcf0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-flow-renderer", - "version": "10.0.0-next.14", + "version": "10.0.0-next.17", "engines": { "node": ">=12" }, diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index 95fac2c0..b75c5ea6 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -5,6 +5,7 @@ import React, { useMemo, useCallback, useRef, MouseEvent } from 'react'; import ReactDraggable, { DraggableData } from 'react-draggable'; +import cc from 'classcat'; import { useStore } from '../../store'; import { Node, ReactFlowState } from '../../types'; @@ -14,6 +15,7 @@ export interface NodesSelectionProps { onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void; onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void; onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void; + noPanClassName?: string; } // @TODO: work with nodeInternals instead of converting it to an array const selector = (s: ReactFlowState) => ({ @@ -33,6 +35,7 @@ export default ({ onSelectionDrag, onSelectionDragStop, onSelectionContextMenu, + noPanClassName, }: NodesSelectionProps) => { const { transform, selectedNodesBbox, selectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } = useStore(selector); @@ -105,7 +108,7 @@ export default ({ } return ( -
+
)}
Date: Thu, 18 Nov 2021 18:01:20 +0100 Subject: [PATCH 3/3] chore(types): enforce defaultProps inside react flow component --- src/components/Nodes/wrapNode.tsx | 5 ++++- src/container/FlowRenderer/index.tsx | 6 ++---- src/container/GraphView/index.tsx | 8 ++++++-- src/container/NodeRenderer/index.tsx | 8 +++++--- src/container/ReactFlow/index.tsx | 9 +++++---- src/container/ZoomPane/index.tsx | 16 +++++++--------- src/types/nodes.ts | 2 ++ 7 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 15c0e7c1..20348106 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -50,6 +50,8 @@ export default (NodeComponent: ComponentType) => { dragHandle, zIndex, isParent, + noPanClassName, + noDragClassName, }: WrapNodeProps) => { const { addSelectedElements, @@ -217,6 +219,7 @@ export default (NodeComponent: ComponentType) => { const nodeClasses = cc([ 'react-flow__node', `react-flow__node-${type}`, + noPanClassName, className, { selected, @@ -232,7 +235,7 @@ export default (NodeComponent: ComponentType) => { onStop={onDragStop} scale={scale} disabled={!isDraggable} - cancel=".nodrag" + cancel={`.${noDragClassName}`} nodeRef={nodeElement} grid={grid} enableUserSelectHack={false} diff --git a/src/container/FlowRenderer/index.tsx b/src/container/FlowRenderer/index.tsx index 8b8d3088..458e9c59 100644 --- a/src/container/FlowRenderer/index.tsx +++ b/src/container/FlowRenderer/index.tsx @@ -59,8 +59,7 @@ const FlowRenderer = ({ onSelectionDrag, onSelectionDragStop, onSelectionContextMenu, - noDragClassName, - noZoomClassName, + noWheelClassName, noPanClassName, }: FlowRendererProps) => { const { unsetNodesSelection, resetSelectedElements, nodesSelectionActive } = useStore(selector, shallow); @@ -110,8 +109,7 @@ const FlowRenderer = ({ defaultZoom={defaultZoom} zoomActivationKeyCode={zoomActivationKeyCode} preventScrolling={preventScrolling} - noDragClassName={noDragClassName} - noZoomClassName={noZoomClassName} + noWheelClassName={noWheelClassName} noPanClassName={noPanClassName} > {children} diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index e08dd5ee..5561833d 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -20,6 +20,9 @@ export interface GraphViewProps extends Omit { useOnLoadHandler(onLoad); @@ -110,7 +113,7 @@ const GraphView = ({ onSelectionContextMenu={onSelectionContextMenu} preventScrolling={preventScrolling} noDragClassName={noDragClassName} - noZoomClassName={noZoomClassName} + noWheelClassName={noWheelClassName} noPanClassName={noPanClassName} > @@ -147,6 +150,7 @@ const GraphView = ({ selectNodesOnDrag={selectNodesOnDrag} onlyRenderVisibleElements={onlyRenderVisibleElements} noPanClassName={noPanClassName} + noDragClassName={noDragClassName} /> diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index c43bb847..a60fdb88 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -1,6 +1,5 @@ import React, { memo, useMemo, ComponentType, MouseEvent } from 'react'; import shallow from 'zustand/shallow'; -import cc from 'classcat'; import { useStore } from '../../store'; import { Node, NodeTypesType, ReactFlowState, WrapNodeProps } from '../../types'; @@ -18,7 +17,8 @@ interface NodeRendererProps { onNodeDrag?: (event: MouseEvent, node: Node) => void; onNodeDragStop?: (event: MouseEvent, node: Node) => void; onlyRenderVisibleElements: boolean; - noPanClassName?: string; + noPanClassName: string; + noDragClassName: string; } const selector = (s: ReactFlowState) => ({ @@ -83,7 +83,7 @@ const NodeRenderer = (props: NodeRendererProps) => { { dragHandle={node.dragHandle} zIndex={internals?.z || 0} isParent={!!internals?.isParent} + noDragClassName={props.noDragClassName} + noPanClassName={props.noPanClassName} /> ); })} diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index 66a007f1..87c3bbfb 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -5,6 +5,7 @@ import React, { MouseEvent as ReactMouseEvent, WheelEvent, forwardRef, + FunctionComponent, } from 'react'; import cc from 'classcat'; @@ -130,7 +131,7 @@ export interface ReactFlowProps extends Omit, 'on onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge) => void; edgeUpdaterRadius?: number; noDragClassName?: string; - noZoomClassName?: string; + noWheelClassName?: string; noPanClassName?: string; } @@ -139,7 +140,7 @@ export type ReactFlowRefType = HTMLDivElement; const initSnapGrid: [number, number] = [15, 15]; const initDefaultPosition: [number, number] = [0, 0]; -const ReactFlow = forwardRef( +const ReactFlow: FunctionComponent = forwardRef( ( { nodes = [], @@ -216,7 +217,7 @@ const ReactFlow = forwardRef( onNodesChange, onEdgesChange, noDragClassName = 'nodrag', - noZoomClassName = 'nowheel', + noWheelClassName = 'nowheel', noPanClassName = 'nopan', ...rest }, @@ -283,7 +284,7 @@ const ReactFlow = forwardRef( edgeUpdaterRadius={edgeUpdaterRadius} defaultMarkerColor={defaultMarkerColor} noDragClassName={noDragClassName} - noZoomClassName={noZoomClassName} + noWheelClassName={noWheelClassName} noPanClassName={noPanClassName} /> @@ -44,7 +43,6 @@ const eventToFlowTransform = (eventTransform: any): FlowTransform => ({ }); const isWrappedWithClass = (event: any, className: string | undefined) => event.target.closest(`.${className}`); -// const hasNoWheelClass = (event: any) => event.target.closest('.nowheel'); const selector = (s: ReactFlowState) => ({ d3Zoom: s.d3Zoom, @@ -73,7 +71,7 @@ const ZoomPane = ({ zoomActivationKeyCode, preventScrolling = true, children, - noZoomClassName, + noWheelClassName, noPanClassName, }: ZoomPaneProps) => { const store = useStoreApi(); @@ -112,7 +110,7 @@ const ZoomPane = ({ if (panOnScroll && !zoomActivationKeyPressed) { d3Selection .on('wheel', (event: any) => { - if (isWrappedWithClass(event, noZoomClassName)) { + if (isWrappedWithClass(event, noWheelClassName)) { return false; } event.preventDefault(); @@ -146,7 +144,7 @@ const ZoomPane = ({ } else if (typeof d3ZoomHandler !== 'undefined') { d3Selection .on('wheel', (event: any) => { - if (!preventScrolling || isWrappedWithClass(event, noZoomClassName)) { + if (!preventScrolling || isWrappedWithClass(event, noWheelClassName)) { return null; } @@ -164,7 +162,7 @@ const ZoomPane = ({ zoomActivationKeyPressed, zoomOnPinch, preventScrolling, - noZoomClassName, + noWheelClassName, ]); useEffect(() => { @@ -238,7 +236,7 @@ const ZoomPane = ({ } // if the target element is inside the nowheel class, we prevent zooming - if (isWrappedWithClass(event, noZoomClassName) && event.type === 'wheel') { + if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') { return false; } diff --git a/src/types/nodes.ts b/src/types/nodes.ts index fe11ef48..ba79bf09 100644 --- a/src/types/nodes.ts +++ b/src/types/nodes.ts @@ -82,6 +82,8 @@ export interface WrapNodeProps { dragHandle?: string; zIndex: number; isParent: boolean; + noPanClassName: string; + noDragClassName: string; } export type NodeHandleBounds = {