diff --git a/examples/vite-app/src/examples/InteractiveMinimap/index.tsx b/examples/vite-app/src/examples/InteractiveMinimap/index.tsx index db8d754f..18fa854a 100644 --- a/examples/vite-app/src/examples/InteractiveMinimap/index.tsx +++ b/examples/vite-app/src/examples/InteractiveMinimap/index.tsx @@ -1,5 +1,6 @@ import { MouseEvent, useCallback } from 'react'; import ReactFlow, { + MiniMap, Background, BackgroundVariant, Controls, @@ -9,7 +10,6 @@ import ReactFlow, { useReactFlow, XYPosition, } from 'reactflow'; -import { MiniMap } from '@reactflow/interactive-minimap'; const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node); const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); @@ -137,7 +137,7 @@ const BasicFlow = () => { fitView > - +
diff --git a/examples/vite-app/src/examples/NestedNodes/index.tsx b/examples/vite-app/src/examples/NestedNodes/index.tsx index 5e9c80f1..a6c083ce 100644 --- a/examples/vite-app/src/examples/NestedNodes/index.tsx +++ b/examples/vite-app/src/examples/NestedNodes/index.tsx @@ -160,7 +160,7 @@ const NestedFlow = () => { maxZoom={4} onlyRenderVisibleElements={false} > - + diff --git a/packages/interactive-minimap/.eslintrc.js b/packages/interactive-minimap/.eslintrc.js deleted file mode 100644 index 31cfbd11..00000000 --- a/packages/interactive-minimap/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - root: true, - extends: ['@reactflow/eslint-config'], -}; diff --git a/packages/interactive-minimap/CHANGELOG.md b/packages/interactive-minimap/CHANGELOG.md deleted file mode 100644 index eb44ff49..00000000 --- a/packages/interactive-minimap/CHANGELOG.md +++ /dev/null @@ -1,42 +0,0 @@ -# @reactflow/minimap - -## 11.0.1 - -### Patch Changes - -- Updated dependencies [[`def11008`](https://github.com/wbkd/react-flow/commit/def11008d88749fec40e6fcba8bc41eea2511bab), [`d00faa6b`](https://github.com/wbkd/react-flow/commit/d00faa6b3e77388bfd655d4c02e9a5375bc515e4)]: - - @reactflow/core@11.1.0 - -## 11.0.0 - -### Major Changes - -- **Better [Accessibility](/docs/guides/accessibility)** - - Nodes and edges are focusable, selectable, moveable and deleteable with the keyboard. - - `aria-` default attributes for all elements and controllable via `ariaLabel` options - - Keyboard controls can be disabled with the new `disableKeyboardA11y` prop -- **Better selectable edges** via new edge option: `interactionWidth` - renders invisible edge that makes it easier to interact -- **Better routing for smoothstep and step edges**: https://twitter.com/reactflowdev/status/1567535405284614145 -- **Nicer edge updating behaviour**: https://twitter.com/reactflowdev/status/1564966917517021184 -- **Node origin**: The new `nodeOrigin` prop lets you control the origin of a node. Useful for layouting. -- **New background pattern**: `BackgroundVariant.Cross` variant -- **[`useOnViewportChange`](/docs/api/hooks/use-on-viewport-change) hook** - handle viewport changes within a component -- **[`useOnSelectionChange`](/docs/api/hooks/use-on-selection-change) hook** - handle selection changes within a component -- **[`useNodesInitialized`](/docs/api/hooks/use-nodes-initialized) hook** - returns true if all nodes are initialized and if there is more than one node -- **Deletable option** for Nodes and edges -- **New Event handlers**: `onPaneMouseEnter`, `onPaneMouseMove` and `onPaneMouseLeave` -- **Edge `pathOptions`** for `smoothstep` and `default` edges -- **Nicer cursor defaults**: Cursor is grabbing, while dragging a node or panning -- **Pane moveable** with middle mouse button -- **Pan over nodes** when they are not draggable (`draggable=false` or `nodesDraggable` false) -- **[``](/docs/api/edges/base-edge) component** that makes it easier to build custom edges -- **[Separately installable packages](/docs/overview/packages/)** - - @reactflow/core - - @reactflow/background - - @reactflow/controls - - @reactflow/minimap - -### Patch Changes - -- Updated dependencies: - - @reactflow/core@11.0.0 diff --git a/packages/interactive-minimap/README.md b/packages/interactive-minimap/README.md deleted file mode 100644 index 2dba17d9..00000000 --- a/packages/interactive-minimap/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# @reactflow/minimap - -Mini map component for React Flow. - -## Installation - -```sh -npm install @reactflow/minimap -``` - diff --git a/packages/interactive-minimap/package.json b/packages/interactive-minimap/package.json deleted file mode 100644 index 8dc4a82a..00000000 --- a/packages/interactive-minimap/package.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "name": "@reactflow/interactive-minimap", - "version": "0.0.1-alpha.0", - "description": "Interactive Minimap component for React Flow.", - "keywords": [ - "react", - "node-based UI", - "graph", - "diagram", - "workflow", - "react-flow" - ], - "files": [ - "dist" - ], - "source": "src/index.tsx", - "main": "dist/umd/index.js", - "module": "dist/esm/index.js", - "types": "dist/esm/index.d.ts", - "sideEffects": false, - "publishConfig": { - "access": "public" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/wbkd/react-flow.git", - "directory": "packages/interactive-minimap" - }, - "scripts": { - "dev": "concurrently \"rollup --config node:@reactflow/rollup-config --watch\" pnpm:css-watch", - "build": "rollup --config node:@reactflow/rollup-config --environment NODE_ENV:production && npm run css", - "css": "postcss src/*.css --config ../../tooling/postcss-config/postcss.config.js --dir dist", - "css-watch": "pnpm css --watch", - "lint": "eslint --ext .js,.jsx,.ts,.tsx src", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@babel/runtime": "^7.18.9", - "@reactflow/core": "workspace:*", - "@types/d3-selection": "^3.0.3", - "@types/d3-zoom": "^3.0.1", - "classcat": "^5.0.3", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0", - "zustand": "^4.1.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - }, - "devDependencies": { - "@reactflow/eslint-config": "workspace:^0.0.0", - "@reactflow/rollup-config": "workspace:*", - "@reactflow/tsconfig": "workspace:*", - "@types/node": "^18.7.16", - "@types/react": "^18.0.19", - "react": "^18.2.0", - "typescript": "^4.8.3" - }, - "rollup": { - "globals": { - "zustand": "Zustand", - "zustand/shallow": "zustandShallow", - "classcat": "cc" - }, - "name": "ReactFlowMinimap" - } -} diff --git a/packages/interactive-minimap/src/MiniMap.tsx b/packages/interactive-minimap/src/MiniMap.tsx deleted file mode 100644 index 46b0ec9f..00000000 --- a/packages/interactive-minimap/src/MiniMap.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { memo, useEffect, useRef, MouseEvent } from 'react'; -import cc from 'classcat'; -import shallow from 'zustand/shallow'; -import { zoom, D3ZoomEvent, zoomIdentity } from 'd3-zoom'; -import { select, pointer } from 'd3-selection'; - -import { useStore, getRectOfNodes, Panel, getBoundsOfRects, useStoreApi, ReactFlowState, Rect } from '@reactflow/core'; - -import MiniMapNode from './MiniMapNode'; -import { MiniMapProps, GetMiniMapNodeAttribute } from './types'; - -declare const window: any; - -const defaultWidth = 200; -const defaultHeight = 150; - -const selector = (s: ReactFlowState) => { - const nodes = Array.from(s.nodeInternals.values()); - const viewBB: Rect = { - x: -s.transform[0] / s.transform[2], - y: -s.transform[1] / s.transform[2], - width: s.width / s.transform[2], - height: s.height / s.transform[2], - }; - - return { - nodes: nodes.filter((node) => !node.hidden && node.width && node.height), - viewBB, - boundingRect: nodes.length > 0 ? getBoundsOfRects(getRectOfNodes(nodes), viewBB) : viewBB, - rfId: s.rfId, - }; -}; - -const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func); - -const ARIA_LABEL_KEY = 'react-flow__minimap-desc'; - -function MiniMap({ - style, - className, - nodeStrokeColor = '#555', - nodeColor = '#999', - nodeClassName = '', - nodeBorderRadius = 5, - nodeStrokeWidth = 2, - maskColor = 'rgb(200, 200, 200, 0.9)', - position = 'bottom-right', - onClick, - onNodeClick, -}: MiniMapProps) { - const store = useStoreApi(); - const svg = useRef(null); - const { boundingRect, viewBB, nodes, rfId } = useStore(selector, shallow); - const elementWidth = (style?.width as number) ?? defaultWidth; - const elementHeight = (style?.height as number) ?? defaultHeight; - const nodeColorFunc = getAttrFunction(nodeColor); - const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor); - const nodeClassNameFunc = getAttrFunction(nodeClassName); - const scaledWidth = boundingRect.width / elementWidth; - const scaledHeight = boundingRect.height / elementHeight; - const viewScale = Math.max(scaledWidth, scaledHeight); - const viewWidth = viewScale * elementWidth; - const viewHeight = viewScale * elementHeight; - const offset = 5 * viewScale; - const x = boundingRect.x - (viewWidth - boundingRect.width) / 2 - offset; - const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset; - const width = viewWidth + offset * 2; - const height = viewHeight + offset * 2; - const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'; - const labelledBy = `${ARIA_LABEL_KEY}-${rfId}`; - const viewScaleRef = useRef(0); - - viewScaleRef.current = viewScale; - - const onSvgClick = (event: MouseEvent) => { - const rfCoord = pointer(event); - onClick?.(event, { x: rfCoord[0], y: rfCoord[1] }); - }; - - useEffect(() => { - if (svg.current) { - const selection = select(svg.current as Element); - - const zoomHandler = zoom() - .on('zoom.wheel', (event: D3ZoomEvent) => { - const { transform, d3Selection, d3Zoom } = store.getState(); - - if (event.sourceEvent.type !== 'wheel' || !d3Selection || !d3Zoom) { - return; - } - - const pinchDelta = - -event.sourceEvent.deltaY * - (event.sourceEvent.deltaMode === 1 ? 0.05 : event.sourceEvent.deltaMode ? 1 : 0.002) * - 10; - const zoom = transform[2] * Math.pow(2, pinchDelta); - - d3Zoom.scaleTo(d3Selection, zoom); - }) - .on('zoom', (event: D3ZoomEvent) => { - const { transform, d3Selection, d3Zoom } = store.getState(); - - if (event.sourceEvent.type !== 'mousemove' || !d3Selection || !d3Zoom) { - return; - } - - const position = { - x: transform[0] - event.sourceEvent.movementX * viewScaleRef.current * transform[2], - y: transform[1] - event.sourceEvent.movementY * viewScaleRef.current * transform[2], - }; - - const nextTransform = zoomIdentity.translate(position.x, position.y).scale(transform[2]); - - d3Zoom.transform(d3Selection, nextTransform); - }); - selection.call(zoomHandler); - - return () => { - selection.on('.zoom', null); - }; - } - }, []); - - const onSvgNodeClick = onNodeClick - ? (event: MouseEvent, nodeId: string) => { - const node = store.getState().nodeInternals.get(nodeId)!; - onNodeClick(event, node); - } - : undefined; - - return ( - - - React Flow mini map - {nodes.map((node) => { - return ( - - ); - })} - - - - ); -} - -MiniMap.displayName = 'MiniMap'; - -export default memo(MiniMap); diff --git a/packages/interactive-minimap/src/MiniMapNode.tsx b/packages/interactive-minimap/src/MiniMapNode.tsx deleted file mode 100644 index 15a5ba3f..00000000 --- a/packages/interactive-minimap/src/MiniMapNode.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { memo, CSSProperties, MouseEvent } from 'react'; -import cc from 'classcat'; - -interface MiniMapNodeProps { - id: string; - x: number; - y: number; - width: number; - height: number; - borderRadius: number; - className: string; - color: string; - shapeRendering: string; - strokeColor: string; - strokeWidth: number; - style?: CSSProperties; - onClick?: (event: MouseEvent, id: string) => void; -} - -const MiniMapNode = ({ - id, - x, - y, - width, - height, - style, - color, - strokeColor, - strokeWidth, - className, - borderRadius, - shapeRendering, - onClick, -}: MiniMapNodeProps) => { - const { background, backgroundColor } = style || {}; - const fill = (color || background || backgroundColor) as string; - - return ( - onClick(event, id) : undefined} - /> - ); -}; - -MiniMapNode.displayName = 'MiniMapNode'; - -export default memo(MiniMapNode); diff --git a/packages/interactive-minimap/src/index.tsx b/packages/interactive-minimap/src/index.tsx deleted file mode 100644 index 7b91e95d..00000000 --- a/packages/interactive-minimap/src/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { default as MiniMap } from './MiniMap'; -export * from './types'; diff --git a/packages/interactive-minimap/src/style.css b/packages/interactive-minimap/src/style.css deleted file mode 100644 index dbb9a931..00000000 --- a/packages/interactive-minimap/src/style.css +++ /dev/null @@ -1,3 +0,0 @@ -.react-flow__minimap { - background-color: #fff; -} diff --git a/packages/interactive-minimap/src/types.ts b/packages/interactive-minimap/src/types.ts deleted file mode 100644 index 80534fc3..00000000 --- a/packages/interactive-minimap/src/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { HTMLAttributes, MouseEvent } from 'react'; -import { Node, PanelPosition, XYPosition } from '@reactflow/core'; - -export type GetMiniMapNodeAttribute = (node: Node) => string; - -export type MiniMapProps = Omit, 'onClick'> & { - nodeColor?: string | GetMiniMapNodeAttribute; - nodeStrokeColor?: string | GetMiniMapNodeAttribute; - nodeClassName?: string | GetMiniMapNodeAttribute; - nodeBorderRadius?: number; - nodeStrokeWidth?: number; - maskColor?: string; - position?: PanelPosition; - onClick?: (event: MouseEvent, position: XYPosition) => void; - onNodeClick?: (event: MouseEvent, node: Node) => void; -}; diff --git a/packages/interactive-minimap/tsconfig.json b/packages/interactive-minimap/tsconfig.json deleted file mode 100644 index 8a8e74ce..00000000 --- a/packages/interactive-minimap/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "@reactflow/tsconfig/react.json", - "display": "@reactflow/minimap", - "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/minimap/package.json b/packages/minimap/package.json index 527aab38..100a12b0 100644 --- a/packages/minimap/package.json +++ b/packages/minimap/package.json @@ -40,7 +40,11 @@ "dependencies": { "@babel/runtime": "^7.18.9", "@reactflow/core": "workspace:*", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", "zustand": "^4.1.1" }, "peerDependencies": { diff --git a/packages/minimap/src/MiniMap.tsx b/packages/minimap/src/MiniMap.tsx index 05cd8771..4fd639da 100644 --- a/packages/minimap/src/MiniMap.tsx +++ b/packages/minimap/src/MiniMap.tsx @@ -1,8 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { memo } from 'react'; +import { memo, useEffect, useRef } from 'react'; +import type { MouseEvent } from 'react'; import cc from 'classcat'; import shallow from 'zustand/shallow'; -import { useStore, getRectOfNodes, getBoundsOfRects, Panel } from '@reactflow/core'; +import { zoom, zoomIdentity } from 'd3-zoom'; +import type { D3ZoomEvent } from 'd3-zoom'; +import { select, pointer } from 'd3-selection'; +import { useStore, getRectOfNodes, Panel, getBoundsOfRects, useStoreApi } from '@reactflow/core'; import type { ReactFlowState, Rect } from '@reactflow/core'; import MiniMapNode from './MiniMapNode'; @@ -44,7 +48,13 @@ function MiniMap({ nodeStrokeWidth = 2, maskColor = 'rgb(240, 242, 243, 0.7)', position = 'bottom-right', + onClick, + onNodeClick, + pannable = false, + zoomable = false, }: MiniMapProps) { + const store = useStoreApi(); + const svg = useRef(null); const { boundingRect, viewBB, nodes, rfId } = useStore(selector, shallow); const elementWidth = (style?.width as number) ?? defaultWidth; const elementHeight = (style?.height as number) ?? defaultHeight; @@ -63,6 +73,68 @@ function MiniMap({ const height = viewHeight + offset * 2; const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'; const labelledBy = `${ARIA_LABEL_KEY}-${rfId}`; + const viewScaleRef = useRef(0); + + viewScaleRef.current = viewScale; + + useEffect(() => { + if (svg.current) { + const selection = select(svg.current as Element); + + const zoomHandler = zoom() + .on('zoom.wheel', (event: D3ZoomEvent) => { + const { transform, d3Selection, d3Zoom } = store.getState(); + + if (event.sourceEvent.type !== 'wheel' || !zoomable || !d3Selection || !d3Zoom) { + return; + } + + const pinchDelta = + -event.sourceEvent.deltaY * + (event.sourceEvent.deltaMode === 1 ? 0.05 : event.sourceEvent.deltaMode ? 1 : 0.002) * + 10; + const zoom = transform[2] * Math.pow(2, pinchDelta); + + d3Zoom.scaleTo(d3Selection, zoom); + }) + .on('zoom', (event: D3ZoomEvent) => { + const { transform, d3Selection, d3Zoom } = store.getState(); + + if (event.sourceEvent.type !== 'mousemove' || !pannable || !d3Selection || !d3Zoom) { + return; + } + + // @TODO: how to calculate the correct next position? Math.max(1, transform[2]) is a workaround. + const position = { + x: transform[0] - event.sourceEvent.movementX * viewScaleRef.current * Math.max(1, transform[2]), + y: transform[1] - event.sourceEvent.movementY * viewScaleRef.current * Math.max(1, transform[2]), + }; + + const nextTransform = zoomIdentity.translate(position.x, position.y).scale(transform[2]); + + d3Zoom.transform(d3Selection, nextTransform); + }); + selection.call(zoomHandler); + + return () => { + selection.on('.zoom', null); + }; + } + }, [pannable, zoomable]); + + const onSvgClick = onClick + ? (event: MouseEvent) => { + const rfCoord = pointer(event); + onClick(event, { x: rfCoord[0], y: rfCoord[1] }); + } + : undefined; + + const onSvgNodeClick = onNodeClick + ? (event: MouseEvent, nodeId: string) => { + const node = store.getState().nodeInternals.get(nodeId)!; + onNodeClick(event, node); + } + : undefined; return ( @@ -72,6 +144,8 @@ function MiniMap({ viewBox={`${x} ${y} ${width} ${height}`} role="img" aria-labelledby={labelledBy} + ref={svg} + onClick={onSvgClick} > React Flow mini map {nodes.map((node) => { @@ -89,6 +163,8 @@ function MiniMap({ strokeColor={nodeStrokeColorFunc(node)} strokeWidth={nodeStrokeWidth} shapeRendering={shapeRendering} + onClick={onSvgNodeClick} + id={node.id} /> ); })} diff --git a/packages/minimap/src/MiniMapNode.tsx b/packages/minimap/src/MiniMapNode.tsx index 111c7ee0..ae000d02 100644 --- a/packages/minimap/src/MiniMapNode.tsx +++ b/packages/minimap/src/MiniMapNode.tsx @@ -1,8 +1,9 @@ import { memo } from 'react'; -import type { CSSProperties } from 'react'; +import type { CSSProperties, MouseEvent } from 'react'; import cc from 'classcat'; interface MiniMapNodeProps { + id: string; x: number; y: number; width: number; @@ -14,9 +15,11 @@ interface MiniMapNodeProps { strokeColor: string; strokeWidth: number; style?: CSSProperties; + onClick?: (event: MouseEvent, id: string) => void; } const MiniMapNode = ({ + id, x, y, width, @@ -28,6 +31,7 @@ const MiniMapNode = ({ className, borderRadius, shapeRendering, + onClick, }: MiniMapNodeProps) => { const { background, backgroundColor } = style || {}; const fill = (color || background || backgroundColor) as string; @@ -45,6 +49,7 @@ const MiniMapNode = ({ stroke={strokeColor} strokeWidth={strokeWidth} shapeRendering={shapeRendering} + onClick={onClick ? (event) => onClick(event, id) : undefined} /> ); }; diff --git a/packages/minimap/src/types.ts b/packages/minimap/src/types.ts index 4fed29ba..63d8c6d9 100644 --- a/packages/minimap/src/types.ts +++ b/packages/minimap/src/types.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { HTMLAttributes } from 'react'; -import type { Node, PanelPosition } from '@reactflow/core'; +import type { HTMLAttributes, MouseEvent } from 'react'; +import type { Node, PanelPosition, XYPosition } from '@reactflow/core'; export type GetMiniMapNodeAttribute = (node: Node) => string; -export type MiniMapProps = HTMLAttributes & { +export type MiniMapProps = Omit, 'onClick'> & { nodeColor?: string | GetMiniMapNodeAttribute; nodeStrokeColor?: string | GetMiniMapNodeAttribute; nodeClassName?: string | GetMiniMapNodeAttribute; @@ -12,4 +12,8 @@ export type MiniMapProps = HTMLAttributes & { nodeStrokeWidth?: number; maskColor?: string; position?: PanelPosition; + onClick?: (event: MouseEvent, position: XYPosition) => void; + onNodeClick?: (event: MouseEvent, node: Node) => void; + pannable?: boolean; + zoomable?: boolean; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91f51a8d..56aff557 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -222,16 +222,24 @@ importers: '@reactflow/eslint-config': workspace:^0.0.0 '@reactflow/rollup-config': workspace:* '@reactflow/tsconfig': workspace:* + '@types/d3-selection': ^3.0.3 + '@types/d3-zoom': ^3.0.1 '@types/node': ^18.7.16 '@types/react': ^18.0.19 classcat: ^5.0.3 + d3-selection: ^3.0.0 + d3-zoom: ^3.0.0 react: ^18.2.0 typescript: ^4.8.3 zustand: ^4.1.1 dependencies: '@babel/runtime': registry.npmjs.org/@babel/runtime/7.19.0 '@reactflow/core': link:../core + '@types/d3-selection': registry.npmjs.org/@types/d3-selection/3.0.3 + '@types/d3-zoom': registry.npmjs.org/@types/d3-zoom/3.0.1 classcat: registry.npmjs.org/classcat/5.0.4 + d3-selection: registry.npmjs.org/d3-selection/3.0.0 + d3-zoom: registry.npmjs.org/d3-zoom/3.0.0 zustand: registry.npmjs.org/zustand/4.1.1_react@18.2.0 devDependencies: '@reactflow/eslint-config': link:../../tooling/eslint-config