Merge branch 'main' into a11y

This commit is contained in:
Moritz Klack
2025-06-03 12:54:44 +02:00
committed by GitHub
28 changed files with 160 additions and 70 deletions

View File

@@ -0,0 +1,5 @@
---
'@xyflow/svelte': patch
---
Fix data in EdgeProps not typed correctly

View File

@@ -0,0 +1,7 @@
---
'@xyflow/react': minor
'@xyflow/svelte': minor
'@xyflow/system': minor
---
Improve typing for Nodes

View File

@@ -0,0 +1,5 @@
---
'@xyflow/svelte': patch
---
Fix setting nodesInitialized multiple times

View File

@@ -0,0 +1,7 @@
---
'@xyflow/react': minor
'@xyflow/svelte': minor
'@xyflow/system': minor
---
Add an `ease` and `interpolate` option to all function that alter the viewport

View File

@@ -0,0 +1,5 @@
---
'@xyflow/svelte': patch
---
Change a11y description inline styles to classes

View File

@@ -0,0 +1,5 @@
---
'@xyflow/svelte': patch
---
Export missing callback types

View File

@@ -20,19 +20,19 @@ That being said, our libraries are only interesting because of the people who ma
The things we need the most help for the library and its community are:
**🐛 Bug reports:** We simply cant catch them all. Check [existing issues](https://github.com/wbkd/react-flow/issues/new/choose) and discussion first, then [create a new issue](https://github.com/wbkd/react-flow/issues/new/choose) to tell us whats up.
**🐛 Bug reports:** We simply cant catch them all. Check [existing issues](https://github.com/xyflow/xyflow/issues/) and discussion first, then [create a new issue](https://github.com/xyflow/xyflow/issues/new/choose) to tell us whats up.
**💬 Answering questions** in our [Discord Server](https://discord.gg/Bqt6xrs) and [Github discussions](https://github.com/wbkd/react-flow/discussions).
**💬 Answering questions** in our [Discord Server](https://discord.gg/Bqt6xrs) and [Github discussions](https://github.com/xyflow/xyflow/discussions).
🎬 **Create tutorials**. Send them to us and well happily share them!
** Edit our [Docs](https://reactflow.dev/docs/introduction/)**: Make changes in the [react-flow-docs repo](https://github.com/wbkd/react-flow-docs), or click the "edit this page” button that lives on every doc site.
** Edit our [Docs](https://reactflow.dev/learn/concepts/introduction/)**: Make changes in our [web repo](https://github.com/xyflow/web), where our docs live.
All interactions should be done with care following our [Code of Conduct](https://github.com/wbkd/react-flow/blob/main/CODE_OF_CONDUCT.md).
All interactions should be done with care following our [Code of Conduct](https://github.com/xyflow/xyflow/blob/main/CODE_OF_CONDUCT.md).
## Enhancements
If you have an idea or suggestion for an enhancement to the React Flow or Svelte Flow library, please use the [New Features](https://github.com/wbkd/react-flow/discussions/categories/new-features) discussion section. If you want to build it yourself, **please reach out to us before you dive into a new pull request.** The direction of React Flow/ Svelte Flow and which new features are added are discussed in our Discord Server and in [this Github discussions section](https://github.com/wbkd/react-flow/discussions/categories/new-features), and in the end they are decided by the core team.
If you have an idea or suggestion for an enhancement to the React Flow or Svelte Flow library, please use the [New Features](https://github.com/xyflow/xyflow/discussions/categories/new-features) discussion section. If you want to build it yourself, **please reach out to us before you dive into a new pull request.** The direction of React Flow/ Svelte Flow and which new features are added are discussed in our Discord Server and in [this Github discussions section](https://github.com/xyflow/xyflow/discussions/categories/new-features), and in the end they are decided by the core team.
Talking to us first about the enhancement you want to build will be the most likely way to get your pull request into the library (see Our Contributing Philosophy above). We would hate to see you write code youre proud of, just to learn that weve already been working on the same thing, or that we feel doesnt fit into the core library.

View File

@@ -159,16 +159,9 @@ For releasing packages we are using [changesets](https://github.com/changesets/c
3. changset creates a PR that bumps all packages based on the changesets
4. merge changeset PR if you want to release to Github and npm
## The xyflow team
React Flow and Svelte Flow are maintained by the team behind [xyflow](https://xyflow.com). If you need help or want to talk to us about a collaboration, reach out through our [contact form](https://xyflow.com/contact) or by joining our [Discord Server](https://discord.gg/Bqt6xrs).
- Christopher • [Twitter](https://twitter.com/chrtze) • [Github](https://github.com/chrtze)
- Hayleigh • [Twitter](https://twitter.com/hayleighdotdev) • [Github](https://github.com/hayleigh-dot-dev)
- Abbey • [Github](https://github.com/printerscanner)
- Moritz • [Twitter](https://twitter.com/moklick) • [Github](https://github.com/moklick)
- Peter • [Github](https://github.com/peterkogo)
## Built by [xyflow](https://xyflow.com)
React Flow and Svelte Flow are maintained by the [xyflow team](https://xyflow.com/about). If you need help or want to talk to us about a collaboration, reach out through our [contact form](https://xyflow.com/contact) or by joining our [Discord Server](https://discord.gg/Bqt6xrs).
## License

View File

@@ -170,7 +170,6 @@ const UseZoomPanHelperFlow = () => {
onConnect={onConnect}
onPaneClick={onPaneClick}
fitView
fitViewOptions={{ duration: 1200, padding: 0.2 }}
maxZoom={Infinity}
>
<Panel position="top-right">
@@ -183,7 +182,16 @@ const UseZoomPanHelperFlow = () => {
console.log('fit view success');
}}
>
fitView
fitView default
</button>
<button
onClick={async () => {
console.log('fit view start');
await fitView({ duration: 1200, padding: 0.3, ease: (t) => +t });
console.log('fit view success');
}}
>
fitView linear
</button>
<button onClick={onAddNode}>add node</button>
<button onClick={onResetNodes}>reset nodes</button>

View File

@@ -44,7 +44,7 @@
</script>
<main>
<SvelteFlow bind:nodes bind:edges fitView>
<SvelteFlow bind:nodes bind:edges fitView maxZoom={4}>
<Controls />
<Background variant={BackgroundVariant.Dots} />
<MiniMap />

View File

@@ -30,7 +30,10 @@
<button onclick={() => zoomIn()}>zoom in</button>
<button onclick={() => zoomOut({ duration: 1000 })}>zoom out transition</button>
<button onclick={() => setZoom(2)}>set zoom</button>
<button onclick={() => fitView()}>fitView</button>
<button onclick={() => fitView({ duration: 600 })}>fitView</button>
<button onclick={() => fitView({ duration: 600, ease: (t) => +t, interpolate: 'linear' })}
>fitView linear</button
>
<button onclick={() => setCenter(0, 0)}>setCenter 0, 0</button>
<button onclick={() => setViewport({ x: 100, y: 100, zoom: 2 })}>setViewport</button>
<button onclick={() => console.log(getViewport())}>getViewport</button>

View File

@@ -53,7 +53,7 @@ const useViewportHelper = (): ViewportHelperFunctions => {
y: viewport.y ?? tY,
zoom: viewport.zoom ?? tZoom,
},
{ duration: options?.duration }
options
);
return Promise.resolve(true);
@@ -78,7 +78,7 @@ const useViewportHelper = (): ViewportHelperFunctions => {
y: centerY,
zoom: nextZoom,
},
{ duration: options?.duration }
{ duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate }
);
return Promise.resolve(true);
@@ -91,7 +91,11 @@ const useViewportHelper = (): ViewportHelperFunctions => {
return Promise.resolve(false);
}
await panZoom.setViewport(viewport, { duration: options?.duration });
await panZoom.setViewport(viewport, {
duration: options?.duration,
ease: options?.ease,
interpolate: options?.interpolate,
});
return Promise.resolve(true);
},

View File

@@ -149,6 +149,7 @@ export type ViewportHelperFunctions = {
*
* @param viewport - the viewport to set
* @param options.duration - optional duration. If set, a transition will be applied
* @param options.ease - optional ease function.
*/
setViewport: SetViewport;
/**
@@ -164,6 +165,8 @@ export type ViewportHelperFunctions = {
* @param x - x position
* @param y - y position
* @param options.zoom - optional zoom
* @param options.duration - optional duration. If set, a transition will be applied
* @param options.ease - optional ease function.
*/
setCenter: SetCenter;
/**
@@ -173,6 +176,8 @@ export type ViewportHelperFunctions = {
*
* @param bounds - the bounds ({ x: number, y: number, width: number, height: number }) to fit the view to
* @param options.padding - optional padding
* @param options.duration - optional duration. If set, a transition will be applied
* @param options.ease - optional ease function.
*/
fitBounds: FitBounds;
/**

View File

@@ -12,7 +12,7 @@ import { NodeTypes } from './general';
*/
export type Node<
NodeData extends Record<string, unknown> = Record<string, unknown>,
NodeType extends string = string
NodeType extends string | undefined = string | undefined
> = NodeBase<NodeData, NodeType> & {
style?: CSSProperties;
className?: string;

View File

@@ -1,21 +1,18 @@
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
import { derived } from 'svelte/store';
import type { SvelteFlowStore } from '$lib/store/types';
import type { Node, Edge } from '$lib/types';
import type { Node, Edge } from '$lib/types';
import { ARIA_EDGE_DESC_KEY, ARIA_LIVE_MESSAGE, ARIA_NODE_DESC_KEY } from '.';
let { store }: { store: SvelteFlowStore<NodeType, EdgeType> } = $props();
</script>
<div id={`${ARIA_NODE_DESC_KEY}-${store.flowId}`} style="display: none;">
<div id={`${ARIA_NODE_DESC_KEY}-${store.flowId}`} class="a11y-hidden">
{store.disableKeyboardA11y
? store.labelConfig['a11yDescription.node.default']
: store.labelConfig['a11yDescription.node.keyboardDisabled']}
</div>
<div id={`${ARIA_EDGE_DESC_KEY}-${store.flowId}`} style="display: none;">
<div id={`${ARIA_EDGE_DESC_KEY}-${store.flowId}`} class="a11y-hidden">
{store.labelConfig['a11yDescription.edge.default']}
</div>
@@ -24,8 +21,26 @@
id={`${ARIA_LIVE_MESSAGE}-${store.flowId}`}
aria-live="assertive"
aria-atomic="true"
style="position: absolute; width: 1px; height: 1px; margin: -1px; border: 0; padding: 0; overflow: hidden; clip: rect(0px, 0px, 0px, 0px); clip-path: inset(100%);"
class="a11y-live-msg"
>
{store.ariaLiveMessage}
</div>
{/if}
{/if}
<style>
.a11y-hidden {
display: none;
}
.a11y-live-msg {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
border: 0;
padding: 0;
overflow: hidden;
clip: rect(0px, 0px, 0px, 0px);
clip-path: inset(100%);
}
</style>

View File

@@ -360,7 +360,7 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
y: nextViewport.y ?? currentViewport.y,
zoom: nextViewport.zoom ?? currentViewport.zoom
},
{ duration: options?.duration }
options
);
return Promise.resolve(true);
@@ -380,7 +380,7 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
y: store.height / 2 - y * nextZoom,
zoom: nextZoom
},
{ duration: options?.duration }
{ duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate }
);
return Promise.resolve(true);
@@ -402,7 +402,11 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
options?.padding ?? 0.1
);
await store.panZoom.setViewport(viewport, { duration: options?.duration });
await store.panZoom.setViewport(viewport, {
duration: options?.duration,
ease: options?.ease,
interpolate: options?.interpolate
});
return Promise.resolve(true);
},

View File

@@ -55,10 +55,10 @@ export type {
EdgeTypes,
DefaultEdgeOptions
} from '$lib/types/edges';
export type { FitViewOptions, OnBeforeDelete } from '$lib/types/general';
export type * from '$lib/types/general';
export type { Node, NodeTypes, BuiltInNode, NodeProps, InternalNode } from '$lib/types/nodes';
export type { SvelteFlowStore } from '$lib/store/types';
export * from '$lib/types/events';
export type { SvelteFlowStore } from '$lib/store/types';
// system types
export {

View File

@@ -108,10 +108,6 @@ export function createStore<NodeType extends Node = Node, EdgeType extends Edge
}
store.nodes = store.nodes.map((node) => newNodes.get(node.id) ?? node);
if (!store.nodesInitialized) {
store.nodesInitialized = true;
}
}
function fitView(options?: FitViewOptions) {

View File

@@ -85,8 +85,6 @@ export type EdgeProps<EdgeType extends Edge = Edge> = Omit<
'sourceHandle' | 'targetHandle'
> &
EdgePosition & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any;
type: string;
markerStart?: string;
markerEnd?: string;

View File

@@ -16,7 +16,7 @@ export type InternalNode<NodeType extends Node = Node> = InternalNodeBase<NodeTy
*/
export type Node<
NodeData extends Record<string, unknown> = Record<string, unknown>,
NodeType extends string = string
NodeType extends string | undefined = string | undefined
> = NodeBase<NodeData, NodeType> & {
class?: ClassValue;
style?: string;

View File

@@ -51,10 +51,12 @@
},
"dependencies": {
"@types/d3-drag": "^3.0.7",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-selection": "^3.0.10",
"@types/d3-transition": "^3.0.8",
"@types/d3-zoom": "^3.0.8",
"d3-drag": "^3.0.0",
"d3-interpolate": "^3.0.1",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0"
},

View File

@@ -130,6 +130,8 @@ export type FitViewOptionsBase<NodeType extends NodeBase = NodeBase> = {
minZoom?: number;
maxZoom?: number;
duration?: number;
ease?: (t: number) => number;
interpolate?: 'smooth' | 'linear';
nodes?: (NodeType | { id: string })[];
};
@@ -173,6 +175,8 @@ export enum PanOnScrollMode {
*/
export type ViewportHelperFunctionOptions = {
duration?: number;
ease?: (t: number) => number;
interpolate?: 'smooth' | 'linear';
};
export type SetCenterOptions = ViewportHelperFunctionOptions & {

View File

@@ -10,7 +10,7 @@ import { Optional } from '../utils/types';
*/
export type NodeBase<
NodeData extends Record<string, unknown> = Record<string, unknown>,
NodeType extends string = string
NodeType extends string | undefined = string | undefined
> = {
/** Unique id of a node. */
id: string;
@@ -21,8 +21,6 @@ export type NodeBase<
position: XYPosition;
/** Arbitrary data passed to a node. */
data: NodeData;
/** Type of node defined in `nodeTypes`. */
type?: NodeType;
/**
* Only relevant for default, source, target nodeType. Controls source position.
* @example 'right', 'left', 'top', 'bottom'
@@ -79,7 +77,15 @@ export type NodeBase<
width?: number;
height?: number;
};
};
} & (undefined extends NodeType
? {
/** Type of node defined in nodeTypes */
type?: string | undefined;
}
: {
/** Type of node defined in nodeTypes */
type: NodeType;
});
export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = Omit<NodeType, 'measured'> & {
measured: {

View File

@@ -20,6 +20,8 @@ export type PanZoomParams = {
export type PanZoomTransformOptions = {
duration?: number;
ease?: (t: number) => number;
interpolate?: 'smooth' | 'linear';
};
export type OnPanZoom = (event: MouseEvent | TouchEvent | null, viewport: Viewport) => void;

View File

@@ -378,7 +378,11 @@ export async function fitViewport<
options?.padding ?? 0.1
);
await panZoom.setViewport(viewport, { duration: options?.duration });
await panZoom.setViewport(viewport, {
duration: options?.duration,
ease: options?.ease,
interpolate: options?.interpolate,
});
return Promise.resolve(true);
}

View File

@@ -1,5 +1,6 @@
import { type ZoomTransform, zoom, zoomTransform } from 'd3-zoom';
import { select } from 'd3-selection';
import { interpolateZoom, interpolate } from 'd3-interpolate';
import {
type CoordinateExtent,
@@ -19,6 +20,7 @@ import {
createZoomOnScrollHandler,
} from './eventhandler';
import { createFilter } from './filter';
import { transition } from 'd3-transition';
export type ZoomPanValues = {
isZoomingOrPanning: boolean;
@@ -78,8 +80,8 @@ export function XYPanZoom({
function setTransform(transform: ZoomTransform, options?: PanZoomTransformOptions) {
if (d3Selection) {
return new Promise<boolean>((resolve) => {
d3ZoomInstance?.transform(
getD3Transition(d3Selection, options?.duration, () => resolve(true)),
d3ZoomInstance?.interpolate(options?.interpolate === 'linear' ? interpolate : interpolateZoom).transform(
getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)),
transform
);
});
@@ -114,22 +116,22 @@ export function XYPanZoom({
const wheelHandler = isPanOnScroll
? createPanOnScrollHandler({
zoomPanValues,
noWheelClassName,
d3Selection,
d3Zoom: d3ZoomInstance,
panOnScrollMode,
panOnScrollSpeed,
zoomOnPinch,
onPanZoomStart,
onPanZoom,
onPanZoomEnd,
})
zoomPanValues,
noWheelClassName,
d3Selection,
d3Zoom: d3ZoomInstance,
panOnScrollMode,
panOnScrollSpeed,
zoomOnPinch,
onPanZoomStart,
onPanZoom,
onPanZoomEnd,
})
: createZoomOnScrollHandler({
noWheelClassName,
preventScrolling,
d3ZoomHandler,
});
noWheelClassName,
preventScrolling,
d3ZoomHandler,
});
d3Selection.on('wheel.zoom', wheelHandler, { passive: false });
@@ -242,8 +244,8 @@ export function XYPanZoom({
function scaleTo(zoom: number, options?: PanZoomTransformOptions) {
if (d3Selection) {
return new Promise<boolean>((resolve) => {
d3ZoomInstance?.scaleTo(
getD3Transition(d3Selection, options?.duration, () => resolve(true)),
d3ZoomInstance?.interpolate(options?.interpolate === 'linear' ? interpolate : interpolateZoom).scaleTo(
getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)),
zoom
);
});
@@ -255,8 +257,8 @@ export function XYPanZoom({
function scaleBy(factor: number, options?: PanZoomTransformOptions) {
if (d3Selection) {
return new Promise<boolean>((resolve) => {
d3ZoomInstance?.scaleBy(
getD3Transition(d3Selection, options?.duration, () => resolve(true)),
d3ZoomInstance?.interpolate(options?.interpolate === 'linear' ? interpolate : interpolateZoom).scaleBy(
getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)),
factor
);
});

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { type ZoomTransform, zoomIdentity } from 'd3-zoom';
import { transition } from 'd3-transition';
import { type D3SelectionInstance, type Viewport } from '../types';
import { isMacOs } from '../utils';
@@ -21,14 +22,17 @@ export const isWrappedWithClass = (event: any, className: string | undefined) =>
export const isRightClickPan = (panOnDrag: boolean | number[], usedButton: number) =>
usedButton === 2 && Array.isArray(panOnDrag) && panOnDrag.includes(2);
export const getD3Transition = (selection: D3SelectionInstance, duration = 0, onEnd = () => {}) => {
// taken from d3-ease: https://github.com/d3/d3-ease/blob/main/src/cubic.js
const defaultEase = (t: number) => ((t *= 2) <= 1 ? t * t * t : (t -= 2) * t * t + 2) / 2;
export const getD3Transition = (selection: D3SelectionInstance, duration = 0, ease = defaultEase, onEnd = () => {}) => {
const hasDuration = typeof duration === 'number' && duration > 0;
if (!hasDuration) {
onEnd();
}
return hasDuration ? selection.transition().duration(duration).on('end', onEnd) : selection;
return hasDuration ? selection.transition().duration(duration).ease(ease).on('end', onEnd) : selection;
};
export const wheelDelta = (event: any) => {

6
pnpm-lock.yaml generated
View File

@@ -372,6 +372,9 @@ importers:
'@types/d3-drag':
specifier: ^3.0.7
version: 3.0.7
'@types/d3-interpolate':
specifier: ^3.0.4
version: 3.0.4
'@types/d3-selection':
specifier: ^3.0.10
version: 3.0.10
@@ -384,6 +387,9 @@ importers:
d3-drag:
specifier: ^3.0.0
version: 3.0.0
d3-interpolate:
specifier: ^3.0.1
version: 3.0.1
d3-selection:
specifier: ^3.0.0
version: 3.0.0