Merge pull request #553 from wbkd/zoom-handling

Zoom handling
This commit is contained in:
Christopher Möller
2020-10-06 14:23:54 +02:00
committed by GitHub
12 changed files with 186 additions and 85 deletions
+2 -2
View File
@@ -509,8 +509,8 @@ The React Flow wrapper has the className `react-flow`. If you want to change the
### Used Class Names
* `.react-flow` - Outer container
* `.react-flow__renderer` - Inner container
* `.react-flow__zoompane` - Zoom & pan pane
* `.react-flow__renderer` - Flow container, used for zooming and panning
* `.react-flow__pane` - Background Pane for click and wheel events, previously `.react-flow__zoompane`
* `.react-flow__selectionpane` - Selection pane
* `.react-flow__selection` - User selection
* `.react-flow__edges` - Edges wrapper
+2 -2
View File
@@ -119,7 +119,7 @@ describe('Basic Flow Rendering', () => {
// for d3 we have to pass the window to the event
// https://github.com/cypress-io/cypress/issues/3441
cy.window().then((win) => {
cy.get('.react-flow__zoompane')
cy.get('.react-flow__renderer')
.trigger('mousedown', 'topLeft', { which: 1, view: win })
.trigger('mousemove', 'bottomLeft')
.trigger('mouseup', { force: true, view: win })
@@ -133,7 +133,7 @@ describe('Basic Flow Rendering', () => {
it('zooms the pane', () => {
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
cy.get('.react-flow__zoompane')
cy.get('.react-flow__renderer')
.trigger('wheel', 'topLeft', { deltaY: -200 })
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
+1 -1
View File
@@ -36,7 +36,7 @@ describe('Controls Testing', () => {
// for d3 we have to pass the window to the event
// https://github.com/cypress-io/cypress/issues/3441
cy.window().then((win) => {
cy.get('.react-flow__zoompane')
cy.get('.react-flow__renderer')
.trigger('mousedown', 'topLeft', { which: 1, view: win })
.trigger('mousemove', 'bottomLeft')
.trigger('mouseup', { force: true, view: win })
+2
View File
@@ -16,6 +16,8 @@ describe('Empty Flow Rendering', () => {
.trigger('mousedown', 'topLeft', { which: 1, force: true })
.trigger('mousemove', 'bottomLeft', { which: 1 })
.trigger('mouseup', 'bottomLeft', { force: true });
cy.get('body').type('{shift}', { release: true });
});
it('renders an empty mini map', () => {
+4 -4
View File
@@ -52,7 +52,7 @@ describe('Interaction Flow Rendering', () => {
it('tries to zoom by scroll', () => {
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
cy.get('.react-flow__zoompane')
cy.get('.react-flow__renderer')
.dblclick()
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
@@ -63,7 +63,7 @@ describe('Interaction Flow Rendering', () => {
it('tries to zoom by double click', () => {
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
cy.get('.react-flow__zoompane')
cy.get('.react-flow__renderer')
.trigger('wheel', 'topLeft', { deltaY: -200 })
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
@@ -122,7 +122,7 @@ describe('Interaction Flow Rendering', () => {
it('zooms by scroll', () => {
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
cy.get('.react-flow__zoompane')
cy.get('.react-flow__renderer')
.trigger('wheel', 'topLeft', { deltaY: 200 })
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
@@ -137,7 +137,7 @@ describe('Interaction Flow Rendering', () => {
it('zooms by double click', () => {
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
cy.get('.react-flow__zoompane')
cy.get('.react-flow__renderer')
.dblclick()
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
+2 -2
View File
@@ -19,7 +19,7 @@ describe('Minimap Testing', () => {
const viewBoxBeforeZoom = Cypress.$('.react-flow__minimap').attr('viewBox');
const maskPathBeforeZoom = Cypress.$('.react-flow__minimap-mask').attr('d');
cy.get('.react-flow__zoompane')
cy.get('.react-flow__renderer')
.trigger('wheel', 'topLeft', { deltaY: -200 })
.then(() => {
const viewBoxAfterZoom = Cypress.$('.react-flow__minimap').attr('viewBox');
@@ -53,7 +53,7 @@ describe('Minimap Testing', () => {
// for d3 we have to pass the window to the event
// https://github.com/cypress-io/cypress/issues/3441
cy.window().then((win) => {
cy.get('.react-flow__zoompane')
cy.get('.react-flow__renderer')
.trigger('mousedown', 'topLeft', { which: 1, view: win })
.trigger('mousemove', 'bottomLeft')
.trigger('mouseup', { force: true, view: win })
+6
View File
@@ -4,6 +4,9 @@ import ReactFlow, { removeElements, addEdge, MiniMap, Controls, Background, isNo
const onNodeDragStart = (event, node) => console.log('drag start', node);
const onNodeDragStop = (event, node) => console.log('drag stop', node);
const onPaneClick = (event) => console.log('pane click', event);
const onPaneScroll = (event) => console.log('pane scroll', event);
const onPaneContextMenu = (event) => console.log('pane context menu', event);
const onSelectionDrag = (event, nodes) => console.log('selection drag', nodes);
const onSelectionDragStart = (event, nodes) => console.log('selection drag start', nodes);
const onSelectionDragStop = (event, nodes) => console.log('selection drag stop', nodes);
@@ -125,6 +128,9 @@ const OverviewFlow = () => {
onElementClick={onElementClick}
onElementsRemove={onElementsRemove}
onConnect={onConnect}
onPaneClick={onPaneClick}
onPaneScroll={onPaneScroll}
onPaneContextMenu={onPaneContextMenu}
onNodeDragStart={onNodeDragStart}
onNodeDragStop={onNodeDragStop}
onSelectionDragStart={onSelectionDragStart}
+8 -1
View File
@@ -12,7 +12,7 @@ type UserSelectionProps = {
};
function getMousePosition(event: React.MouseEvent): XYPosition | void {
const reactFlowNode = document.querySelector('.react-flow');
const reactFlowNode = (event.target as Element).closest('.react-flow');
if (!reactFlowNode) {
return;
}
@@ -51,6 +51,7 @@ export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
const setUserSelection = useStoreActions((actions) => actions.setUserSelection);
const updateUserSelection = useStoreActions((actions) => actions.updateUserSelection);
const unsetUserSelection = useStoreActions((actions) => actions.unsetUserSelection);
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const renderUserSelectionPane = selectionActive || selectionKeyPressed;
if (!elementsSelectable || !renderUserSelectionPane) {
@@ -81,12 +82,18 @@ export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
const onMouseUp = () => unsetUserSelection();
const onMouseLeave = () => {
unsetUserSelection();
unsetNodesSelection();
};
return (
<div
className="react-flow__selectionpane"
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onMouseUp={onMouseUp}
onMouseLeave={onMouseLeave}
>
<SelectionRect />
</div>
+113
View File
@@ -0,0 +1,113 @@
import React, { useCallback, useRef, memo, ReactNode, WheelEvent, MouseEvent } from 'react';
import { useStoreActions, useStoreState } from '../../store/hooks';
import useResizeHandler from '../../hooks/useResizeHandler';
import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler';
import useD3Zoom from '../../hooks/useD3Zoom';
import useKeyPress from '../../hooks/useKeyPress';
import { GraphViewProps } from '../GraphView';
import UserSelection from '../../components/UserSelection';
import NodesSelection from '../../components/NodesSelection';
interface FlowRendererProps
extends Omit<
GraphViewProps,
| 'elements'
| 'snapToGrid'
| 'nodeTypes'
| 'edgeTypes'
| 'snapGrid'
| 'connectionLineType'
| 'arrowHeadColor'
| 'onlyRenderVisibleNodes'
> {
children: ReactNode;
}
const FlowRenderer = ({
children,
onPaneClick,
onPaneContextMenu,
onPaneScroll,
onElementsRemove,
deleteKeyCode,
onMove,
onMoveStart,
onMoveEnd,
selectionKeyCode,
zoomOnScroll,
zoomOnDoubleClick,
paneMoveable,
defaultPosition,
defaultZoom,
translateExtent,
onSelectionDragStart,
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
}: FlowRendererProps) => {
const zoomPane = useRef<HTMLDivElement>(null);
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const nodesSelectionActive = useStoreState((state) => state.nodesSelectionActive);
const selectionKeyPressed = useKeyPress(selectionKeyCode);
useResizeHandler(zoomPane);
useGlobalKeyHandler({ onElementsRemove, deleteKeyCode });
useD3Zoom({
zoomPane,
onMove,
onMoveStart,
onMoveEnd,
selectionKeyPressed,
zoomOnScroll,
zoomOnDoubleClick,
paneMoveable,
defaultPosition,
defaultZoom,
translateExtent,
});
const onClick = useCallback(
(event: MouseEvent) => {
onPaneClick?.(event);
unsetNodesSelection();
},
[onPaneClick]
);
const onContextMenu = useCallback(
(event: MouseEvent) => {
onPaneContextMenu?.(event);
},
[onPaneContextMenu]
);
const onWheel = useCallback(
(event: WheelEvent) => {
onPaneScroll?.(event);
},
[onPaneScroll]
);
return (
<div className="react-flow__renderer" ref={zoomPane}>
{children}
<UserSelection selectionKeyPressed={selectionKeyPressed} />
{nodesSelectionActive && (
<NodesSelection
onSelectionDragStart={onSelectionDragStart}
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu}
/>
)}
<div className="react-flow__pane" onClick={onClick} onContextMenu={onContextMenu} onWheel={onWheel} />
</div>
);
};
FlowRenderer.displayName = 'FlowRenderer';
export default memo(FlowRenderer);
+24 -69
View File
@@ -1,15 +1,10 @@
import React, { useEffect, useRef, useCallback, memo, CSSProperties, MouseEvent, WheelEvent } from 'react';
import React, { useEffect, useRef, memo, CSSProperties, MouseEvent, WheelEvent } from 'react';
import { useStoreState, useStoreActions, useStore } from '../../store/hooks';
import FlowRenderer from '../FlowRenderer';
import NodeRenderer from '../NodeRenderer';
import EdgeRenderer from '../EdgeRenderer';
import UserSelection from '../../components/UserSelection';
import NodesSelection from '../../components/NodesSelection';
import useKeyPress from '../../hooks/useKeyPress';
import useD3Zoom from '../../hooks/useD3Zoom';
import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler';
import useElementUpdater from '../../hooks/useElementUpdater';
import useResizeHandler from '../../hooks/useResizeHandler';
import { onLoadProject, onLoadGetElements } from '../../utils/graph';
import {
Elements,
@@ -130,11 +125,7 @@ const GraphView = ({
onPaneContextMenu,
}: GraphViewProps) => {
const isInitialised = useRef<boolean>(false);
const zoomPane = useRef<HTMLDivElement>(null);
const rendererNode = useRef<HTMLDivElement>(null);
const d3Initialised = useStoreState((state) => state.d3Initialised);
const nodesSelectionActive = useStoreState((state) => state.nodesSelectionActive);
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const setOnConnect = useStoreActions((actions) => actions.setOnConnect);
const setOnConnectStart = useStoreActions((actions) => actions.setOnConnectStart);
const setOnConnectStop = useStoreActions((actions) => actions.setOnConnectStop);
@@ -153,26 +144,8 @@ const GraphView = ({
const zoomTo = useStoreActions((actions) => actions.zoomTo);
const currentStore = useStore();
useResizeHandler(rendererNode);
useGlobalKeyHandler({ onElementsRemove, deleteKeyCode });
useElementUpdater(elements);
const selectionKeyPressed = useKeyPress(selectionKeyCode);
useD3Zoom({
zoomPane,
onMove,
onMoveStart,
onMoveEnd,
selectionKeyPressed,
zoomOnScroll,
zoomOnDoubleClick,
paneMoveable,
defaultPosition,
defaultZoom,
translateExtent,
});
useEffect(() => {
if (!isInitialised.current && d3Initialised) {
if (onLoad) {
@@ -192,28 +165,6 @@ const GraphView = ({
}
}, [d3Initialised, onLoad]);
const onZoomPaneClick = useCallback(
(event: React.MouseEvent) => {
onPaneClick?.(event);
unsetNodesSelection();
},
[onPaneClick]
);
const onZoomPaneContextMenu = useCallback(
(event: React.MouseEvent) => {
onPaneContextMenu?.(event);
},
[onPaneContextMenu]
);
const onZoomPaneScroll = useCallback(
(event: WheelEvent) => {
onPaneScroll?.(event);
},
[onPaneScroll]
);
useEffect(() => {
if (onConnect) {
setOnConnect(onConnect);
@@ -287,7 +238,27 @@ const GraphView = ({
}, [translateExtent]);
return (
<div className="react-flow__renderer" ref={rendererNode}>
<FlowRenderer
onPaneClick={onPaneClick}
onPaneContextMenu={onPaneContextMenu}
onPaneScroll={onPaneScroll}
onElementsRemove={onElementsRemove}
deleteKeyCode={deleteKeyCode}
selectionKeyCode={selectionKeyCode}
onMove={onMove}
onMoveStart={onMoveStart}
onMoveEnd={onMoveEnd}
zoomOnScroll={zoomOnScroll}
zoomOnDoubleClick={zoomOnDoubleClick}
paneMoveable={paneMoveable}
defaultPosition={defaultPosition}
defaultZoom={defaultZoom}
translateExtent={translateExtent}
onSelectionDragStart={onSelectionDragStart}
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu}
>
<NodeRenderer
nodeTypes={nodeTypes}
onElementClick={onElementClick}
@@ -311,23 +282,7 @@ const GraphView = ({
markerEndId={markerEndId}
connectionLineComponent={connectionLineComponent}
/>
<UserSelection selectionKeyPressed={selectionKeyPressed} />
{nodesSelectionActive && (
<NodesSelection
onSelectionDragStart={onSelectionDragStart}
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu}
/>
)}
<div
className="react-flow__zoompane"
onClick={onZoomPaneClick}
onContextMenu={onZoomPaneContextMenu}
onWheel={onZoomPaneScroll}
ref={zoomPane}
/>
</div>
</FlowRenderer>
);
};
+15 -1
View File
@@ -107,6 +107,20 @@ export default ({
useEffect(() => {
if (d3Zoom) {
d3Zoom.filter((event: any) => {
if (selectionKeyPressed) {
return false;
}
// only allow zoom on nodes
if (event.target.closest('.react-flow__node') && event.type !== 'wheel') {
return false;
}
// only allow zoom on user selection
if (event.target.closest('.react-flow__nodesselection') && event.type !== 'wheel') {
return false;
}
if (!paneMoveable) {
return false;
}
@@ -122,5 +136,5 @@ export default ({
return !event.ctrlKey && !event.button;
});
}
}, [d3Zoom, zoomOnScroll, zoomOnDoubleClick, paneMoveable]);
}, [d3Zoom, zoomOnScroll, zoomOnDoubleClick, paneMoveable, selectionKeyPressed]);
};
+7 -3
View File
@@ -6,7 +6,7 @@
}
.react-flow__renderer,
.react-flow__zoompane,
.react-flow__pane,
.react-flow__selectionpane {
width: 100%;
height: 100%;
@@ -15,12 +15,16 @@
left: 0;
}
.react-flow__zoompane {
.react-flow__pane {
z-index: 1;
}
.react-flow__renderer {
z-index: 4;
}
.react-flow__selectionpane {
z-index: 2;
z-index: 5;
}
.react-flow__selection {