Merge branch 'main' into bug-fix-node-positions

This commit is contained in:
Dylan Middendorf
2025-07-09 14:37:26 -04:00
103 changed files with 4114 additions and 3146 deletions
-7
View File
@@ -1,7 +0,0 @@
---
"@xyflow/react": minor
"@xyflow/svelte": minor
"@xyflow/system": patch
---
Add `ariaRole` prop to nodes and edges
+5
View File
@@ -0,0 +1,5 @@
---
'@xyflow/react': patch
---
Fix node fallback to respect custom default node type when unknown node type is encountered
-5
View File
@@ -1,5 +0,0 @@
---
'@xyflow/svelte': patch
---
Fix data in `EdgeProps` that was not typed correctly
-7
View File
@@ -1,7 +0,0 @@
---
'@xyflow/react': minor
'@xyflow/svelte': minor
'@xyflow/system': patch
---
Improve typing for Nodes
-5
View File
@@ -1,5 +0,0 @@
---
'@xyflow/svelte': patch
---
Fix setting nodesInitialized multiple times
-7
View File
@@ -1,7 +0,0 @@
---
'@xyflow/react': minor
'@xyflow/svelte': minor
'@xyflow/system': patch
---
Add an `ease` and `interpolate` option to all function that alter the viewport
-5
View File
@@ -1,5 +0,0 @@
---
'@xyflow/svelte': patch
---
Change a11y description inline styles to classes
+6
View File
@@ -0,0 +1,6 @@
---
'@xyflow/react': patch
'@xyflow/svelte': patch
---
Return intersections correctly of passed node is bigger than intersecting nodes
+5
View File
@@ -0,0 +1,5 @@
---
'@xyflow/react': patch
---
Fix edge fallback to respect custom default edge type when unknown edge type is encountered.
-8
View File
@@ -1,8 +0,0 @@
---
"@xyflow/react": minor
"@xyflow/svelte": minor
"@xyflow/system": patch
---
Add `ariaLabelConfig` prop for customizing UI text like aria labels and descriptions.
-5
View File
@@ -1,5 +0,0 @@
---
'@xyflow/svelte': patch
---
Export missing callback types
+37 -1
View File
@@ -41,7 +41,7 @@ Talking to us first about the enhancement you want to build will be the most lik
To ask about a possible enhancement, email us at info@reactflow.dev
### 💫 Pull Requests
## 💫 Pull Requests
If you want to contribute improvements or new features we are happy to review your PR :)
Please use a meaningful commit message and add a little description of your changes.
@@ -50,3 +50,39 @@ Please use a meaningful commit message and add a little description of your chan
2. Start dev server `pnpm dev`
3. Test your changes with the existing examples or add a new one if it's needed for your changes
4. Run tests `pnpm test` and add new new tests if you are introducing a new feature
## Changeset Style Guide
*Inspired and taken from [Common Changelogs](https://github.com/vweevers/common-changelog?tab=readme-ov-file) and [Warp by Broad Institute](https://broadinstitute.github.io/warp/docs/contribution/contribute_to_warp/changelog_style/)*
If you are writing a changeset for a PR, here are some helpful tips:
## TLDR
- Changelogs are for humans
- Communicate the impact of changes
- Use active voice *and* presence tense *(”Fix …” instead of “… was fixed”)*
- Omit redundant verbs
- *“Document” instead of “Add documentation”*
- Omit personal pronouns
- Use backticks for function or component names (`getNodesBounds`, `<ReactFlow />`, etc) for a better syntax highlighting
## Examples
**🛑 Bad:**
“minimap: use latest node dimensions”
**✅ Good:**
“Display minimap nodes even if onNodesChange is not implemented”
**🛑 Bad:**
“fix(handles): reconnect for connectionMode=loose”
**✅ Good:**
“Fix reconnections when connectionMode is set to loose”
**🛑 Bad:**
“use correct index when using setNodes for inserting”
**✅ Good:**
“Fix incorrect order of nodes added with setNodes from useReactFlow”
+4 -4
View File
@@ -11,15 +11,15 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/react": "^3.0.2",
"@astrojs/svelte": "^4.0.2",
"@astrojs/react": "^4.3.0",
"@astrojs/svelte": "^7.1.0",
"@types/react": "^18.2.24",
"@types/react-dom": "^18.2.8",
"@xyflow/react": "workspace:^",
"@xyflow/svelte": "workspace:^",
"astro": "^3.2.2",
"astro": "^5.9.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"svelte": "^4.2.1"
"svelte": "^5.33.18"
}
}
@@ -1,10 +1,18 @@
<script lang="ts">
import { writable } from 'svelte/store';
import { SvelteFlow, Controls, Background, BackgroundVariant, Position, type Node, type Edge } from '@xyflow/svelte';
import {
SvelteFlow,
Controls,
Background,
BackgroundVariant,
Position,
ViewportPortal,
type Node,
type Edge,
} from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
const nodes = writable<Node[]>([
let nodes = $state.raw<Node[]>([
{
id: '0',
position: { x: 0, y: 150 },
@@ -59,7 +67,7 @@
},
]);
const edges = writable<Edge[]>([
let edges = $state.raw<Edge[]>([
{ id: '0A', source: '0', target: 'A', animated: true },
{ id: '0B', source: '0', target: 'B', animated: true },
{ id: '0C', source: '0', target: 'C', animated: true },
@@ -71,8 +79,11 @@
</script>
<div style="height: 400px; width: 700px;">
<SvelteFlow {nodes} {edges} fitView {defaultEdgeOptions} width={700} height={400}>
<SvelteFlow bind:nodes bind:edges fitView {defaultEdgeOptions} width={700} height={400}>
<Controls />
<Background variant={BackgroundVariant.Dots} />
<ViewportPortal target="front">
<div style:transform="translate(100px, 100px)" style:position="absolute">[100, 100] inside the flow.</div>
</ViewportPortal>
</SvelteFlow>
</div>
@@ -1,11 +1,10 @@
<script lang="ts">
import { writable } from 'svelte/store';
import { SvelteFlow, Controls, Background, BackgroundVariant, type Node, type Edge } from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
import CustomNode from './CustomNode.svelte';
const nodes = writable<Node[]>([
let nodes = $state.raw<Node[]>([
{
id: '1',
position: { x: 0, y: 0 },
@@ -24,7 +23,7 @@
},
]);
const edges = writable<Edge[]>([{ id: '1-2', source: '1', target: '2' }]);
let edges = $state.raw<Edge[]>([{ id: '1-2', source: '1', target: '2' }]);
const nodeTypes = {
'input-node': CustomNode,
@@ -32,7 +31,7 @@
</script>
<div style="height: 400px; width: 700px;">
<SvelteFlow {nodes} {edges} {nodeTypes} fitView width={700} height={400}>
<SvelteFlow bind:nodes bind:edges {nodeTypes} fitView width={700} height={400}>
<Controls />
<Background variant={BackgroundVariant.Dots} />
</SvelteFlow>
+28 -28
View File
@@ -6,38 +6,38 @@ import SvelteFlowInitialApp from '../components/SvelteFlowInitialExample/index.s
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro example for React Flow and Svelte Flow</title>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro example for React Flow and Svelte Flow</title>
<style>
html, body {
margin:0;
font-family: sans-serif;
}
</style>
</head>
<body>
<h2>React Flow</h2>
<p>no client hydration</p>
<ReactFlowApp />
<style>
html,
body {
margin: 0;
font-family: sans-serif;
}
</style>
</head>
<body>
<h2>React Flow</h2>
<p>no client hydration</p>
<ReactFlowApp />
<p>client hydration on load (client:load)</p>
<ReactFlowApp client:load />
<p>client hydration on load (client:load)</p>
<ReactFlowApp client:load />
<p>client hydration on load (client:load) and initialWidth / initialHeight</p>
<ReactFlowInitialApp client:load />
<p>client hydration on load (client:load) and initialWidth / initialHeight</p>
<ReactFlowInitialApp client:load />
<h2>Svelte Flow</h2>
<SvelteFlowApp />
<h2>Svelte Flow</h2>
<SvelteFlowApp />
<p>client hydration on load (client:load)</p>
<SvelteFlowApp client:load />
<p>client hydration on load (client:load)</p>
<SvelteFlowApp client:load />
<p>client hydration on load (client:load) and initialWidth / initialHeight</p>
<SvelteFlowInitialApp client:load />
</body>
<p>client hydration on load (client:load) and initialWidth / initialHeight</p>
<SvelteFlowInitialApp client:load />
</body>
</html>
+12
View File
@@ -9,6 +9,8 @@ import ControlledViewport from '../examples/ControlledViewport';
import CustomConnectionLine from '../examples/CustomConnectionLine';
import CustomMiniMapNode from '../examples/CustomMiniMapNode';
import CustomNode from '../examples/CustomNode';
import DefaultEdgeOverwrite from '../examples/DefaultEdgeOverwrite';
import DefaultNodeOverwrite from '../examples/DefaultNodeOverwrite';
import DefaultNodes from '../examples/DefaultNodes';
import DragHandle from '../examples/DragHandle';
import DragNDrop from '../examples/DragNDrop';
@@ -129,6 +131,16 @@ const routes: IRoute[] = [
path: 'custom-node',
component: CustomNode,
},
{
name: 'Default Node Overwrite',
path: 'default-node-overwrite',
component: DefaultNodeOverwrite,
},
{
name: 'Default Edge Overwrite',
path: 'default-edge-overwrite',
component: DefaultEdgeOverwrite,
},
{
name: 'Default Nodes',
path: 'default-nodes',
+44 -19
View File
@@ -1,4 +1,4 @@
import { MouseEvent } from 'react';
import { useState } from 'react';
import {
ReactFlow,
MiniMap,
@@ -8,15 +8,10 @@ import {
ReactFlowProvider,
Node,
Edge,
OnNodeDrag,
AriaLabelConfig,
Panel,
} from '@xyflow/react';
const onNodeDrag: OnNodeDrag = (_, node: Node, nodes: Node[]) => console.log('drag', node, nodes);
const onNodeDragStart = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes);
const onNodeDragStop = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag stop', node, nodes);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const initialNodes: Node[] = [
{
id: '1',
@@ -24,24 +19,46 @@ const initialNodes: Node[] = [
data: { label: 'A11y Node 1' },
position: { x: 250, y: 5 },
className: 'light',
domAttributes: {
tabIndex: 10,
'aria-roledescription': 'A11y Node',
},
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
position: { x: 1000, y: 100 },
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
position: { x: 100, y: 100 },
className: 'light',
ariaRole: 'button',
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 300, y: 100 },
},
{
id: '5',
data: { label: 'Node 5' },
position: { x: 400, y: 200 },
},
{
id: '6',
data: { label: 'Node 6' },
position: { x: -1000, y: 200 },
},
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e1-4', source: '1', target: '4' },
{ id: 'e1-5', source: '4', target: '5' },
{ id: 'e1-6', source: '3', target: '6' },
];
const ariaLabelConfig: Partial<AriaLabelConfig> = {
@@ -59,19 +76,13 @@ const ariaLabelConfig: Partial<AriaLabelConfig> = {
};
const A11y = () => {
const [autoPanOnNodeFocus, setAutoPanOnNodeFocus] = useState(true);
return (
<ReactFlow
defaultNodes={initialNodes}
defaultEdges={initialEdges}
onNodesChange={console.log}
onNodeClick={onNodeClick}
onNodeDragStop={onNodeDragStop}
onNodeDragStart={onNodeDragStart}
onNodeDrag={onNodeDrag}
className="react-flow-basic-example"
minZoom={0.2}
maxZoom={4}
fitView
autoPanOnNodeFocus={autoPanOnNodeFocus}
selectNodesOnDrag={false}
elevateEdgesOnSelect
elevateNodesOnSelect={false}
@@ -81,6 +92,20 @@ const A11y = () => {
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
<Controls />
<Panel position="top-right">
<div>
<label htmlFor="focusPannable">
<input
id="focusPannable"
type="checkbox"
checked={autoPanOnNodeFocus}
onChange={(event) => setAutoPanOnNodeFocus(event.target.checked)}
className="xy-theme__checkbox"
/>
autoPanOnNodeFocus
</label>
</div>
</Panel>
</ReactFlow>
);
};
@@ -36,6 +36,7 @@ const ConnectionLineFlow = () => {
onEdgesChange={onEdgesChange}
connectionLineComponent={ConnectionLine}
onConnect={onConnect}
connectionDragThreshold={25}
>
<Background variant={BackgroundVariant.Lines} />
</ReactFlow>
@@ -0,0 +1,69 @@
import {
ReactFlow,
Node,
Edge,
ReactFlowProvider,
Background,
BackgroundVariant,
EdgeProps,
getBezierPath,
} from '@xyflow/react';
const initialNodes: Node[] = [
{
id: '1',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
},
];
const initialEdges: Edge[] = [
{
id: 'e1-2',
source: '1',
target: '2',
type: 'unregistered', // This will fallback to custom default
},
];
const CustomEdge = ({ sourceX, sourceY, targetX, targetY }: EdgeProps) => {
const [edgePath] = getBezierPath({
sourceX,
sourceY,
targetX,
targetY,
});
return (
<>
<path d={edgePath} stroke="red" strokeWidth={3} fill="none" strokeDasharray="5,5" />
</>
);
};
const edgeTypes = {
default: CustomEdge,
};
const DefaultEdgeOverwrite = () => {
return (
<ReactFlow defaultNodes={initialNodes} defaultEdges={initialEdges} edgeTypes={edgeTypes} fitView>
<Background variant={BackgroundVariant.Lines} />
</ReactFlow>
);
};
export default function App() {
return (
<ReactFlowProvider>
<DefaultEdgeOverwrite />
</ReactFlowProvider>
);
}
@@ -0,0 +1,41 @@
import { ReactFlow, Node, ReactFlowProvider, Background, BackgroundVariant, NodeProps } from '@xyflow/react';
const initialNodes: Node[] = [
{
id: '1',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
},
{
id: '2',
data: { label: 'Node 2' },
type: 'unregistered',
position: { x: 100, y: 100 },
className: 'light',
},
];
const CustomNode = (_: NodeProps) => {
return <div>Custom node</div>;
};
const nodeTypes = {
default: CustomNode,
};
const DefaultNodeOverwrite = () => {
return (
<ReactFlow defaultNodes={initialNodes} nodeTypes={nodeTypes} fitView>
<Background variant={BackgroundVariant.Lines} />
</ReactFlow>
);
};
export default function App() {
return (
<ReactFlowProvider>
<DefaultNodeOverwrite />
</ReactFlowProvider>
);
}
@@ -10,6 +10,8 @@ const CustomResizerNode: FC<NodeProps> = ({ data }) => {
resizeDirection="horizontal"
minWidth={100}
maxWidth={500}
color="orange"
autoScale={false}
/>
<Handle type="target" position={Position.Left} />
<div>{data.label}</div>
@@ -152,10 +152,10 @@ const initialEdges: Edge[] = [
},
},
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e3-4', source: '3', target: '4', zIndex: 100 },
{ id: 'e3-4', source: '3', target: '4' },
{ id: 'e3-4b', source: '3', target: '4b' },
{ id: 'e4a-4b1', source: '4a', target: '4b1' },
{ id: 'e4a-4b2', source: '4a', target: '4b2', zIndex: 100 },
{ id: 'e4a-4b2', source: '4a', target: '4b2' },
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' },
];
@@ -62,6 +62,29 @@ export default {
data: { label: '11' },
position: { x: 100, y: 500 },
},
{
id: '12',
data: { label: '12' },
position: { x: 100, y: 600 },
width: 200,
height: 100,
},
{
id: '12-a',
parentId: '12',
data: { label: '12-a' },
position: { x: 10, y: 20 },
width: 50,
height: 50,
},
{
id: '12-b',
parentId: '12',
data: { label: '12-b' },
position: { x: 140, y: 20 },
width: 50,
height: 50,
},
// {
// id: '12',
// data: { label: '12' },
@@ -145,6 +168,16 @@ export default {
markerEnd: { type: MarkerType.Arrow },
markerStart: { type: MarkerType.ArrowClosed },
},
{
id: 'subflow-edge',
source: '11',
target: '12-a',
},
{
id: 'subflow-edge-2',
source: '12-a',
target: '12-b',
},
// {
// id: 'updatable',
// source: '9',
+2 -2
View File
@@ -14,8 +14,8 @@
"devDependencies": {
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.20.7",
"@typescript-eslint/eslint-plugin": "^8.30.1",
"@typescript-eslint/parser": "^8.30.1",
"@typescript-eslint/eslint-plugin": "^8.34.0",
"@typescript-eslint/parser": "^8.34.0",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-svelte": "^3.5.1",
@@ -61,6 +61,29 @@ export default {
id: '11',
data: { label: '11' },
position: { x: 100, y: 500 }
},
{
id: '12',
data: { label: '12' },
position: { x: 100, y: 600 },
width: 200,
height: 100
},
{
id: '12-a',
parentId: '12',
data: { label: '12-a' },
position: { x: 10, y: 20 },
width: 50,
height: 50
},
{
id: '12-b',
parentId: '12',
data: { label: '12-b' },
position: { x: 140, y: 20 },
width: 50,
height: 50
}
// {
// id: '12',
@@ -142,6 +165,16 @@ export default {
label: 'markers',
markerEnd: { type: MarkerType.Arrow },
markerStart: { type: MarkerType.ArrowClosed }
},
{
id: 'subflow-edge',
source: '11',
target: '12-a'
},
{
id: 'subflow-edge-2',
source: '12-a',
target: '12-b'
}
// {
// id: 'updatable',
@@ -1,5 +1,5 @@
<script lang="ts">
import { SvelteFlow, Controls, Background, MiniMap } from '@xyflow/svelte';
import { SvelteFlow, Controls, Background, MiniMap, Panel } from '@xyflow/svelte';
import '@xyflow/svelte/dist/style.css';
@@ -10,7 +10,7 @@
data: { label: 'A' }
},
{ id: 'B', position: { x: -100, y: 150 }, data: { label: 'B' } },
{ id: 'C', position: { x: 100, y: 150 }, data: { label: 'C' } },
{ id: 'C', position: { x: 1000, y: 150 }, data: { label: 'C' } },
{ id: 'D', position: { x: 0, y: 260 }, data: { label: 'D' } }
]);
@@ -19,17 +19,19 @@
{ id: 'A-C', source: 'A', target: 'C' },
{ id: 'A-D', source: 'A', target: 'D' }
]);
</script>
<SvelteFlow
bind:nodes
bind:edges
fitView
ariaLabelConfig={{
let autoPanOnNodeFocus = $state(true);
const ariaLabelConfig = $state({
'node.a11yDescription.default': 'Svelte Custom Node Desc.',
'node.a11yDescription.keyboardDisabled': 'Svelte Custom Keyboard Desc.',
'node.a11yDescription.ariaLiveMessage': ({ direction, x, y }) =>
`Custom Moved selected node ${direction}. New position, x: ${x}, y: ${y}`,
'node.a11yDescription.ariaLiveMessage': ({
direction,
x,
y
}: {
direction: string;
x: number;
y: number;
}) => `Custom Moved selected node ${direction}. New position, x: ${x}, y: ${y}`,
'edge.a11yDescription.default': 'Svelte Custom Edge Desc.',
'controls.ariaLabel': 'Svelte Custom Control Aria Label',
'controls.zoomIn.ariaLabel': 'Svelte Custom Zoom in',
@@ -37,9 +39,24 @@
// 'controls.fitView.ariaLabel': 'Svelte Custom Fit View',
'controls.interactive.ariaLabel': 'Svelte Custom Toggle Interactivity',
'minimap.ariaLabel': 'Svelte Custom Minimap'
}}
>
});
</script>
<SvelteFlow bind:nodes bind:edges {autoPanOnNodeFocus} {ariaLabelConfig}>
<Controls />
<Background />
<MiniMap />
<Panel class="panel top-right">
<div>
<label for="focusPannable">
Enable Pan on Focus
<input
id="focusPannable"
type="checkbox"
bind:checked={autoPanOnNodeFocus}
class="svelte-flow__zoomonscroll"
/>
</label>
</div>
</Panel>
</SvelteFlow>
@@ -24,7 +24,14 @@
</script>
<div style="height:100vh;">
<SvelteFlow bind:nodes bind:edges {nodeTypes} fitView connectionLineComponent={ConnectionLine}>
<SvelteFlow
bind:nodes
bind:edges
{nodeTypes}
fitView
connectionLineComponent={ConnectionLine}
connectionDragThreshold={100}
>
<Background variant={BackgroundVariant.Lines} />
</SvelteFlow>
</div>
@@ -16,6 +16,15 @@
handleType: 'target'
});
useNodeConnections({
onConnect: (connection) => {
console.log('Connection made:', connection);
},
onDisconnect: (connection) => {
console.log('Connection disconnected:', connection);
}
});
let nodeData = $derived(
useNodesData<MyNode>(connections.current.map((connection) => connection.source))
);
+1 -1
View File
@@ -26,7 +26,7 @@
"@changesets/cli": "^2.25.0",
"@playwright/test": "^1.44.1",
"concurrently": "^7.6.0",
"eslint": "^8.22.0",
"eslint": "^8.57.0",
"prettier": "^2.7.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+60
View File
@@ -1,5 +1,65 @@
# @xyflow/react
## 12.8.1
### Patch Changes
- Updated dependencies [[`26f2cdd7`](https://github.com/xyflow/xyflow/commit/26f2cdd720fc2c8fb337d3af13b82dab6a90fb60)]:
- @xyflow/system@0.0.65
## 12.8.0
### Minor Changes
- [#5361](https://github.com/xyflow/xyflow/pull/5361) [`90e9247a`](https://github.com/xyflow/xyflow/commit/90e9247adbdfa9d06db97e1d0d895e35c960551c) Thanks [@peterkogo](https://github.com/peterkogo)! - Render edges above nodes when they are within a subflow
- [#5344](https://github.com/xyflow/xyflow/pull/5344) [`2441bf8d`](https://github.com/xyflow/xyflow/commit/2441bf8d97a6b72494f216915d52d5acbeefefde) Thanks [@moklick](https://github.com/moklick)! - Add connectionDragThreshold prop
### Patch Changes
- [#5368](https://github.com/xyflow/xyflow/pull/5368) [`445017ed`](https://github.com/xyflow/xyflow/commit/445017ed6fdedee4b39a0eaeb1cbbe0155f9a037) Thanks [@moklick](https://github.com/moklick)! - Cleanup store updater
- [#5362](https://github.com/xyflow/xyflow/pull/5362) [`72dc1d60`](https://github.com/xyflow/xyflow/commit/72dc1d602110947e3db83c37b9a9125ee85cf4bc) Thanks [@moklick](https://github.com/moklick)! - Remove pointer events from Panel via CSS while a selection gets dragged
- Updated dependencies [[`72dc1d60`](https://github.com/xyflow/xyflow/commit/72dc1d602110947e3db83c37b9a9125ee85cf4bc), [`90e9247a`](https://github.com/xyflow/xyflow/commit/90e9247adbdfa9d06db97e1d0d895e35c960551c), [`2441bf8d`](https://github.com/xyflow/xyflow/commit/2441bf8d97a6b72494f216915d52d5acbeefefde)]:
- @xyflow/system@0.0.64
## 12.7.1
### Patch Changes
- [#5354](https://github.com/xyflow/xyflow/pull/5354) [`c4312d89`](https://github.com/xyflow/xyflow/commit/c4312d8997ecdc7ef12cfa4efc1fde7131a2b950) Thanks [@moklick](https://github.com/moklick)! - Add TSDoc annotations
- [#5333](https://github.com/xyflow/xyflow/pull/5333) [`3d7e8b6b`](https://github.com/xyflow/xyflow/commit/3d7e8b6bb10001ee84d79ca4f6a9fd0053c4a276) Thanks [@peterkogo](https://github.com/peterkogo)! - Add missing type exports
- [#5353](https://github.com/xyflow/xyflow/pull/5353) [`4dd3ae66`](https://github.com/xyflow/xyflow/commit/4dd3ae6684fa3c62ee8cabd64e7631a52173c8f4) Thanks [@aidanbarrett](https://github.com/aidanbarrett)! - Add missing `FinalConnectionState` parameter to `onReconnectEnd` prop types
- Updated dependencies [[`c4312d89`](https://github.com/xyflow/xyflow/commit/c4312d8997ecdc7ef12cfa4efc1fde7131a2b950), [`3d7e8b6b`](https://github.com/xyflow/xyflow/commit/3d7e8b6bb10001ee84d79ca4f6a9fd0053c4a276), [`9c61000c`](https://github.com/xyflow/xyflow/commit/9c61000cac6277ce97274cc626fa7266f82dec27)]:
- @xyflow/system@0.0.63
## 12.7.0
### Minor Changes
- [#5299](https://github.com/xyflow/xyflow/pull/5299) [`848b486b`](https://github.com/xyflow/xyflow/commit/848b486b2201b650ecb3317f367a723edb2458e1) Thanks [@printerscanner](https://github.com/printerscanner)! - Add `ariaRole` prop to nodes and edges
- [#5280](https://github.com/xyflow/xyflow/pull/5280) [`dba6faf2`](https://github.com/xyflow/xyflow/commit/dba6faf20e7ec2524d5270d177331d3bd260f3ac) Thanks [@moklick](https://github.com/moklick)! - Improve typing for Nodes
- [#5276](https://github.com/xyflow/xyflow/pull/5276) [`6ce44a05`](https://github.com/xyflow/xyflow/commit/6ce44a05c829068ff5a8416ce3fa4ee6e0eced48) Thanks [@moklick](https://github.com/moklick)! - Add an `ease` and `interpolate` option to all function that alter the viewport
- [#5277](https://github.com/xyflow/xyflow/pull/5277) [`f59730ce`](https://github.com/xyflow/xyflow/commit/f59730ce3530a91f579f6bbd2ea9335680f552ef) Thanks [@printerscanner](https://github.com/printerscanner)! - Add `ariaLabelConfig` prop for customizing UI text like aria labels and descriptions.
- [#5317](https://github.com/xyflow/xyflow/pull/5317) [`09458f52`](https://github.com/xyflow/xyflow/commit/09458f52ff57356e03404c58e9bfdbfd50579850) Thanks [@moklick](https://github.com/moklick)! - Add `domAttributes` option for nodes and edges
- [#5326](https://github.com/xyflow/xyflow/pull/5326) [`050b511c`](https://github.com/xyflow/xyflow/commit/050b511cd6966ba526299f7ca11f9ca4791fd2cf) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent NodeResizer controls to become too small when zooming out
- [#5308](https://github.com/xyflow/xyflow/pull/5308) [`09fab679`](https://github.com/xyflow/xyflow/commit/09fab6794031410c9e9465281d038c3520afe783) Thanks [@printerscanner](https://github.com/printerscanner)! - Focus nodes on tab if not within the viewport and add a new prop `autoPanOnNodeFocus`
### Patch Changes
- Updated dependencies [[`848b486b`](https://github.com/xyflow/xyflow/commit/848b486b2201b650ecb3317f367a723edb2458e1), [`dba6faf2`](https://github.com/xyflow/xyflow/commit/dba6faf20e7ec2524d5270d177331d3bd260f3ac), [`6ce44a05`](https://github.com/xyflow/xyflow/commit/6ce44a05c829068ff5a8416ce3fa4ee6e0eced48), [`f59730ce`](https://github.com/xyflow/xyflow/commit/f59730ce3530a91f579f6bbd2ea9335680f552ef), [`09458f52`](https://github.com/xyflow/xyflow/commit/09458f52ff57356e03404c58e9bfdbfd50579850), [`050b511c`](https://github.com/xyflow/xyflow/commit/050b511cd6966ba526299f7ca11f9ca4791fd2cf)]:
- @xyflow/system@0.0.62
## 12.6.4
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@xyflow/react",
"version": "12.6.4",
"version": "12.8.1",
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
"keywords": [
"react",
@@ -1,5 +1,6 @@
import { useRef, useEffect, memo } from 'react';
import { useRef, useEffect, memo, useCallback } from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
import {
XYResizer,
ResizeControlVariant,
@@ -13,18 +14,28 @@ import {
evaluateAbsolutePosition,
ParentExpandChild,
XYPosition,
ControlPosition,
} from '@xyflow/system';
import { useStoreApi } from '../../hooks/useStore';
import { useStoreApi, useStore } from '../../hooks/useStore';
import { useNodeId } from '../../contexts/NodeIdContext';
import type { ResizeControlProps, ResizeControlLineProps } from './types';
import { ReactFlowState } from '../../types';
const scaleSelector = (calculateScale: boolean) => (store: ReactFlowState) =>
calculateScale ? `${Math.max(1 / store.transform[2], 1)}` : undefined;
const defaultPositions: Record<ResizeControlVariant, ControlPosition> = {
[ResizeControlVariant.Line]: 'right',
[ResizeControlVariant.Handle]: 'bottom-right',
};
function ResizeControl({
nodeId,
position,
variant = ResizeControlVariant.Handle,
className,
style = {},
style = undefined,
children,
color,
minWidth = 10,
@@ -33,6 +44,7 @@ function ResizeControl({
maxHeight = Number.MAX_VALUE,
keepAspectRatio = false,
resizeDirection,
autoScale = true,
shouldResize,
onResizeStart,
onResize,
@@ -42,10 +54,13 @@ function ResizeControl({
const id = typeof nodeId === 'string' ? nodeId : contextNodeId;
const store = useStoreApi();
const resizeControlRef = useRef<HTMLDivElement>(null);
const defaultPosition = variant === ResizeControlVariant.Line ? 'right' : 'bottom-right';
const controlPosition = position ?? defaultPosition;
const isHandleControl = variant === ResizeControlVariant.Handle;
const scale = useStore(
useCallback(scaleSelector(isHandleControl && autoScale), [isHandleControl, autoScale]),
shallow
);
const resizer = useRef<XYResizerInstance | null>(null);
const controlPosition = position ?? defaultPositions[variant];
useEffect(() => {
if (!resizeControlRef.current || !id) {
@@ -192,14 +207,16 @@ function ResizeControl({
]);
const positionClassNames = controlPosition.split('-');
const colorStyleProp = variant === ResizeControlVariant.Line ? 'borderColor' : 'backgroundColor';
const controlStyle = color ? { ...style, [colorStyleProp]: color } : style;
return (
<div
className={cc(['react-flow__resize-control', 'nodrag', ...positionClassNames, variant, className])}
ref={resizeControlRef}
style={controlStyle}
style={{
...style,
scale,
...(color && { [isHandleControl ? 'backgroundColor' : 'borderColor']: color }),
}}
>
{children}
</div>
@@ -40,6 +40,7 @@ export function NodeResizer({
maxWidth = Number.MAX_VALUE,
maxHeight = Number.MAX_VALUE,
keepAspectRatio = false,
autoScale = true,
shouldResize,
onResizeStart,
onResize,
@@ -66,6 +67,7 @@ export function NodeResizer({
maxHeight={maxHeight}
onResizeStart={onResizeStart}
keepAspectRatio={keepAspectRatio}
autoScale={autoScale}
shouldResize={shouldResize}
onResize={onResize}
onResizeEnd={onResizeEnd}
@@ -85,6 +87,7 @@ export function NodeResizer({
maxHeight={maxHeight}
onResizeStart={onResizeStart}
keepAspectRatio={keepAspectRatio}
autoScale={autoScale}
shouldResize={shouldResize}
onResize={onResize}
onResizeEnd={onResizeEnd}
@@ -59,6 +59,11 @@ export type NodeResizerProps = {
* @default false
*/
keepAspectRatio?: boolean;
/**
* Scale the controls with the zoom level.
* @default true
*/
autoScale?: boolean;
/** Callback to determine if node should resize. */
shouldResize?: ShouldResize;
/** Callback called when resizing starts. */
@@ -82,6 +87,7 @@ export type ResizeControlProps = Pick<
| 'maxHeight'
| 'keepAspectRatio'
| 'shouldResize'
| 'autoScale'
| 'onResizeStart'
| 'onResize'
| 'onResizeEnd'
@@ -1,5 +1,12 @@
// Reconnectable edges have a anchors around their handles to reconnect the edge.
import { XYHandle, type Connection, EdgePosition, FinalConnectionState, HandleType } from '@xyflow/system';
import {
XYHandle,
type Connection,
EdgePosition,
FinalConnectionState,
HandleType,
OnConnectStart,
} from '@xyflow/system';
import { EdgeAnchor } from '../Edges/EdgeAnchor';
import type { EdgeWrapperProps, Edge } from '../../types/edges';
@@ -60,15 +67,17 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
} = store.getState();
const isTarget = oppositeHandle.type === 'target';
setReconnecting(true);
onReconnectStart?.(event, edge, oppositeHandle.type);
const _onReconnectEnd = (evt: MouseEvent | TouchEvent, connectionState: FinalConnectionState) => {
setReconnecting(false);
onReconnectEnd?.(evt, edge, oppositeHandle.type, connectionState);
};
const onConnectEdge = (connection: Connection) => onReconnect?.(edge, connection);
const _onConnectStart: OnConnectStart = (_event, params) => {
setReconnecting(true);
onReconnectStart?.(event, edge, oppositeHandle.type);
onConnectStart?.(_event, params);
};
XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect,
@@ -86,12 +95,13 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
panBy,
isValidConnection,
onConnect: onConnectEdge,
onConnectStart,
onConnectStart: _onConnectStart,
onConnectEnd,
onReconnectEnd: _onReconnectEnd,
updateConnection,
getTransform: () => store.getState().transform,
getFromHandle: () => store.getState().connection.fromHandle,
dragThreshold: store.getState().connectionDragThreshold,
});
};
@@ -46,7 +46,7 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
if (EdgeComponent === undefined) {
onError?.('011', errorMessages['error011'](edgeType));
edgeType = 'default';
EdgeComponent = builtinEdgeTypes.default;
EdgeComponent = edgeTypes?.['default'] || builtinEdgeTypes.default;
}
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
@@ -199,7 +199,7 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
onKeyDown={isFocusable ? onKeyDown : undefined}
tabIndex={isFocusable ? 0 : undefined}
role={edge.ariaRole ?? (isFocusable ? 'group' : 'img')}
aria-roledescription={edge.ariaRoleDescription || 'edge'}
aria-roledescription="edge"
data-id={id}
data-testid={`rf__edge-${id}`}
aria-label={
@@ -207,6 +207,7 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
}
aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined}
ref={edgeRef}
{...edge.domAttributes}
>
{!reconnecting && (
<EdgeComponent
@@ -149,6 +149,7 @@ function HandleComponent(
getTransform: () => store.getState().transform,
getFromHandle: () => store.getState().connection.fromHandle,
autoPanSpeed: currentStore.autoPanSpeed,
dragThreshold: currentStore.connectionDragThreshold,
});
}
@@ -7,6 +7,7 @@ import {
getNodeDimensions,
isInputDOMNode,
nodeHasDimensions,
getNodesInside,
} from '@xyflow/system';
import { useStore, useStoreApi } from '../../hooks/useStore';
@@ -57,7 +58,7 @@ export function NodeWrapper<NodeType extends Node>({
if (NodeComponent === undefined) {
onError?.('003', errorMessages['error003'](nodeType));
nodeType = 'default';
NodeComponent = builtinNodeTypes.default;
NodeComponent = nodeTypes?.['default'] || builtinNodeTypes.default;
}
const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'));
@@ -158,6 +159,28 @@ export function NodeWrapper<NodeType extends Node>({
});
}
};
const onFocus = () => {
if (disableKeyboardA11y || !nodeRef.current?.matches(':focus-visible')) {
return;
}
const { transform, width, height, autoPanOnNodeFocus, setCenter } = store.getState();
if (!autoPanOnNodeFocus) {
return;
}
const withinViewport =
getNodesInside(new Map([[id, node]]), { x: 0, y: 0, width, height }, transform, true).length > 0;
if (!withinViewport) {
setCenter(node.position.x + nodeDimensions.width / 2, node.position.y + nodeDimensions.height / 2, {
zoom: transform[2],
});
}
};
return (
<div
className={cc([
@@ -195,10 +218,12 @@ export function NodeWrapper<NodeType extends Node>({
onDoubleClick={onDoubleClickHandler}
onKeyDown={isFocusable ? onKeyDown : undefined}
tabIndex={isFocusable ? 0 : undefined}
onFocus={isFocusable ? onFocus : undefined}
role={node.ariaRole ?? (isFocusable ? 'group' : undefined)}
aria-roledescription={node.ariaRoleDescription || 'node'}
aria-roledescription="node"
aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`}
aria-label={node.ariaLabel}
{...node.domAttributes}
>
<Provider value={id}>
<NodeComponent
+1 -12
View File
@@ -2,9 +2,6 @@ import { HTMLAttributes, forwardRef } from 'react';
import cc from 'classcat';
import type { PanelPosition } from '@xyflow/system';
import { useStore } from '../../hooks/useStore';
import type { ReactFlowState } from '../../types';
/**
* @expand
*/
@@ -16,8 +13,6 @@ export type PanelProps = HTMLAttributes<HTMLDivElement> & {
position?: PanelPosition;
};
const selector = (s: ReactFlowState) => (s.userSelectionActive ? 'none' : 'all');
/**
* The `<Panel />` component helps you position content above the viewport.
* It is used internally by the [`<MiniMap />`](/api-reference/components/minimap)
@@ -45,16 +40,10 @@ const selector = (s: ReactFlowState) => (s.userSelectionActive ? 'none' : 'all')
*/
export const Panel = forwardRef<HTMLDivElement, PanelProps>(
({ position = 'top-left', children, className, style, ...rest }, ref) => {
const pointerEvents = useStore(selector);
const positionClasses = `${position}`.split('-');
return (
<div
className={cc(['react-flow__panel', className, ...positionClasses])}
style={{ ...style, pointerEvents }}
ref={ref}
{...rest}
>
<div className={cc(['react-flow__panel', className, ...positionClasses])} style={style} ref={ref} {...rest}>
{children}
</div>
);
@@ -23,6 +23,7 @@ const reactFlowFieldsToTrack = [
'onClickConnectStart',
'onClickConnectEnd',
'nodesDraggable',
'autoPanOnNodeFocus',
'nodesConnectable',
'nodesFocusable',
'edgesFocusable',
@@ -64,6 +65,7 @@ const reactFlowFieldsToTrack = [
'isValidConnection',
'selectNodesOnDrag',
'nodeDragThreshold',
'connectionDragThreshold',
'onBeforeDelete',
'debug',
'autoPanSpeed',
@@ -154,13 +156,11 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
else if (fieldName === 'translateExtent') setTranslateExtent(fieldValue as CoordinateExtent);
else if (fieldName === 'nodeExtent') setNodeExtent(fieldValue as CoordinateExtent);
else if (fieldName === 'paneClickDistance') setPaneClickDistance(fieldValue as number);
else if (fieldName === 'ariaLabelConfig')
store.setState({ ariaLabelConfig: mergeAriaLabelConfig(fieldValue as AriaLabelConfig) });
// Renamed fields
else if (fieldName === 'fitView') store.setState({ fitViewQueued: fieldValue as boolean });
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOptions: fieldValue as FitViewOptions });
if (fieldName === 'ariaLabelConfig') {
store.setState({ ariaLabelConfig: mergeAriaLabelConfig(fieldValue as AriaLabelConfig) });
}
// General case
else store.setState({ [fieldName]: fieldValue });
}
@@ -48,13 +48,13 @@ function NodeRendererComponent<NodeType extends Node>(props: NodeRendererProps<N
* The split of responsibilities between NodeRenderer and
* NodeComponentWrapper may appear weird. However, its designed to
* minimize the cost of updates when individual nodes change.
*
*
* For example, when youre dragging a single node, that node gets
* updated multiple times per second. If `NodeRenderer` were to update
* every time, it would have to re-run the `nodes.map()` loop every
* time. This gets pricey with hundreds of nodes, especially if every
* loop cycle does more than just rendering a JSX element!
*
*
* As a result of this choice, we took the following implementation
* decisions:
* - NodeRenderer subscribes *only* to node IDs and therefore
@@ -77,6 +77,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
onlyRenderVisibleElements = false,
selectNodesOnDrag,
nodesDraggable,
autoPanOnNodeFocus,
nodesConnectable,
nodesFocusable,
nodeOrigin = defaultNodeOrigin,
@@ -138,6 +139,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
style,
id,
nodeDragThreshold,
connectionDragThreshold,
viewport,
onViewportChange,
width,
@@ -261,6 +263,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
onClickConnectStart={onClickConnectStart}
onClickConnectEnd={onClickConnectEnd}
nodesDraggable={nodesDraggable}
autoPanOnNodeFocus={autoPanOnNodeFocus}
nodesConnectable={nodesConnectable}
nodesFocusable={nodesFocusable}
edgesFocusable={edgesFocusable}
@@ -304,6 +307,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
isValidConnection={isValidConnection}
selectNodesOnDrag={selectNodesOnDrag}
nodeDragThreshold={nodeDragThreshold}
connectionDragThreshold={connectionDragThreshold}
onBeforeDelete={onBeforeDelete}
paneClickDistance={paneClickDistance}
debug={debug}
+5 -1
View File
@@ -222,7 +222,11 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
const partiallyVisible = partially && overlappingArea > 0;
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
return (
partiallyVisible ||
overlappingArea >= currNodeRect.width * currNodeRect.height ||
overlappingArea >= nodeRect.width * nodeRect.height
);
}) as NodeType[];
},
isNodeIntersecting: (nodeOrRect, area, partially = true) => {
+1 -19
View File
@@ -63,25 +63,7 @@ const useViewportHelper = (): ViewportHelperFunctions => {
return { x, y, zoom };
},
setCenter: async (x, y, options) => {
const { width, height, maxZoom, panZoom } = store.getState();
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom;
const centerX = width / 2 - x * nextZoom;
const centerY = height / 2 - y * nextZoom;
if (!panZoom) {
return Promise.resolve(false);
}
await panZoom.setViewport(
{
x: centerX,
y: centerY,
zoom: nextZoom,
},
{ duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate }
);
return Promise.resolve(true);
return store.getState().setCenter(x, y, options);
},
fitBounds: async (bounds, options) => {
const { width, height, minZoom, maxZoom, panZoom } = store.getState();
+3
View File
@@ -109,6 +109,9 @@ export {
type NodeConnection,
type OnReconnect,
type AriaLabelConfig,
type SetCenter,
type SetViewport,
type FitBounds,
} from '@xyflow/system';
// we need this workaround to prevent a duplicate identifier error
+20
View File
@@ -360,6 +360,26 @@ const createStore = ({
return panBySystem({ delta, panZoom, transform, translateExtent, width, height });
},
setCenter: async (x, y, options) => {
const { width, height, maxZoom, panZoom } = get();
if (!panZoom) {
return Promise.resolve(false);
}
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom;
await panZoom.setViewport(
{
x: width / 2 - x * nextZoom,
y: height / 2 - y * nextZoom,
zoom: nextZoom,
},
{ duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate }
);
return Promise.resolve(true);
},
cancelConnection: () => {
set({
connection: { ...initialConnection },
+3
View File
@@ -107,6 +107,7 @@ const getInitialState = ({
noPanClassName: 'nopan',
nodeOrigin: storeNodeOrigin,
nodeDragThreshold: 1,
connectionDragThreshold: 1,
snapGrid: [15, 15],
snapToGrid: false,
@@ -134,7 +135,9 @@ const getInitialState = ({
ariaLiveMessage: '',
autoPanOnConnect: true,
autoPanOnNodeDrag: true,
autoPanOnNodeFocus: true,
autoPanSpeed: 15,
connectionRadius: 20,
onError: devWarn,
isValidConnection: undefined,
+13 -1
View File
@@ -22,6 +22,7 @@ import type {
SnapGrid,
OnReconnect,
AriaLabelConfig,
FinalConnectionState,
} from '@xyflow/system';
import type {
@@ -138,7 +139,7 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* This event fires when the user releases the source or target of an editable edge. It is called
* even if an edge update does not occur.
*/
onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void;
onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType, connectionState: FinalConnectionState) => void;
/**
* Use this event handler to add interactivity to a controlled flow.
* It is called on node drag, select, and move.
@@ -383,6 +384,11 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* @default true
*/
nodesDraggable?: boolean;
/**
* When `true`, the viewport will pan when a node is focused.
* @default true
*/
autoPanOnNodeFocus?: boolean;
/**
* Controls whether all nodes should be connectable or not. Individual nodes can override this
* setting by setting their `connectable` prop.
@@ -652,6 +658,12 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* @default 1
*/
nodeDragThreshold?: number;
/**
* The threshold in pixels that the mouse must move before a connection line starts to drag.
* This is useful to prevent accidental connections when clicking on a handle.
* @default 1
*/
connectionDragThreshold?: number;
/** Sets a fixed width for the flow. */
width?: number;
/** Sets a fixed height for the flow. */
+6 -2
View File
@@ -69,6 +69,10 @@ export type Edge<
* @default "group"
*/
ariaRole?: AriaRole;
/**
* General escape hatch for adding custom attributes to the edge's DOM element.
*/
domAttributes?: Omit<SVGAttributes<SVGGElement>, 'id' | 'style' | 'className' | 'role' | 'aria-label'>;
};
type SmoothStepEdge<EdgeData extends Record<string, unknown> = Record<string, unknown>> = Edge<
@@ -180,12 +184,12 @@ export type BaseEdgeProps = Omit<SVGAttributes<SVGPathElement>, 'd' | 'path' | '
path: string;
/**
* The id of the SVG marker to use at the start of the edge. This should be defined in a
* `<defs>` element in a separate SVG document or element.
* `<defs>` element in a separate SVG document or element. Use the format "url(#markerId)" where markerId is the id of your marker definition.
*/
markerStart?: string;
/**
* The id of the SVG marker to use at the end of the edge. This should be defined in a `<defs>`
* element in a separate SVG document or element.
* element in a separate SVG document or element. Use the format "url(#markerId)" where markerId is the id of your marker definition.
*/
markerEnd?: string;
};
+16
View File
@@ -99,6 +99,9 @@ export type OnSelectionChangeFunc<NodeType extends Node = Node, EdgeType extends
params: OnSelectionChangeParams<NodeType, EdgeType>
) => void;
/**
* @inline
*/
export type FitViewParams<NodeType extends Node = Node> = FitViewParamsBase<NodeType>;
/**
@@ -107,9 +110,18 @@ export type FitViewParams<NodeType extends Node = Node> = FitViewParamsBase<Node
* transform the viewport smoothly over a given amount of time.
*
* @public
* @inline
*/
export type FitViewOptions<NodeType extends Node = Node> = FitViewOptionsBase<NodeType>;
/**
* @inline
*/
export type FitView<NodeType extends Node = Node> = (fitViewOptions?: FitViewOptions<NodeType>) => Promise<boolean>;
/**
* @inline
*/
export type OnInit<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (
reactFlowInstance: ReactFlowInstance<NodeType, EdgeType>
) => void;
@@ -209,4 +221,8 @@ export type OnBeforeDelete<NodeType extends Node = Node, EdgeType extends Edge =
EdgeType
>;
/**
* This type can be used to type the `isValidConnection` function.
* If the function returns `true`, the connection is valid and can be created.
*/
export type IsValidConnection<EdgeType extends Edge = Edge> = (edge: EdgeType | Connection) => boolean;
+6 -6
View File
@@ -105,7 +105,7 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
* parameter can be set to `true` to include nodes that are only partially intersecting.
*
* @param node - the node or rect to check for intersections
* @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed node or rect
* @param partially - true by default, if set to false, only nodes that are fully intersecting will be returned
* @param nodes - optional nodes array to check for intersections
*
* @returns an array of intersecting nodes
@@ -252,10 +252,10 @@ export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edg
NodeType,
EdgeType
> &
ViewportHelperFunctions & {
/**
* React Flow needs to mount the viewport to the DOM and initialize its zoom and pan behavior.
* This property tells you when viewport is initialized.
*/
ViewportHelperFunctions & {
/**
* React Flow needs to mount the viewport to the DOM and initialize its zoom and pan behavior.
* This property tells you when viewport is initialized.
*/
viewportInitialized: boolean;
};
+9 -2
View File
@@ -1,4 +1,4 @@
import type { CSSProperties, MouseEvent as ReactMouseEvent, AriaRole } from 'react';
import type { CSSProperties, MouseEvent as ReactMouseEvent, AriaRole, HTMLAttributes, DOMAttributes } from 'react';
import type { CoordinateExtent, NodeBase, OnError, NodeProps as NodePropsBase, InternalNodeBase } from '@xyflow/system';
import { NodeTypes } from './general';
@@ -22,8 +22,15 @@ export type Node<
* The ARIA role attribute for the node element, used for accessibility.
* @default "group"
*/
ariaRole?: AriaRole;
/**
* General escape hatch for adding custom attributes to the node's DOM element.
*/
domAttributes?: Omit<
HTMLAttributes<HTMLDivElement>,
'id' | 'style' | 'className' | 'draggable' | 'role' | 'aria-label' | keyof DOMAttributes<HTMLDivElement>
>;
};
/**
+4
View File
@@ -29,6 +29,7 @@ import {
type EdgeChange,
type ParentLookup,
type AriaLabelConfig,
SetCenter,
} from '@xyflow/system';
import type {
@@ -75,6 +76,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
nodeExtent: CoordinateExtent;
nodeOrigin: NodeOrigin;
nodeDragThreshold: number;
connectionDragThreshold: number;
nodesSelectionActive: boolean;
userSelectionActive: boolean;
@@ -88,6 +90,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
snapGrid: SnapGrid;
nodesDraggable: boolean;
autoPanOnNodeFocus: boolean;
nodesConnectable: boolean;
nodesFocusable: boolean;
edgesFocusable: boolean;
@@ -171,6 +174,7 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
triggerNodeChanges: (changes: NodeChange<NodeType>[]) => void;
triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void;
panBy: PanBy;
setCenter: SetCenter;
setPaneClickDistance: (distance: number) => void;
};
+78
View File
@@ -1,5 +1,83 @@
# @xyflow/svelte
## 1.2.1
### Patch Changes
- Updated dependencies [[`26f2cdd7`](https://github.com/xyflow/xyflow/commit/26f2cdd720fc2c8fb337d3af13b82dab6a90fb60)]:
- @xyflow/system@0.0.65
## 1.2.0
### Minor Changes
- [#5361](https://github.com/xyflow/xyflow/pull/5361) [`90e9247a`](https://github.com/xyflow/xyflow/commit/90e9247adbdfa9d06db97e1d0d895e35c960551c) Thanks [@peterkogo](https://github.com/peterkogo)! - Render edges above nodes when they are within a subflow
- [#5344](https://github.com/xyflow/xyflow/pull/5344) [`2441bf8d`](https://github.com/xyflow/xyflow/commit/2441bf8d97a6b72494f216915d52d5acbeefefde) Thanks [@moklick](https://github.com/moklick)! - Add connectionDragThreshold prop
### Patch Changes
- [#5362](https://github.com/xyflow/xyflow/pull/5362) [`72dc1d60`](https://github.com/xyflow/xyflow/commit/72dc1d602110947e3db83c37b9a9125ee85cf4bc) Thanks [@moklick](https://github.com/moklick)! - Remove pointer events from Panel via CSS while a selection gets dragged
- Updated dependencies [[`72dc1d60`](https://github.com/xyflow/xyflow/commit/72dc1d602110947e3db83c37b9a9125ee85cf4bc), [`90e9247a`](https://github.com/xyflow/xyflow/commit/90e9247adbdfa9d06db97e1d0d895e35c960551c), [`2441bf8d`](https://github.com/xyflow/xyflow/commit/2441bf8d97a6b72494f216915d52d5acbeefefde)]:
- @xyflow/system@0.0.64
## 1.1.1
### Patch Changes
- [#5339](https://github.com/xyflow/xyflow/pull/5339) [`56ebde81`](https://github.com/xyflow/xyflow/commit/56ebde811555775a0a9e2b2f88d35d9511f90c95) Thanks [@jrmajor](https://github.com/jrmajor)! - Fix `onmove`, `onmovestart` and `onmoveend` events not firing
- [#5354](https://github.com/xyflow/xyflow/pull/5354) [`c4312d89`](https://github.com/xyflow/xyflow/commit/c4312d8997ecdc7ef12cfa4efc1fde7131a2b950) Thanks [@moklick](https://github.com/moklick)! - Add TSDoc annotations
- [#5335](https://github.com/xyflow/xyflow/pull/5335) [`8474ba49`](https://github.com/xyflow/xyflow/commit/8474ba49cc1f48b6758a728fe4371f38260952e5) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent proxying objects in the store
- [#5336](https://github.com/xyflow/xyflow/pull/5336) [`d6db97c5`](https://github.com/xyflow/xyflow/commit/d6db97c53597db0eee8edd19beaf20b83f89fabf) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix `useNodeConnections` callbacks firing only when returned signal is used
- [#5333](https://github.com/xyflow/xyflow/pull/5333) [`3d7e8b6b`](https://github.com/xyflow/xyflow/commit/3d7e8b6bb10001ee84d79ca4f6a9fd0053c4a276) Thanks [@peterkogo](https://github.com/peterkogo)! - Add missing type exports
- Updated dependencies [[`c4312d89`](https://github.com/xyflow/xyflow/commit/c4312d8997ecdc7ef12cfa4efc1fde7131a2b950), [`3d7e8b6b`](https://github.com/xyflow/xyflow/commit/3d7e8b6bb10001ee84d79ca4f6a9fd0053c4a276), [`9c61000c`](https://github.com/xyflow/xyflow/commit/9c61000cac6277ce97274cc626fa7266f82dec27)]:
- @xyflow/system@0.0.63
## 1.1.0
### Minor Changes
- [#5299](https://github.com/xyflow/xyflow/pull/5299) [`848b486b`](https://github.com/xyflow/xyflow/commit/848b486b2201b650ecb3317f367a723edb2458e1) Thanks [@printerscanner](https://github.com/printerscanner)! - Add `ariaRole` prop to nodes and edges
- [#5280](https://github.com/xyflow/xyflow/pull/5280) [`dba6faf2`](https://github.com/xyflow/xyflow/commit/dba6faf20e7ec2524d5270d177331d3bd260f3ac) Thanks [@moklick](https://github.com/moklick)! - Improve typing for Nodes
- [#5276](https://github.com/xyflow/xyflow/pull/5276) [`6ce44a05`](https://github.com/xyflow/xyflow/commit/6ce44a05c829068ff5a8416ce3fa4ee6e0eced48) Thanks [@moklick](https://github.com/moklick)! - Add an `ease` and `interpolate` option to all function that alter the viewport
- [#5277](https://github.com/xyflow/xyflow/pull/5277) [`f59730ce`](https://github.com/xyflow/xyflow/commit/f59730ce3530a91f579f6bbd2ea9335680f552ef) Thanks [@printerscanner](https://github.com/printerscanner)! - Add `ariaLabelConfig` prop for customizing UI text like aria labels and descriptions.
- [#5317](https://github.com/xyflow/xyflow/pull/5317) [`09458f52`](https://github.com/xyflow/xyflow/commit/09458f52ff57356e03404c58e9bfdbfd50579850) Thanks [@moklick](https://github.com/moklick)! - Add `domAttributes` option for nodes and edges
- [#5326](https://github.com/xyflow/xyflow/pull/5326) [`050b511c`](https://github.com/xyflow/xyflow/commit/050b511cd6966ba526299f7ca11f9ca4791fd2cf) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent NodeResizer controls to become too small when zooming out
- [#5308](https://github.com/xyflow/xyflow/pull/5308) [`09fab679`](https://github.com/xyflow/xyflow/commit/09fab6794031410c9e9465281d038c3520afe783) Thanks [@printerscanner](https://github.com/printerscanner)! - Focus nodes on tab if not within the viewport and add a new prop `autoPanOnNodeFocus`
### Patch Changes
- [#5327](https://github.com/xyflow/xyflow/pull/5327) [`75ed6dec`](https://github.com/xyflow/xyflow/commit/75ed6decfb3ff408f6136bc1bc712fc4eb3737ef) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent selecting of edges when spacebar is pressed
- [#5294](https://github.com/xyflow/xyflow/pull/5294) [`4a582e23`](https://github.com/xyflow/xyflow/commit/4a582e2371066170b12f3df947f24fe233b542cd) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix data in `EdgeProps` that was not typed correctly
- [#5327](https://github.com/xyflow/xyflow/pull/5327) [`d0c36fdb`](https://github.com/xyflow/xyflow/commit/d0c36fdb708b784cbbd87711512b813eb68757ac) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix initial fitView for SSR
- [#5275](https://github.com/xyflow/xyflow/pull/5275) [`a67bfc09`](https://github.com/xyflow/xyflow/commit/a67bfc09d5b352b5ff3b896c9a9e7138f2c4b20c) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix setting nodesInitialized multiple times
- [#5271](https://github.com/xyflow/xyflow/pull/5271) [`5224a1a2`](https://github.com/xyflow/xyflow/commit/5224a1a252eee59ffc48fffd74872206e19207f3) Thanks [@leejuyuu](https://github.com/leejuyuu)! - Change a11y description inline styles to classes
- [#5327](https://github.com/xyflow/xyflow/pull/5327) [`d0c36fdb`](https://github.com/xyflow/xyflow/commit/d0c36fdb708b784cbbd87711512b813eb68757ac) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix `ViewportPortal` not working when used outside of `SvelteFlow` component
- [#5295](https://github.com/xyflow/xyflow/pull/5295) [`93aefe71`](https://github.com/xyflow/xyflow/commit/93aefe71fb199d0e3463aca38a4564252ab8cbf4) Thanks [@peterkogo](https://github.com/peterkogo)! - Export missing callback types
- [#5327](https://github.com/xyflow/xyflow/pull/5327) [`d0c36fdb`](https://github.com/xyflow/xyflow/commit/d0c36fdb708b784cbbd87711512b813eb68757ac) Thanks [@peterkogo](https://github.com/peterkogo)! - Display nodes correctly in SSR output
- Updated dependencies [[`848b486b`](https://github.com/xyflow/xyflow/commit/848b486b2201b650ecb3317f367a723edb2458e1), [`dba6faf2`](https://github.com/xyflow/xyflow/commit/dba6faf20e7ec2524d5270d177331d3bd260f3ac), [`6ce44a05`](https://github.com/xyflow/xyflow/commit/6ce44a05c829068ff5a8416ce3fa4ee6e0eced48), [`f59730ce`](https://github.com/xyflow/xyflow/commit/f59730ce3530a91f579f6bbd2ea9335680f552ef), [`09458f52`](https://github.com/xyflow/xyflow/commit/09458f52ff57356e03404c58e9bfdbfd50579850), [`050b511c`](https://github.com/xyflow/xyflow/commit/050b511cd6966ba526299f7ca11f9ca4791fd2cf)]:
- @xyflow/system@0.0.62
## 1.0.2
### Patch Changes
+19 -19
View File
@@ -1,6 +1,6 @@
{
"name": "@xyflow/svelte",
"version": "1.0.2",
"version": "1.2.1",
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
"keywords": [
"svelte",
@@ -55,35 +55,35 @@
"@xyflow/system": "workspace:*"
},
"devDependencies": {
"@eslint/js": "^9.24.0",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.20.7",
"@eslint/js": "^9.28.0",
"@sveltejs/adapter-auto": "^6.0.1",
"@sveltejs/kit": "^2.21.4",
"@sveltejs/package": "^2.3.11",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@typescript-eslint/eslint-plugin": "^8.30.1",
"@typescript-eslint/parser": "^8.30.1",
"@sveltejs/vite-plugin-svelte": "^5.1.0",
"@typescript-eslint/eslint-plugin": "^8.34.0",
"@typescript-eslint/parser": "^8.34.0",
"autoprefixer": "^10.4.21",
"cssnano": "^7.0.6",
"cssnano": "^7.0.7",
"dotenv": "^16.5.0",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-svelte": "^3.5.1",
"globals": "^16.0.0",
"postcss": "^8.5.3",
"eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-svelte": "^3.9.2",
"globals": "^16.2.0",
"postcss": "^8.5.4",
"postcss-cli": "^11.0.1",
"postcss-combine-duplicated-selectors": "^10.0.3",
"postcss-import": "^16.1.0",
"postcss-nested": "^7.0.2",
"postcss-rename": "^0.6.1",
"postcss-rename": "^0.8.0",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.27.0",
"svelte-check": "^4.1.6",
"svelte-eslint-parser": "^1.1.2",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.33.19",
"svelte-check": "^4.2.1",
"svelte-eslint-parser": "^1.2.0",
"svelte-preprocess": "^6.0.3",
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"typescript-eslint": "^8.30.1"
"typescript-eslint": "^8.34.0"
},
"peerDependencies": {
"svelte": "^5.25.0"
@@ -1 +1,2 @@
export { portal } from './portal.svelte';
export { hideOnSSR } from './utils.svelte';
@@ -15,25 +15,32 @@ function tryToMount(node: Element, domNode: Element | null, target: Portal | und
}
export function portal(node: Element, target: Portal | undefined) {
// TODO: does this work if called outside of SvelteFlow
const store = useStore();
const { domNode } = $derived(useStore());
let previousTarget: Portal | undefined = target;
tryToMount(node, store.domNode, target);
let destroyEffect: (() => void) | undefined;
// svelte-ignore state_referenced_locally
if (domNode) {
// if the domNode is already mounted, we can directly try to mount the node
tryToMount(node, domNode, target);
} else {
// if the domNode is not mounted yet, we need to wait for it to be ready
destroyEffect = $effect.root(() => {
$effect(() => {
tryToMount(node, domNode, target);
destroyEffect?.();
});
});
}
return {
async update(target: Portal) {
if (target !== previousTarget) {
node.parentNode?.removeChild(node);
previousTarget = target;
}
tryToMount(node, store.domNode, target);
async update(target: Portal | undefined) {
tryToMount(node, domNode, target);
},
destroy() {
if (node.parentNode) {
node.parentNode.removeChild(node);
}
destroyEffect?.();
}
};
}
@@ -0,0 +1,17 @@
export function hideOnSSR(): { value: boolean } {
let hide = $state(typeof window === 'undefined');
if (hide) {
const destroyEffect = $effect.root(() => {
$effect(() => {
hide = false;
destroyEffect?.();
});
});
}
return {
get value() {
return hide;
}
};
}
@@ -44,6 +44,9 @@ export default function zoom(domNode: Element, params: ZoomParams) {
minZoom,
maxZoom,
initialViewport,
onPanZoomStart,
onPanZoom,
onPanZoomEnd,
translateExtent,
paneClickDistance,
setPanZoomInstance,
@@ -58,6 +61,9 @@ export default function zoom(domNode: Element, params: ZoomParams) {
translateExtent,
viewport: initialViewport,
paneClickDistance,
onPanZoom,
onPanZoomStart,
onPanZoomEnd,
onDraggingChange
});
@@ -1,6 +1,6 @@
<script lang="ts">
import { getContext } from 'svelte';
import { portal } from '$lib/actions/portal';
import { hideOnSSR, portal } from '$lib/actions/portal';
import { useStore } from '$lib/store';
import type { EdgeLabelProps } from './types';
@@ -29,6 +29,7 @@
<div
use:portal={'edge-labels'}
style:display={hideOnSSR().value ? 'none' : undefined}
class={['svelte-flow__edge-label', { transparent }, className]}
style:cursor={selectEdgeOnClick ? 'pointer' : undefined}
style:transform="translate(-50%, -50%) translate({x}px,{y}px)"
@@ -1,7 +1,7 @@
<script lang="ts">
import { useStore } from '$lib/store';
import type { Edge } from '$lib/types';
import { XYHandle, type HandleType } from '@xyflow/system';
import { XYHandle, type HandleType, type OnConnectStart } from '@xyflow/system';
import { getContext } from 'svelte';
import { EdgeLabel } from '../EdgeLabel';
import type { EdgeReconnectAnchorProps } from './types';
@@ -12,6 +12,7 @@
position,
class: className,
size = 25,
dragThreshold = 1,
children,
...rest
}: EdgeReconnectAnchorProps = $props();
@@ -52,8 +53,11 @@
let newEdge: Edge | undefined;
let edge = edgeLookup.get(edgeId)!;
reconnecting = true;
onreconnectstart?.(event, edge, type);
const _onConnectStart: OnConnectStart = (evt, params) => {
reconnecting = true;
onreconnectstart?.(event, edge, type);
onconnectstart?.(evt, params);
};
const opposite =
type === 'target'
@@ -79,7 +83,7 @@
cancelConnection,
panBy,
isValidConnection,
onConnectStart: onconnectstart,
onConnectStart: _onConnectStart,
onConnectEnd: onconnectend,
onConnect: (connection) => {
newEdge = { ...edge, ...connection };
@@ -97,7 +101,8 @@
},
updateConnection,
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
getFromHandle: () => store.connection.fromHandle
getFromHandle: () => store.connection.fromHandle,
dragThreshold: dragThreshold ?? store.connectionDragThreshold
});
};
</script>
@@ -10,4 +10,5 @@ export type EdgeReconnectAnchorProps = {
position?: XYPosition;
size?: number;
children?: Snippet;
dragThreshold?: number;
} & HTMLAttributes<HTMLDivElement>;
@@ -89,8 +89,7 @@
}
}
onkeydown = (event: KeyboardEvent) => {
// TODO: Possible Svelte Bug? onkeydown is always firing for the last edge
function onkeydown(event: KeyboardEvent) {
if (!store.disableKeyboardA11y && elementSelectionKeys.includes(event.key) && selectable) {
const { unselectNodesAndEdges, addSelectedEdges } = store;
const unselect = event.key === 'Escape';
@@ -102,7 +101,7 @@
addSelectedEdges([id]);
}
}
};
}
</script>
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
@@ -138,9 +137,10 @@
: `Edge from ${source} to ${target}`}
aria-describedby={focusable ? `${ARIA_EDGE_DESC_KEY}-${store.flowId}` : undefined}
role={edge.ariaRole ?? (focusable ? 'group' : 'img')}
aria-roledescription={edge.ariaRoleDescription || 'edge'}
aria-roledescription="edge"
onkeydown={focusable ? onkeydown : undefined}
tabindex={focusable ? 0 : undefined}
{...edge.domAttributes}
>
<EdgeComponent
{id}
@@ -46,7 +46,6 @@
let store = useStore();
let ariaLabelConfig = $derived(store.ariaLabelConfig);
let prevConnections: Map<string, HandleConnection> | null = null;
$effect.pre(() => {
if (onconnect || ondisconnect) {
@@ -140,7 +139,8 @@
store.onconnectend?.(event, connectionState);
},
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
getFromHandle: () => store.connection.fromHandle
getFromHandle: () => store.connection.fromHandle,
dragThreshold: store.connectionDragThreshold
});
}
}
@@ -5,7 +5,8 @@
errorMessages,
isInputDOMNode,
nodeHasDimensions,
Position
Position,
getNodesInside
} from '@xyflow/system';
import drag from '$lib/actions/drag';
@@ -65,7 +66,9 @@
let draggable = $derived(_draggable ?? store.nodesDraggable);
let selectable = $derived(_selectable ?? store.elementsSelectable);
let connectable = $derived(_connectable ?? store.nodesConnectable);
let initialized = $derived(nodeHasDimensions(node) && !!node.internals.handleBounds);
let hasDimensions = $derived(nodeHasDimensions(node));
let hasHandleBounds = $derived(!!node.internals.handleBounds);
let isInitialized = $derived(hasDimensions && hasHandleBounds);
let focusable = $derived(_focusable ?? store.nodesFocusable);
function isInParentLookup(id: string) {
@@ -149,7 +152,7 @@
$effect(() => {
/* eslint-disable @typescript-eslint/no-unused-expressions */
if (resizeObserver && (!initialized || nodeRef !== prevNodeRef)) {
if (resizeObserver && (!isInitialized || nodeRef !== prevNodeRef)) {
prevNodeRef && resizeObserver.unobserve(prevNodeRef);
nodeRef && resizeObserver.observe(nodeRef);
prevNodeRef = nodeRef;
@@ -197,6 +200,34 @@
store.moveSelectedNodes(arrowKeyDiffs[event.key], event.shiftKey ? 4 : 1);
}
}
const onFocus = () => {
if (
store.disableKeyboardA11y ||
!store.autoPanOnNodeFocus ||
!nodeRef?.matches(':focus-visible')
) {
return;
}
const { width, height, viewport } = store;
const withinViewport =
getNodesInside(
new Map([[id, node]]),
{ x: 0, y: 0, width, height },
[viewport.x, viewport.y, viewport.zoom],
true
).length > 0;
if (!withinViewport) {
store.setCenter(
node.position.x + (node.measured.width ?? 0) / 2,
node.position.y + (node.measured.height ?? 0) / 2,
{ zoom: viewport.zoom }
);
}
};
</script>
{#if !hidden}
@@ -236,7 +267,7 @@
class:parent={isParent}
style:z-index={zIndex}
style:transform="translate({positionX}px, {positionY}px)"
style:visibility={initialized ? 'visible' : 'hidden'}
style:visibility={hasDimensions ? 'visible' : 'hidden'}
style={nodeStyle}
onclick={onSelectNodeHandler}
onpointerenter={onnodepointerenter
@@ -252,12 +283,14 @@
? (event) => onnodecontextmenu({ node: userNode, event })
: undefined}
onkeydown={focusable ? onKeyDown : undefined}
onfocus={focusable ? onFocus : undefined}
tabIndex={focusable ? 0 : undefined}
role={node.ariaRole ?? (focusable ? 'group' : undefined)}
aria-roledescription={node.ariaRoleDescription || 'node'}
aria-roledescription="node"
aria-describedby={store.disableKeyboardA11y
? undefined
: `${ARIA_NODE_DESC_KEY}-${store.flowId}`}
{...node.domAttributes}
>
<NodeComponent
{data}
@@ -1,10 +1,14 @@
<script lang="ts">
import { portal } from '$lib/actions/portal';
import { hideOnSSR, portal } from '$lib/actions/portal';
import type { ViewportPortalProps } from './types';
let { target = 'front', children, ...rest }: ViewportPortalProps = $props();
</script>
<div use:portal={`viewport-${target}`} {...rest}>
<div
use:portal={`viewport-${target}`}
style:display={hideOnSSR().value ? 'none' : undefined}
{...rest}
>
{@render children?.()}
</div>
@@ -1,19 +1,11 @@
<script lang="ts">
import type { PanelProps } from './types';
import { useStore } from '$lib/store';
let { position = 'top-right', style, class: className, children, ...rest }: PanelProps = $props();
let store = $derived(useStore());
let positionClasses = $derived(`${position}`.split('-'));
</script>
<div
class={['svelte-flow__panel', className, ...positionClasses]}
{style}
style:pointer-events={store.selectionRectMode ? 'none' : ''}
{...rest}
>
<div class={['svelte-flow__panel', className, ...positionClasses]} {style} {...rest}>
{@render children?.()}
</div>
@@ -64,6 +64,7 @@
fitViewOptions,
nodeOrigin,
nodeDragThreshold,
connectionDragThreshold,
minZoom,
maxZoom,
initialViewport,
@@ -84,6 +85,7 @@
elevateNodesOnSelect,
elevateEdgesOnSelect,
nodesDraggable,
autoPanOnNodeFocus,
nodesConnectable,
elementsSelectable,
nodesFocusable,
@@ -100,6 +102,16 @@
type OnlyDivAttributes<T> = {
[K in keyof T]: K extends keyof HTMLAttributes<HTMLDivElement> ? T[K] : never;
};
// Undo scroll events, preventing viewport from shifting when nodes outside of it are focused
function wrapperOnScroll(e: UIEvent & { currentTarget: EventTarget & HTMLDivElement }) {
e.currentTarget.scrollTo({ top: 0, left: 0, behavior: 'auto' });
// Forward the event to any existing onscroll handler if needed
if (rest.onscroll) {
rest.onscroll(e);
}
}
</script>
<div
@@ -111,6 +123,7 @@
class={['svelte-flow', 'svelte-flow__container', className, colorMode]}
data-testid="svelte-flow__wrapper"
role="application"
onscroll={wrapperOnScroll}
{...divAttributes satisfies OnlyDivAttributes<typeof divAttributes>}
>
{@render children?.()}
@@ -170,6 +170,12 @@ export type SvelteFlowProps<
* @default 0
*/
nodeClickDistance?: number;
/**
* The threshold in pixels that the mouse must move before a connection line starts to drag.
* This is useful to prevent accidental connections when clicking on a handle.
* @default 1
*/
connectionDragThreshold?: number;
/** Minimum zoom level
* @default 0.5
*/
@@ -232,6 +238,11 @@ export type SvelteFlowProps<
* @default true
*/
nodesDraggable?: boolean;
/**
* When `true`, the viewport will pan when a node is focused.
* @default true
*/
autoPanOnNodeFocus?: boolean;
/**
* Controls if all nodes should be connectable to each other
* @default true
@@ -17,6 +17,8 @@ type UseNodeConnectionsParams = {
onDisconnect?: (connections: Connection[]) => void;
};
type ConnectionMap = Map<string, NodeConnection>;
const initialConnections: NodeConnection[] = [];
/**
@@ -42,26 +44,46 @@ export function useNodeConnections({
const contextNodeId = getContext<string>('svelteflow__node_id');
const nodeId = id ?? contextNodeId;
let prevConnections: Map<string, NodeConnection> = new Map();
let connectionMaps: { previous: ConnectionMap; next: ConnectionMap } = {
previous: new Map(),
next: new Map()
};
let connectionsArray: NodeConnection[] = initialConnections;
const connections = $derived.by(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
edges;
const prevConnections = connectionMaps.next;
const nextConnections =
connectionLookup.get(
`${nodeId}${handleType ? (handleId ? `-${handleType}-${handleId}` : `-${handleType}`) : ''}`
) ?? new Map();
if (!areConnectionMapsEqual(nextConnections, prevConnections)) {
if (onConnect) handleConnectionChange(nextConnections, prevConnections, onConnect);
if (onDisconnect) handleConnectionChange(prevConnections, nextConnections, onDisconnect);
prevConnections = nextConnections;
connectionMaps = {
previous: prevConnections,
next: nextConnections
};
connectionsArray = Array.from(nextConnections.values() || initialConnections);
}
return connectionsArray;
});
$effect(() => {
// We subscribe to changes to the connections only when onConnect/onDisconnect are provided
if (onConnect) {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
connections;
handleConnectionChange(connectionMaps.next, connectionMaps.previous, onConnect);
}
if (onDisconnect) {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
connections;
handleConnectionChange(connectionMaps.previous, connectionMaps.next, onDisconnect);
}
});
return {
get current() {
return connections;
@@ -125,7 +125,7 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
* Returns all nodes that intersect with the given node or rect.
*
* @param node - the node or rect to check for intersections
* @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed node or rect
* @param partially - true by default, if set to false, only nodes that are fully intersecting will be returned
* @param nodes - optional nodes array to check for intersections
*
* @returns an array of intersecting nodes
@@ -366,28 +366,8 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
return Promise.resolve(true);
},
getViewport: () => $state.snapshot(store.viewport),
setCenter: async (x, y, options) => {
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : store.maxZoom;
const currentPanZoom = store.panZoom;
if (!currentPanZoom) {
return Promise.resolve(false);
}
await currentPanZoom.setViewport(
{
x: store.width / 2 - x * nextZoom,
y: store.height / 2 - y * nextZoom,
zoom: nextZoom
},
{ duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate }
);
return Promise.resolve(true);
},
fitView: (options?: FitViewOptions) => {
return store.fitView(options);
},
setCenter: async (x, y, options) => store.setCenter(x, y, options),
fitView: (options?: FitViewOptions) => store.fitView(options),
fitBounds: async (bounds: Rect, options?: FitBoundsOptions) => {
if (!store.panZoom) {
return Promise.resolve(false);
@@ -432,7 +412,11 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
const partiallyVisible = partially && overlappingArea > 0;
return partiallyVisible || overlappingArea >= nodeRect.width * nodeRect.height;
return (
partiallyVisible ||
overlappingArea >= currNodeRect.width * currNodeRect.height ||
overlappingArea >= nodeRect.width * nodeRect.height
);
});
},
isNodeIntersecting: (
+7 -1
View File
@@ -105,6 +105,9 @@ export {
type OnResizeStart,
type OnResize,
type OnResizeEnd,
type OnReconnect,
type OnReconnectStart,
type OnReconnectEnd,
type ControlPosition,
type ControlLinePosition,
ResizeControlVariant,
@@ -113,7 +116,10 @@ export {
type ResizeDragEvent,
type IsValidConnection,
type NodeConnection,
type AriaLabelConfig
type AriaLabelConfig,
type SetCenter,
type SetViewport,
type FitBounds
} from '@xyflow/system';
// system utils
@@ -23,7 +23,6 @@
buttonColor,
buttonColorHover,
buttonBorderColor,
'aria-label': ariaLabel,
fitViewOptions,
children,
before,
@@ -14,6 +14,7 @@
handleStyle,
lineClass,
lineStyle,
autoScale = true,
...rest
}: NodeResizerProps = $props();
</script>
@@ -25,11 +26,19 @@
style={lineStyle}
{nodeId}
{position}
{autoScale}
variant={ResizeControlVariant.Line}
{...rest}
/>
{/each}
{#each XY_RESIZER_HANDLE_POSITIONS as position (position)}
<ResizeControl class={handleClass} style={handleStyle} {nodeId} {position} {...rest} />
<ResizeControl
class={handleClass}
style={handleStyle}
{nodeId}
{position}
{autoScale}
{...rest}
/>
{/each}
{/if}
@@ -22,6 +22,7 @@
maxWidth = Number.MAX_VALUE,
maxHeight = Number.MAX_VALUE,
keepAspectRatio = false,
autoScale = true,
shouldResize,
onResizeStart,
onResize,
@@ -40,10 +41,10 @@
let resizeControlRef: HTMLDivElement;
let resizer: XYResizerInstance | null = $state(null);
let isLineVariant = $derived(variant === ResizeControlVariant.Line);
let controlPosition = $derived.by(() => {
let defaultPosition = (
variant === ResizeControlVariant.Line ? 'right' : 'bottom-right'
) as ControlPosition;
let defaultPosition = (isLineVariant ? 'right' : 'bottom-right') as ControlPosition;
return position ?? defaultPosition;
});
@@ -119,8 +120,9 @@
<div
class={['svelte-flow__resize-control', store.noDragClass, ...positionClasses, variant, className]}
bind:this={resizeControlRef}
style:border-color={variant === ResizeControlVariant.Line ? color : undefined}
style:background-color={variant === ResizeControlVariant.Line ? undefined : color}
style:border-color={isLineVariant ? color : undefined}
style:background-color={isLineVariant ? undefined : color}
style:scale={isLineVariant || !autoScale ? undefined : Math.max(1 / store.viewport.zoom, 1)}
{...rest}
>
{@render children?.()}
@@ -36,6 +36,8 @@ export type NodeResizerProps = {
maxHeight?: number;
/** Keep aspect ratio when resizing */
keepAspectRatio?: boolean;
/** Automatically scale the node when resizing */
autoScale?: boolean;
/** Callback to determine if node should resize */
shouldResize?: ShouldResize;
/** Callback called when resizing starts */
@@ -55,6 +57,7 @@ export type ResizeControlProps = Pick<
| 'maxWidth'
| 'maxHeight'
| 'keepAspectRatio'
| 'autoScale'
| 'shouldResize'
| 'onResizeStart'
| 'onResize'
@@ -2,7 +2,7 @@
import { getContext } from 'svelte';
import { Position, getNodeToolbarTransform } from '@xyflow/system';
import { portal } from '$lib/actions/portal';
import { hideOnSSR, portal } from '$lib/actions/portal';
import { useStore } from '$lib/store';
import { useSvelteFlow } from '$lib/hooks/useSvelteFlow.svelte';
@@ -68,6 +68,7 @@
{#if store.domNode && isActive && toolbarNodes}
<div
use:portal={'root'}
style:display={hideOnSSR().value ? 'none' : undefined}
class="svelte-flow__node-toolbar"
data-id={toolbarNodes.reduce((acc, node) => `${acc}${node.id} `, '').trim()}
style:position="absolute"
+23 -1
View File
@@ -14,7 +14,8 @@ import {
type ConnectionState,
updateAbsolutePositions,
snapPosition,
calculateNodePosition
calculateNodePosition,
type SetCenterOptions
} from '@xyflow/system';
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
@@ -126,6 +127,26 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
return fitViewResolver.promise;
}
async function setCenter(x: number, y: number, options?: SetCenterOptions) {
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : store.maxZoom;
const currentPanZoom = store.panZoom;
if (!currentPanZoom) {
return Promise.resolve(false);
}
await currentPanZoom.setViewport(
{
x: store.width / 2 - x * nextZoom,
y: store.height / 2 - y * nextZoom,
zoom: nextZoom
},
{ duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate }
);
return Promise.resolve(true);
}
function zoomBy(factor: number, options?: ViewportHelperFunctionOptions) {
const panZoom = store.panZoom;
if (!panZoom) {
@@ -372,6 +393,7 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
zoomIn,
zoomOut,
fitView,
setCenter,
setMinZoom,
setMaxZoom,
setTranslateExtent,
@@ -84,6 +84,25 @@ export const initialEdgeTypes = {
step: StepEdgeInternal
};
function getInitialViewport(
// This is just used to make sure adoptUserNodes is called before we calculate the viewport
_nodesInitialized: boolean,
fitView: boolean | undefined,
initialViewport: Viewport | undefined,
width: number,
height: number,
nodeLookup: NodeLookup
) {
if (fitView && !initialViewport && width && height) {
const bounds = getInternalNodesBounds(nodeLookup, {
filter: (node) => !!((node.width || node.initialWidth) && (node.height || node.initialHeight))
});
return getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
} else {
return initialViewport ?? { x: 0, y: 0, zoom: 1 };
}
}
export function getInitialStore<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
signals: StoreSignals<NodeType, EdgeType>
) {
@@ -91,10 +110,10 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
// Inline classes have some performance implications but we just call it once (max twice).
class SvelteFlowStore {
flowId: string = $derived(signals.props.id ?? '1');
domNode = $state<HTMLDivElement | null>(null);
panZoom: PanZoomInstance | null = $state(null);
width = $state<number>(signals.width ?? 0);
height = $state<number>(signals.height ?? 0);
domNode = $state.raw<HTMLDivElement | null>(null);
panZoom: PanZoomInstance | null = $state.raw(null);
width = $state.raw<number>(signals.width ?? 0);
height = $state.raw<number>(signals.height ?? 0);
nodesInitialized: boolean = $derived.by(() => {
const nodesInitialized = adoptUserNodes(signals.nodes, this.nodeLookup, this.parentLookup, {
@@ -266,6 +285,8 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
nodeDragThreshold: number = $derived(signals.props.nodeDragThreshold ?? 1);
autoPanOnNodeDrag: boolean = $derived(signals.props.autoPanOnNodeDrag ?? true);
autoPanOnConnect: boolean = $derived(signals.props.autoPanOnConnect ?? true);
autoPanOnNodeFocus: boolean = $derived(signals.props.autoPanOnNodeFocus ?? true);
connectionDragThreshold: number = $derived(signals.props.connectionDragThreshold ?? 1);
fitViewQueued: boolean = signals.props.fitView ?? false;
fitViewOptions: FitViewOptions | undefined = signals.props.fitViewOptions;
@@ -273,16 +294,16 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
snapGrid: SnapGrid | null = $derived(signals.props.snapGrid ?? null);
dragging: boolean = $state(false);
selectionRect: SelectionRect | null = $state(null);
dragging: boolean = $state.raw(false);
selectionRect: SelectionRect | null = $state.raw(null);
selectionKeyPressed: boolean = $state(false);
multiselectionKeyPressed: boolean = $state(false);
deleteKeyPressed: boolean = $state(false);
panActivationKeyPressed: boolean = $state(false);
zoomActivationKeyPressed: boolean = $state(false);
selectionRectMode: string | null = $state(null);
ariaLiveMessage = $state<string>('');
selectionKeyPressed: boolean = $state.raw(false);
multiselectionKeyPressed: boolean = $state.raw(false);
deleteKeyPressed: boolean = $state.raw(false);
panActivationKeyPressed: boolean = $state.raw(false);
zoomActivationKeyPressed: boolean = $state.raw(false);
selectionRectMode: string | null = $state.raw(null);
ariaLiveMessage = $state.raw<string>('');
selectionMode: SelectionMode = $derived(signals.props.selectionMode ?? SelectionMode.Partial);
nodeTypes: NodeTypes = $derived({ ...initialNodeTypes, ...signals.props.nodeTypes });
@@ -297,7 +318,16 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
// _viewport is the internal viewport.
// when binding to viewport, we operate on signals.viewport instead
_viewport: Viewport = $state(signals.props.initialViewport ?? { x: 0, y: 0, zoom: 1 });
_viewport: Viewport = $state.raw(
getInitialViewport(
this.nodesInitialized,
signals.props.fitView,
signals.props.initialViewport,
this.width,
this.height,
this.nodeLookup
)
);
get viewport() {
return signals.viewport ?? this._viewport;
}
@@ -309,21 +339,21 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
}
// _connection is viewport independent and originating from XYHandle
_connection: ConnectionState = $state(initialConnection);
_connection: ConnectionState = $state.raw(initialConnection);
// We derive a viewport dependent connection here
connection: ConnectionState = $derived.by(() => {
if (this._connection.inProgress) {
return {
...this._connection,
to: pointToRendererPoint(this._connection.to, [
this.viewport.x,
this.viewport.y,
this.viewport.zoom
])
};
} else {
if (!this._connection.inProgress) {
return this._connection;
}
return {
...this._connection,
to: pointToRendererPoint(this._connection.to, [
this.viewport.x,
this.viewport.y,
this.viewport.zoom
])
};
});
connectionMode: ConnectionMode = $derived(
signals.props.connectionMode ?? ConnectionMode.Strict
@@ -363,7 +393,7 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
clickConnect?: boolean = $derived(signals.props.clickConnect ?? true);
onclickconnectstart?: OnConnectStart = $derived(signals.props.onclickconnectstart);
onclickconnectend?: OnConnectEnd = $derived(signals.props.onclickconnectend);
clickConnectStartHandle: Pick<Handle, 'id' | 'nodeId' | 'type'> | null = $state(null);
clickConnectStartHandle: Pick<Handle, 'id' | 'nodeId' | 'type'> | null = $state.raw(null);
onselectiondrag?: OnSelectionDrag<NodeType> = $derived(signals.props.onselectiondrag);
onselectiondragstart?: OnSelectionDrag<NodeType> = $derived(signals.props.onselectiondragstart);
@@ -409,15 +439,6 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
);
constructor() {
// Process intial fitView here
if (signals.props.fitView && !signals.props.initialViewport && this.width && this.height) {
const bounds = getInternalNodesBounds(this.nodeLookup, {
filter: (node) =>
!!((node.width || node.initialWidth) && (node.height || node.initialHeight))
});
this.viewport = getViewportForBounds(bounds, this.width, this.height, 0.5, 2, 0.1);
}
if (process.env.NODE_ENV === 'development') {
warnIfDeeplyReactive(signals.nodes, 'nodes');
warnIfDeeplyReactive(signals.edges, 'edges');
+3 -1
View File
@@ -6,7 +6,8 @@ import type {
UpdateNodePositions,
CoordinateExtent,
UpdateConnection,
Viewport
Viewport,
SetCenter
} from '@xyflow/system';
import type { getInitialStore } from './initial-store.svelte';
@@ -24,6 +25,7 @@ export type SvelteFlowStoreActions<NodeType extends Node = Node, EdgeType extend
setTranslateExtent: (extent: CoordinateExtent) => void;
setPaneClickDistance: (distance: number) => void;
fitView: (options?: FitViewOptions) => Promise<boolean>;
setCenter: SetCenter;
updateNodePositions: UpdateNodePositions;
updateNodeInternals: (updates: Map<string, InternalNodeUpdate>) => void;
unselectNodesAndEdges: (params?: { nodes?: NodeType[]; edges?: EdgeType[] }) => void;
+14 -3
View File
@@ -1,4 +1,5 @@
import type { Component } from 'svelte';
import type { ClassValue, HTMLAttributes, SVGAttributes } from 'svelte/elements';
import type {
EdgeBase,
BezierPathOptions,
@@ -9,7 +10,6 @@ import type {
} from '@xyflow/system';
import type { Node } from '$lib/types';
import type { ClassValue, HTMLAttributes } from 'svelte/elements';
/**
* An `Edge` is the complete description with everything Svelte Flow needs to know in order to
@@ -30,6 +30,13 @@ export type Edge<
* @default "group"
*/
ariaRole?: HTMLAttributes<HTMLElement>['role'];
/**
* General escape hatch for adding custom attributes to the edge's DOM element.
*/
domAttributes?: Omit<
SVGAttributes<SVGGElement>,
'id' | 'style' | 'class' | 'role' | 'aria-label'
>;
};
export type BaseEdgeProps = Pick<
@@ -43,11 +50,15 @@ export type BaseEdgeProps = Pick<
labelX?: number;
/** The y coordinate of the label */
labelY?: number;
/** Marker at start of edge
/**
* The id of the SVG marker to use at the start of the edge. This should be defined in a
* `<defs>` element in a separate SVG document or element. Use the format "url(#markerId)" where markerId is the id of your marker definition.
* @example 'url(#arrow)'
*/
markerStart?: string;
/** Marker at end of edge
/**
* The id of the SVG marker to use at the end of the edge. This should be defined in a `<defs>`
* element in a separate SVG document or element. Use the format "url(#markerId)" where markerId is the id of your marker definition.
* @example 'url(#arrow)'
*/
markerEnd?: string;
+12
View File
@@ -21,8 +21,16 @@ export type ConnectionData = {
connectionStatus: string | null;
};
/**
* @inline
*/
export type FitViewOptions<NodeType extends Node = Node> = FitViewOptionsBase<NodeType>;
/**
* This type can be used to type the `onDelete` function with a custom node and edge type.
*
* @public
*/
export type OnDelete<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (params: {
nodes: NodeType[];
edges: EdgeType[];
@@ -40,6 +48,10 @@ export type OnBeforeDelete<
EdgeType extends Edge = Edge
> = OnBeforeDeleteBase<NodeType, EdgeType>;
/**
* This type can be used to type the `isValidConnection` function.
* If the function returns `true`, the connection is valid and can be created.
*/
export type IsValidConnection<EdgeType extends Edge = Edge> = (
edge: EdgeType | Connection
) => boolean;
+16 -2
View File
@@ -1,5 +1,5 @@
import type { Component } from 'svelte';
import type { ClassValue, HTMLAttributes } from 'svelte/elements';
import type { ClassValue, HTMLAttributes, DOMAttributes } from 'svelte/elements';
import type { InternalNodeBase, NodeBase, NodeProps as NodePropsBase } from '@xyflow/system';
/**
@@ -25,7 +25,21 @@ export type Node<
* The ARIA role attribute for the node element, used for accessibility.
* @default "group"
*/
ariaRole?: HTMLAttributes<HTMLElement>['role'];
ariaRole?: HTMLAttributes<HTMLDivElement>['role'];
/**
* General escape hatch for adding custom attributes to the node's DOM element.
*/
domAttributes?: Omit<
HTMLAttributes<HTMLDivElement>,
| 'id'
| 'style'
| 'class'
| 'draggable'
| 'role'
| 'aria-label'
| keyof DOMAttributes<HTMLDivElement>
>;
};
// @todo: currently generics for nodes are not really supported
+42
View File
@@ -1,5 +1,47 @@
# @xyflow/system
## 0.0.65
### Patch Changes
- [#5370](https://github.com/xyflow/xyflow/pull/5370) [`26f2cdd7`](https://github.com/xyflow/xyflow/commit/26f2cdd720fc2c8fb337d3af13b82dab6a90fb60) Thanks [@moklick](https://github.com/moklick)! - Only fire connection end events if connection was started
## 0.0.64
### Patch Changes
- [#5362](https://github.com/xyflow/xyflow/pull/5362) [`72dc1d60`](https://github.com/xyflow/xyflow/commit/72dc1d602110947e3db83c37b9a9125ee85cf4bc) Thanks [@moklick](https://github.com/moklick)! - Remove pointer events from Panel via CSS while a selection gets dragged
- [#5361](https://github.com/xyflow/xyflow/pull/5361) [`90e9247a`](https://github.com/xyflow/xyflow/commit/90e9247adbdfa9d06db97e1d0d895e35c960551c) Thanks [@peterkogo](https://github.com/peterkogo)! - Render edges above nodes when they are within a subflow
- [#5344](https://github.com/xyflow/xyflow/pull/5344) [`2441bf8d`](https://github.com/xyflow/xyflow/commit/2441bf8d97a6b72494f216915d52d5acbeefefde) Thanks [@moklick](https://github.com/moklick)! - Add connectionDragThreshold prop
## 0.0.63
### Patch Changes
- [#5354](https://github.com/xyflow/xyflow/pull/5354) [`c4312d89`](https://github.com/xyflow/xyflow/commit/c4312d8997ecdc7ef12cfa4efc1fde7131a2b950) Thanks [@moklick](https://github.com/moklick)! - Add TSDoc annotations
- [#5333](https://github.com/xyflow/xyflow/pull/5333) [`3d7e8b6b`](https://github.com/xyflow/xyflow/commit/3d7e8b6bb10001ee84d79ca4f6a9fd0053c4a276) Thanks [@peterkogo](https://github.com/peterkogo)! - Add missing type exports
- [#5350](https://github.com/xyflow/xyflow/pull/5350) [`9c61000c`](https://github.com/xyflow/xyflow/commit/9c61000cac6277ce97274cc626fa7266f82dec27) Thanks [@moklick](https://github.com/moklick)! - Only send node position updates if positions changed
## 0.0.62
### Patch Changes
- [#5299](https://github.com/xyflow/xyflow/pull/5299) [`848b486b`](https://github.com/xyflow/xyflow/commit/848b486b2201b650ecb3317f367a723edb2458e1) Thanks [@printerscanner](https://github.com/printerscanner)! - Add `ariaRole` prop to nodes and edges
- [#5280](https://github.com/xyflow/xyflow/pull/5280) [`dba6faf2`](https://github.com/xyflow/xyflow/commit/dba6faf20e7ec2524d5270d177331d3bd260f3ac) Thanks [@moklick](https://github.com/moklick)! - Improve typing for Nodes
- [#5276](https://github.com/xyflow/xyflow/pull/5276) [`6ce44a05`](https://github.com/xyflow/xyflow/commit/6ce44a05c829068ff5a8416ce3fa4ee6e0eced48) Thanks [@moklick](https://github.com/moklick)! - Add an `ease` and `interpolate` option to all function that alter the viewport
- [#5277](https://github.com/xyflow/xyflow/pull/5277) [`f59730ce`](https://github.com/xyflow/xyflow/commit/f59730ce3530a91f579f6bbd2ea9335680f552ef) Thanks [@printerscanner](https://github.com/printerscanner)! - Add `ariaLabelConfig` prop for customizing UI text like aria labels and descriptions.
- [#5317](https://github.com/xyflow/xyflow/pull/5317) [`09458f52`](https://github.com/xyflow/xyflow/commit/09458f52ff57356e03404c58e9bfdbfd50579850) Thanks [@moklick](https://github.com/moklick)! - Add `domAttributes` option for nodes and edges
- [#5326](https://github.com/xyflow/xyflow/pull/5326) [`050b511c`](https://github.com/xyflow/xyflow/commit/050b511cd6966ba526299f7ca11f9ca4791fd2cf) Thanks [@peterkogo](https://github.com/peterkogo)! - Prevent NodeResizer controls to become too small when zooming out
## 0.0.61
### Patch Changes
+41 -26
View File
@@ -1,47 +1,62 @@
# @xyflow/system
Core system that powers React Flow and Svelte Flow.
Core system utilities powering [React Flow](https://reactflow.dev) and [Svelte Flow](https://svelteflow.dev).
## Installation
> **Note:** This package is designed as a shared vanilla utility layer for React Flow and Svelte Flow. It is not intended for use with unrelated libraries.
```sh
npm install @xyflow/system
## Installation
```sh
pnpm add @xyflow/system
```
## What is this package about?
## What is this package?
The @xyflow/system package was created to have a place for vanilla utils for React Flow and Svelte Flow. The package exports helpers for edge creation, pan and zoom, dragging of nodes, general utils and lots of types. All the helpers are specifically built for React Flow and Svelte Flow so it's probably not too interesting to use them with other libraries.
`@xyflow/system` provides core, framework-agnostic helpers and types for building node-based editors and flow diagrams. It contains the logic and utilities that are shared between React Flow and Svelte Flow, such as edge path calculations, pan/zoom, node dragging, and more.
### XYPanZoom
## Features
Adds zoom and pan for the pane.
- **Pan & Zoom (`XYPanZoom`)**: Utilities for adding interactive pan and zoom to your canvas.
- **Dragging (`XYDrag`)**: Helpers for node and selection dragging.
- **Handles/Connections (`XYHandle`)**: Logic for drawing and managing connection lines between nodes.
- **Minimap (`XYMiniMap`)**: Interactive minimap utilities for overview and navigation.
- **Edge Utilities**: Functions for SVG edge path creation (bezier, straight, step, smoothstep, etc.).
- **Store Utilities**: Helpers for managing and updating flow state.
- **DOM Utilities**: Functions for DOM measurements and interactions.
- **Marker Utilities**: Helpers for SVG markers on edges.
- **Graph Utilities**: Functions for working with nodes, edges, and graph structure.
- **General Utilities**: Miscellaneous helpers used throughout the system.
- **Types & Constants**: Shared types, enums, and constants for consistent data structures.
### XYDrag
## Usage
Adds drag for nodes and selection.
You can import any utility, type, or helper directly from the package:
### XYHandle
```ts
import { getBezierPath, getConnectedEdges, Position, XYPanZoom } from '@xyflow/system';
```
Adds connection line drawing.
### Example: Bezier Edge Path Creation
### XYMinimap
```ts
import { getBezierPath, Position } from '@xyflow/system';
Adds interactive mini map (zoom and pan).
const [path, labelX, labelY] = getBezierPath({
sourceX: 0,
sourceY: 20,
sourcePosition: Position.Right,
targetX: 150,
targetY: 100,
targetPosition: Position.Left,
});
```
### Edge utils
## API Reference
Util function for SVG edge path creating.
There is currently no dedicated API documentation for this package. For details on available utilities, types, and helpers, please refer to the [source code](./src) or check out the [React Flow documentation](https://reactflow.dev/api-reference/) where we are documenting a lot of stuff from this package.
### Store utils
## Contributing
Helpers for store functions.
### Dom utils
### Marker utils
### Graph utils
### General utils
See the main [xyflow repository](https://github.com/xyflow/xyflow) for contribution guidelines.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@xyflow/system",
"version": "0.0.61",
"version": "0.0.65",
"description": "xyflow core system that powers React Flow and Svelte Flow.",
"keywords": [
"node-based UI",
+4
View File
@@ -265,6 +265,10 @@ svg.xy-flow__connectionline {
pointer-events: all;
}
.xy-flow__pane.selection .xy-flow__panel {
pointer-events: none;
}
.xy-flow__panel {
position: absolute;
z-index: 5;
+3 -3
View File
@@ -28,12 +28,12 @@
/* handle styles */
.xy-flow__resize-control.handle {
width: 4px;
height: 4px;
width: 5px;
height: 5px;
border: 1px solid #fff;
border-radius: 1px;
background-color: var(--xy-resize-background-color, var(--xy-resize-background-color-default));
transform: translate(-50%, -50%);
translate: -50% -50%;
}
.xy-flow__resize-control.handle.left {
-5
View File
@@ -40,11 +40,6 @@ export type EdgeBase<
* This property sets the width of that invisible path.
*/
interactionWidth?: number;
/**
* A description of the edge's, used for accessibility.
* @default "edge"
*/
ariaRoleDescription?: string;
};
export type SmoothStepPathOptions = {
+46
View File
@@ -13,16 +13,56 @@ import { EdgeBase } from '..';
export type Project = (position: XYPosition) => XYPosition;
/**
* This type is used to define the `onMove` handler.
*/
export type OnMove = (event: MouseEvent | TouchEvent | null, viewport: Viewport) => void;
export type OnMoveStart = OnMove;
export type OnMoveEnd = OnMove;
/**
* @inline
*/
export type ZoomInOut = (options?: ViewportHelperFunctionOptions) => Promise<boolean>;
/**
* @inline
*/
export type ZoomTo = (zoomLevel: number, options?: ViewportHelperFunctionOptions) => Promise<boolean>;
/**
* @inline
*/
export type GetZoom = () => number;
/**
* @inline
*/
export type GetViewport = () => Viewport;
/**
* The `SetViewport` function is used to set the viewport of the flow.
*
* @inline
* @param viewport - The viewport to set.
* @param options - Optional parameters to control the animation and easing of the viewport change.
*/
export type SetViewport = (viewport: Viewport, options?: ViewportHelperFunctionOptions) => Promise<boolean>;
/**
* The `SetCenter` function is used to set the center of the flow viewport to a specific position
*
* @inline
* @param x - x coordinate
* @param y - y coordinate
* @param options - Optional parameters to control the animation and easing of the viewport change.
*/
export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => Promise<boolean>;
/**
* The `FitBounds` function is used to fit the flow viewport to the bounds of the nodes.
*
* @inline
* @param bounds - The bounds to fit the viewport to.
* @param options - Optional parameters to control the animation and easing of the viewport change.
*/
export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => Promise<boolean>;
/**
@@ -179,10 +219,16 @@ export type ViewportHelperFunctionOptions = {
interpolate?: 'smooth' | 'linear';
};
/**
* @inline
*/
export type SetCenterOptions = ViewportHelperFunctionOptions & {
zoom?: number;
};
/**
* @inline
*/
export type FitBoundsOptions = ViewportHelperFunctionOptions & {
padding?: number;
};
-5
View File
@@ -73,11 +73,6 @@ export type NodeBase<
*/
origin?: NodeOrigin;
handles?: NodeHandle[];
/**
* A description of the node's role, used for accessibility.
* @default "node"
*/
ariaRoleDescription?: string;
measured?: {
width?: number;
height?: number;
+14 -5
View File
@@ -30,21 +30,30 @@ export type GetEdgeZIndexParams = {
elevateOnSelect?: boolean;
};
/**
* Returns the z-index for an edge based on the node it connects and whether it is selected.
* By default, edges are rendered below nodes. This behaviour is different for edges that are
* connected to nodes with a parent, as they are rendered above the parent node.
*/
export function getElevatedEdgeZIndex({
sourceNode,
targetNode,
selected = false,
zIndex = 0,
zIndex,
elevateOnSelect = false,
}: GetEdgeZIndexParams): number {
if (!elevateOnSelect) {
if (zIndex !== undefined) {
return zIndex;
}
const edgeOrConnectedNodeSelected = selected || targetNode.selected || sourceNode.selected;
const selectedZIndex = Math.max(sourceNode.internals.z || 0, targetNode.internals.z || 0, 1000);
const edgeZ = elevateOnSelect && selected ? 1000 : 0;
return zIndex + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
const nodeZ = Math.max(
sourceNode.parentId ? sourceNode.internals.z : 0,
targetNode.parentId ? targetNode.internals.z : 0
);
return edgeZ + nodeZ;
}
type IsEdgeVisibleParams = {
+1 -1
View File
@@ -232,7 +232,7 @@ function calculateChildXYZ<NodeType extends NodeBase>(
return {
x: absolutePosition.x,
y: absolutePosition.y,
z: parentZ > childZ ? parentZ : childZ,
z: parentZ >= childZ ? parentZ + 1 : childZ,
};
}
+8 -1
View File
@@ -104,6 +104,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
let dragStarted = false;
let d3Selection: Selection<Element, unknown, null, undefined> | null = null;
let abortDrag = false; // prevents unintentional dragging on multitouch
let nodePositionsChanged = false;
// public functions
function update({
@@ -191,6 +192,8 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
dragItem.internals.positionAbsolute = positionAbsolute;
}
nodePositionsChanged = nodePositionsChanged || hasChange;
if (!hasChange) {
return;
}
@@ -294,6 +297,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
containerBounds = domNode?.getBoundingClientRect() || null;
abortDrag = false;
nodePositionsChanged = false;
if (nodeDragThreshold === 0) {
startDrag(event);
@@ -354,7 +358,10 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
if (dragItems.size > 0) {
const { nodeLookup, updateNodePositions, onNodeDragStop, onSelectionDragStop } = getStoreItems();
updateNodePositions(dragItems, false);
if (nodePositionsChanged) {
updateNodePositions(dragItems, false);
nodePositionsChanged = false;
}
if (onDragStop || onNodeDragStop || (!nodeId && onSelectionDragStop)) {
const [currentNode, currentNodes] = getEventHandlerParams({
+43 -21
View File
@@ -45,6 +45,7 @@ function onPointerDown(
getTransform,
getFromHandle,
autoPanSpeed,
dragThreshold = 1,
}: OnPointerDownParams
) {
// when xyflow is used inside a shadow root we can't use document
@@ -56,6 +57,7 @@ function onPointerDown(
const clickedHandle = doc?.elementFromPoint(x, y);
const handleType = getHandleType(edgeUpdaterType, clickedHandle);
const containerBounds = domNode?.getBoundingClientRect();
let connectionStarted = false;
if (!containerBounds || !handleType) {
return;
@@ -92,10 +94,9 @@ function onPointerDown(
};
const fromNodeInternal = nodeLookup.get(nodeId)!;
const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true);
const newConnection: ConnectionInProgress = {
let previousConnection: ConnectionInProgress = {
inProgress: true,
isValid: null,
@@ -110,12 +111,30 @@ function onPointerDown(
toNode: null,
};
updateConnection(newConnection);
let previousConnection: ConnectionInProgress = newConnection;
function startConnection() {
connectionStarted = true;
updateConnection(previousConnection);
onConnectStart?.(event, { nodeId, handleId, handleType });
}
onConnectStart?.(event, { nodeId, handleId, handleType });
if (dragThreshold === 0) {
startConnection();
}
function onPointerMove(event: MouseEvent | TouchEvent) {
if (!connectionStarted) {
const { x: evtX, y: evtY } = getEventPosition(event);
const dx = evtX - x;
const dy = evtY - y;
const nextConnectionStarted = dx * dx + dy * dy > dragThreshold * dragThreshold;
if (!nextConnectionStarted) {
return;
}
startConnection();
}
if (!getFromHandle() || !fromHandle) {
onPointerUp(event);
return;
@@ -188,24 +207,27 @@ function onPointerDown(
}
function onPointerUp(event: MouseEvent | TouchEvent) {
if ((closestHandle || handleDomNode) && connection && isValid) {
onConnect?.(connection);
}
if (connectionStarted) {
if ((closestHandle || handleDomNode) && connection && isValid) {
onConnect?.(connection);
}
/*
* it's important to get a fresh reference from the store here
* in order to get the latest state of onConnectEnd
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { inProgress, ...connectionState } = previousConnection;
const finalConnectionState = {
...connectionState,
toPosition: previousConnection.toHandle ? previousConnection.toPosition : null,
};
onConnectEnd?.(event, finalConnectionState);
/*
* it's important to get a fresh reference from the store here
* in order to get the latest state of onConnectEnd
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { inProgress, ...connectionState } = previousConnection;
const finalConnectionState = {
...connectionState,
toPosition: previousConnection.toHandle ? previousConnection.toPosition : null,
};
if (edgeUpdaterType) {
onReconnectEnd?.(event, finalConnectionState);
onConnectEnd?.(event, finalConnectionState);
if (edgeUpdaterType) {
onReconnectEnd?.(event, finalConnectionState);
}
}
cancelConnection();
+1
View File
@@ -37,6 +37,7 @@ export type OnPointerDownParams = {
getTransform: () => Transform;
getFromHandle: () => Handle | null;
autoPanSpeed?: number;
dragThreshold?: number;
};
export type IsValidParams = {
@@ -20,6 +20,7 @@ import {
createZoomOnScrollHandler,
} from './eventhandler';
import { createFilter } from './filter';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { transition } from 'd3-transition';
export type ZoomPanValues = {
+1
View File
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { type ZoomTransform, zoomIdentity } from 'd3-zoom';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { transition } from 'd3-transition';
import { type D3SelectionInstance, type Viewport } from '../types';
+2834 -2737
View File
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More