feat(react/svelte): controlled viewport (#3461)

* feat(react): controlled viewport

* refactor(react): cleanup viewport

* feat(svelte): two way data binding for viewport, cleanup transform <> viewport stuff

* refactor(svelte): viewport stuff
This commit is contained in:
Moritz Klack
2023-09-28 16:19:03 +02:00
committed by GitHub
parent 2215b68438
commit ae33cba887
35 changed files with 351 additions and 95 deletions
@@ -24,6 +24,7 @@ export type FlowRendererProps = Omit<
| 'rfId'
| 'nodeOrigin'
> & {
isControlledViewport: boolean;
children: ReactNode;
};
@@ -63,6 +64,8 @@ const FlowRenderer = ({
noWheelClassName,
noPanClassName,
disableKeyboardA11y,
onViewportChange,
isControlledViewport,
}: FlowRendererProps) => {
const nodesSelectionActive = useStore(selector);
const selectionKeyPressed = useKeyPress(selectionKeyCode);
@@ -92,6 +95,8 @@ const FlowRenderer = ({
preventScrolling={preventScrolling}
noWheelClassName={noWheelClassName}
noPanClassName={noPanClassName}
onViewportChange={onViewportChange}
isControlledViewport={isControlledViewport}
>
<Pane
onSelectionStart={onSelectionStart}
@@ -5,8 +5,9 @@ import NodeRenderer from '../NodeRenderer';
import EdgeRenderer from '../EdgeRenderer';
import ViewportWrapper from '../Viewport';
import useOnInitHandler from '../../hooks/useOnInitHandler';
import useViewportSync from '../../hooks/useViewportSync';
import ConnectionLine from '../../components/ConnectionLine';
import type { NodeTypesWrapped, ReactFlowProps } from '../../types';
import type { ReactFlowProps } from '../../types';
import { createNodeTypes } from '../NodeRenderer/utils';
import { createEdgeTypes } from '../EdgeRenderer/utils';
import { useNodeOrEdgeTypes } from './utils';
@@ -104,11 +105,14 @@ const GraphView = ({
nodeOrigin,
nodeExtent,
rfId,
viewport,
onViewportChange,
}: GraphViewProps) => {
const nodeTypesWrapped = useNodeOrEdgeTypes(nodeTypes, createNodeTypes);
const edgeTypesWrapped = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes);
useOnInitHandler(onInit);
useViewportSync(viewport);
return (
<FlowRenderer
@@ -145,6 +149,8 @@ const GraphView = ({
noWheelClassName={noWheelClassName}
noPanClassName={noPanClassName}
disableKeyboardA11y={disableKeyboardA11y}
onViewportChange={onViewportChange}
isControlledViewport={!!viewport}
>
<ViewportWrapper>
<EdgeRenderer
@@ -164,6 +164,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
style,
id,
nodeDragThreshold,
viewport,
onViewportChange,
...rest
},
ref
@@ -242,6 +244,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
disableKeyboardA11y={disableKeyboardA11y}
nodeOrigin={nodeOrigin}
nodeExtent={nodeExtent}
viewport={viewport}
onViewportChange={onViewportChange}
/>
<StoreUpdater
nodes={nodes}
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react';
import { useStore } from '../../hooks/useStore';
import type { ReactFlowState } from '../../types';
import { Viewport } from '@xyflow/system';
const selector = (s: ReactFlowState) => `translate(${s.transform[0]}px,${s.transform[1]}px) scale(${s.transform[2]})`;
@@ -18,7 +18,9 @@ type ZoomPaneProps = Omit<
| 'noDragClassName'
| 'disableKeyboardA11y'
| 'selectionOnDrag'
>;
> & {
isControlledViewport: boolean;
};
const selector = (s: ReactFlowState) => ({
userSelectionActive: s.userSelectionActive,
@@ -43,6 +45,8 @@ const ZoomPane = ({
children,
noWheelClassName,
noPanClassName,
onViewportChange,
isControlledViewport,
}: ZoomPaneProps) => {
const store = useStoreApi();
const zoomPane = useRef<HTMLDivElement>(null);
@@ -60,7 +64,13 @@ const ZoomPane = ({
maxZoom,
translateExtent,
viewport: defaultViewport,
onTransformChange: (transform: Transform) => store.setState({ transform }),
onTransformChange: (transform: Transform) => {
onViewportChange?.({ x: transform[0], y: transform[1], zoom: transform[2] });
if (!isControlledViewport) {
store.setState({ transform });
}
},
onDraggingChange: (paneDragging: boolean) => store.setState({ paneDragging }),
onPanZoomStart: (event, vp) => {
const { onViewportChangeStart, onMoveStart } = store.getState();
@@ -0,0 +1,21 @@
import { useEffect } from 'react';
import type { Viewport } from '@xyflow/system';
import { useStore, useStoreApi } from './useStore';
import type { ReactFlowState } from '../types';
const selector = (state: ReactFlowState) => state.panZoom?.syncViewport;
export default function useViewportSync(viewport?: Viewport) {
const syncViewport = useStore(selector);
const store = useStoreApi();
useEffect(() => {
if (viewport) {
syncViewport?.(viewport);
store.setState({ transform: [viewport.x, viewport.y, viewport.zoom] });
}
}, [viewport, syncViewport]);
return null;
}
@@ -120,7 +120,9 @@ export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
panOnDrag?: boolean | number[];
minZoom?: number;
maxZoom?: number;
viewport?: Viewport;
defaultViewport?: Viewport;
onViewportChange?: (viewport: Viewport) => void;
translateExtent?: CoordinateExtent;
preventScrolling?: boolean;
nodeExtent?: CoordinateExtent;