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
+5
View File
@@ -0,0 +1,5 @@
---
'@xyflow/svelte': patch
---
Fix data in EdgeProps not typed correctly
+7
View File
@@ -0,0 +1,7 @@
---
'@xyflow/react': minor
'@xyflow/svelte': minor
'@xyflow/system': minor
---
Improve typing for Nodes
+5
View File
@@ -0,0 +1,5 @@
---
'@xyflow/svelte': patch
---
Fix setting nodesInitialized multiple times
+7
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
+5
View File
@@ -0,0 +1,5 @@
---
'@xyflow/svelte': patch
---
Change a11y description inline styles to classes
+5
View File
@@ -0,0 +1,5 @@
---
'@xyflow/svelte': patch
---
Export missing callback types
+5 -5
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: 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! 🎬 **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 ## 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. 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.
+2 -9
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 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 4. merge changeset PR if you want to release to Github and npm
## The xyflow team ## Built by [xyflow](https://xyflow.com)
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)
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 ## License
@@ -170,7 +170,6 @@ const UseZoomPanHelperFlow = () => {
onConnect={onConnect} onConnect={onConnect}
onPaneClick={onPaneClick} onPaneClick={onPaneClick}
fitView fitView
fitViewOptions={{ duration: 1200, padding: 0.2 }}
maxZoom={Infinity} maxZoom={Infinity}
> >
<Panel position="top-right"> <Panel position="top-right">
@@ -183,7 +182,16 @@ const UseZoomPanHelperFlow = () => {
console.log('fit view success'); 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>
<button onClick={onAddNode}>add node</button> <button onClick={onAddNode}>add node</button>
<button onClick={onResetNodes}>reset nodes</button> <button onClick={onResetNodes}>reset nodes</button>
@@ -44,7 +44,7 @@
</script> </script>
<main> <main>
<SvelteFlow bind:nodes bind:edges fitView> <SvelteFlow bind:nodes bind:edges fitView maxZoom={4}>
<Controls /> <Controls />
<Background variant={BackgroundVariant.Dots} /> <Background variant={BackgroundVariant.Dots} />
<MiniMap /> <MiniMap />
@@ -30,7 +30,10 @@
<button onclick={() => zoomIn()}>zoom in</button> <button onclick={() => zoomIn()}>zoom in</button>
<button onclick={() => zoomOut({ duration: 1000 })}>zoom out transition</button> <button onclick={() => zoomOut({ duration: 1000 })}>zoom out transition</button>
<button onclick={() => setZoom(2)}>set zoom</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={() => setCenter(0, 0)}>setCenter 0, 0</button>
<button onclick={() => setViewport({ x: 100, y: 100, zoom: 2 })}>setViewport</button> <button onclick={() => setViewport({ x: 100, y: 100, zoom: 2 })}>setViewport</button>
<button onclick={() => console.log(getViewport())}>getViewport</button> <button onclick={() => console.log(getViewport())}>getViewport</button>
@@ -53,7 +53,7 @@ const useViewportHelper = (): ViewportHelperFunctions => {
y: viewport.y ?? tY, y: viewport.y ?? tY,
zoom: viewport.zoom ?? tZoom, zoom: viewport.zoom ?? tZoom,
}, },
{ duration: options?.duration } options
); );
return Promise.resolve(true); return Promise.resolve(true);
@@ -78,7 +78,7 @@ const useViewportHelper = (): ViewportHelperFunctions => {
y: centerY, y: centerY,
zoom: nextZoom, zoom: nextZoom,
}, },
{ duration: options?.duration } { duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate }
); );
return Promise.resolve(true); return Promise.resolve(true);
@@ -91,7 +91,11 @@ const useViewportHelper = (): ViewportHelperFunctions => {
return Promise.resolve(false); 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); return Promise.resolve(true);
}, },
+5
View File
@@ -149,6 +149,7 @@ export type ViewportHelperFunctions = {
* *
* @param viewport - the viewport to set * @param viewport - the viewport to set
* @param options.duration - optional duration. If set, a transition will be applied * @param options.duration - optional duration. If set, a transition will be applied
* @param options.ease - optional ease function.
*/ */
setViewport: SetViewport; setViewport: SetViewport;
/** /**
@@ -164,6 +165,8 @@ export type ViewportHelperFunctions = {
* @param x - x position * @param x - x position
* @param y - y position * @param y - y position
* @param options.zoom - optional zoom * @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; 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 bounds - the bounds ({ x: number, y: number, width: number, height: number }) to fit the view to
* @param options.padding - optional padding * @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; fitBounds: FitBounds;
/** /**
+1 -1
View File
@@ -12,7 +12,7 @@ import { NodeTypes } from './general';
*/ */
export type Node< export type Node<
NodeData extends Record<string, unknown> = Record<string, unknown>, NodeData extends Record<string, unknown> = Record<string, unknown>,
NodeType extends string = string NodeType extends string | undefined = string | undefined
> = NodeBase<NodeData, NodeType> & { > = NodeBase<NodeData, NodeType> & {
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
@@ -1,21 +1,18 @@
<script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge"> <script lang="ts" generics="NodeType extends Node = Node, EdgeType extends Edge = Edge">
import { derived } from 'svelte/store'; import { derived } from 'svelte/store';
import type { SvelteFlowStore } from '$lib/store/types'; 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 '.'; import { ARIA_EDGE_DESC_KEY, ARIA_LIVE_MESSAGE, ARIA_NODE_DESC_KEY } from '.';
let { store }: { store: SvelteFlowStore<NodeType, EdgeType> } = $props(); let { store }: { store: SvelteFlowStore<NodeType, EdgeType> } = $props();
</script> </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.disableKeyboardA11y
? store.labelConfig['a11yDescription.node.default'] ? store.labelConfig['a11yDescription.node.default']
: store.labelConfig['a11yDescription.node.keyboardDisabled']} : store.labelConfig['a11yDescription.node.keyboardDisabled']}
</div> </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']} {store.labelConfig['a11yDescription.edge.default']}
</div> </div>
@@ -24,8 +21,26 @@
id={`${ARIA_LIVE_MESSAGE}-${store.flowId}`} id={`${ARIA_LIVE_MESSAGE}-${store.flowId}`}
aria-live="assertive" aria-live="assertive"
aria-atomic="true" 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} {store.ariaLiveMessage}
</div> </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>
@@ -360,7 +360,7 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
y: nextViewport.y ?? currentViewport.y, y: nextViewport.y ?? currentViewport.y,
zoom: nextViewport.zoom ?? currentViewport.zoom zoom: nextViewport.zoom ?? currentViewport.zoom
}, },
{ duration: options?.duration } options
); );
return Promise.resolve(true); return Promise.resolve(true);
@@ -380,7 +380,7 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
y: store.height / 2 - y * nextZoom, y: store.height / 2 - y * nextZoom,
zoom: nextZoom zoom: nextZoom
}, },
{ duration: options?.duration } { duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate }
); );
return Promise.resolve(true); return Promise.resolve(true);
@@ -402,7 +402,11 @@ export function useSvelteFlow<NodeType extends Node = Node, EdgeType extends Edg
options?.padding ?? 0.1 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); return Promise.resolve(true);
}, },
+2 -2
View File
@@ -55,10 +55,10 @@ export type {
EdgeTypes, EdgeTypes,
DefaultEdgeOptions DefaultEdgeOptions
} from '$lib/types/edges'; } 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 { Node, NodeTypes, BuiltInNode, NodeProps, InternalNode } from '$lib/types/nodes';
export type { SvelteFlowStore } from '$lib/store/types';
export * from '$lib/types/events'; export * from '$lib/types/events';
export type { SvelteFlowStore } from '$lib/store/types';
// system types // system types
export { export {
-4
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); store.nodes = store.nodes.map((node) => newNodes.get(node.id) ?? node);
if (!store.nodesInitialized) {
store.nodesInitialized = true;
}
} }
function fitView(options?: FitViewOptions) { function fitView(options?: FitViewOptions) {
-2
View File
@@ -85,8 +85,6 @@ export type EdgeProps<EdgeType extends Edge = Edge> = Omit<
'sourceHandle' | 'targetHandle' 'sourceHandle' | 'targetHandle'
> & > &
EdgePosition & { EdgePosition & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any;
type: string; type: string;
markerStart?: string; markerStart?: string;
markerEnd?: string; markerEnd?: string;
+1 -1
View File
@@ -16,7 +16,7 @@ export type InternalNode<NodeType extends Node = Node> = InternalNodeBase<NodeTy
*/ */
export type Node< export type Node<
NodeData extends Record<string, unknown> = Record<string, unknown>, NodeData extends Record<string, unknown> = Record<string, unknown>,
NodeType extends string = string NodeType extends string | undefined = string | undefined
> = NodeBase<NodeData, NodeType> & { > = NodeBase<NodeData, NodeType> & {
class?: ClassValue; class?: ClassValue;
style?: string; style?: string;
+2
View File
@@ -51,10 +51,12 @@
}, },
"dependencies": { "dependencies": {
"@types/d3-drag": "^3.0.7", "@types/d3-drag": "^3.0.7",
"@types/d3-interpolate": "^3.0.4",
"@types/d3-selection": "^3.0.10", "@types/d3-selection": "^3.0.10",
"@types/d3-transition": "^3.0.8", "@types/d3-transition": "^3.0.8",
"@types/d3-zoom": "^3.0.8", "@types/d3-zoom": "^3.0.8",
"d3-drag": "^3.0.0", "d3-drag": "^3.0.0",
"d3-interpolate": "^3.0.1",
"d3-selection": "^3.0.0", "d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0" "d3-zoom": "^3.0.0"
}, },
+4
View File
@@ -130,6 +130,8 @@ export type FitViewOptionsBase<NodeType extends NodeBase = NodeBase> = {
minZoom?: number; minZoom?: number;
maxZoom?: number; maxZoom?: number;
duration?: number; duration?: number;
ease?: (t: number) => number;
interpolate?: 'smooth' | 'linear';
nodes?: (NodeType | { id: string })[]; nodes?: (NodeType | { id: string })[];
}; };
@@ -173,6 +175,8 @@ export enum PanOnScrollMode {
*/ */
export type ViewportHelperFunctionOptions = { export type ViewportHelperFunctionOptions = {
duration?: number; duration?: number;
ease?: (t: number) => number;
interpolate?: 'smooth' | 'linear';
}; };
export type SetCenterOptions = ViewportHelperFunctionOptions & { export type SetCenterOptions = ViewportHelperFunctionOptions & {
+10 -4
View File
@@ -10,7 +10,7 @@ import { Optional } from '../utils/types';
*/ */
export type NodeBase< export type NodeBase<
NodeData extends Record<string, unknown> = Record<string, unknown>, NodeData extends Record<string, unknown> = Record<string, unknown>,
NodeType extends string = string NodeType extends string | undefined = string | undefined
> = { > = {
/** Unique id of a node. */ /** Unique id of a node. */
id: string; id: string;
@@ -21,8 +21,6 @@ export type NodeBase<
position: XYPosition; position: XYPosition;
/** Arbitrary data passed to a node. */ /** Arbitrary data passed to a node. */
data: NodeData; data: NodeData;
/** Type of node defined in `nodeTypes`. */
type?: NodeType;
/** /**
* Only relevant for default, source, target nodeType. Controls source position. * Only relevant for default, source, target nodeType. Controls source position.
* @example 'right', 'left', 'top', 'bottom' * @example 'right', 'left', 'top', 'bottom'
@@ -79,7 +77,15 @@ export type NodeBase<
width?: number; width?: number;
height?: 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'> & { export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = Omit<NodeType, 'measured'> & {
measured: { measured: {
+2
View File
@@ -20,6 +20,8 @@ export type PanZoomParams = {
export type PanZoomTransformOptions = { export type PanZoomTransformOptions = {
duration?: number; duration?: number;
ease?: (t: number) => number;
interpolate?: 'smooth' | 'linear';
}; };
export type OnPanZoom = (event: MouseEvent | TouchEvent | null, viewport: Viewport) => void; export type OnPanZoom = (event: MouseEvent | TouchEvent | null, viewport: Viewport) => void;
+5 -1
View File
@@ -378,7 +378,11 @@ export async function fitViewport<
options?.padding ?? 0.1 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); return Promise.resolve(true);
} }
+23 -21
View File
@@ -1,5 +1,6 @@
import { type ZoomTransform, zoom, zoomTransform } from 'd3-zoom'; import { type ZoomTransform, zoom, zoomTransform } from 'd3-zoom';
import { select } from 'd3-selection'; import { select } from 'd3-selection';
import { interpolateZoom, interpolate } from 'd3-interpolate';
import { import {
type CoordinateExtent, type CoordinateExtent,
@@ -19,6 +20,7 @@ import {
createZoomOnScrollHandler, createZoomOnScrollHandler,
} from './eventhandler'; } from './eventhandler';
import { createFilter } from './filter'; import { createFilter } from './filter';
import { transition } from 'd3-transition';
export type ZoomPanValues = { export type ZoomPanValues = {
isZoomingOrPanning: boolean; isZoomingOrPanning: boolean;
@@ -78,8 +80,8 @@ export function XYPanZoom({
function setTransform(transform: ZoomTransform, options?: PanZoomTransformOptions) { function setTransform(transform: ZoomTransform, options?: PanZoomTransformOptions) {
if (d3Selection) { if (d3Selection) {
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
d3ZoomInstance?.transform( d3ZoomInstance?.interpolate(options?.interpolate === 'linear' ? interpolate : interpolateZoom).transform(
getD3Transition(d3Selection, options?.duration, () => resolve(true)), getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)),
transform transform
); );
}); });
@@ -114,22 +116,22 @@ export function XYPanZoom({
const wheelHandler = isPanOnScroll const wheelHandler = isPanOnScroll
? createPanOnScrollHandler({ ? createPanOnScrollHandler({
zoomPanValues, zoomPanValues,
noWheelClassName, noWheelClassName,
d3Selection, d3Selection,
d3Zoom: d3ZoomInstance, d3Zoom: d3ZoomInstance,
panOnScrollMode, panOnScrollMode,
panOnScrollSpeed, panOnScrollSpeed,
zoomOnPinch, zoomOnPinch,
onPanZoomStart, onPanZoomStart,
onPanZoom, onPanZoom,
onPanZoomEnd, onPanZoomEnd,
}) })
: createZoomOnScrollHandler({ : createZoomOnScrollHandler({
noWheelClassName, noWheelClassName,
preventScrolling, preventScrolling,
d3ZoomHandler, d3ZoomHandler,
}); });
d3Selection.on('wheel.zoom', wheelHandler, { passive: false }); d3Selection.on('wheel.zoom', wheelHandler, { passive: false });
@@ -242,8 +244,8 @@ export function XYPanZoom({
function scaleTo(zoom: number, options?: PanZoomTransformOptions) { function scaleTo(zoom: number, options?: PanZoomTransformOptions) {
if (d3Selection) { if (d3Selection) {
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
d3ZoomInstance?.scaleTo( d3ZoomInstance?.interpolate(options?.interpolate === 'linear' ? interpolate : interpolateZoom).scaleTo(
getD3Transition(d3Selection, options?.duration, () => resolve(true)), getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)),
zoom zoom
); );
}); });
@@ -255,8 +257,8 @@ export function XYPanZoom({
function scaleBy(factor: number, options?: PanZoomTransformOptions) { function scaleBy(factor: number, options?: PanZoomTransformOptions) {
if (d3Selection) { if (d3Selection) {
return new Promise<boolean>((resolve) => { return new Promise<boolean>((resolve) => {
d3ZoomInstance?.scaleBy( d3ZoomInstance?.interpolate(options?.interpolate === 'linear' ? interpolate : interpolateZoom).scaleBy(
getD3Transition(d3Selection, options?.duration, () => resolve(true)), getD3Transition(d3Selection, options?.duration, options?.ease, () => resolve(true)),
factor factor
); );
}); });
+6 -2
View File
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { type ZoomTransform, zoomIdentity } from 'd3-zoom'; import { type ZoomTransform, zoomIdentity } from 'd3-zoom';
import { transition } from 'd3-transition';
import { type D3SelectionInstance, type Viewport } from '../types'; import { type D3SelectionInstance, type Viewport } from '../types';
import { isMacOs } from '../utils'; import { isMacOs } from '../utils';
@@ -21,14 +22,17 @@ export const isWrappedWithClass = (event: any, className: string | undefined) =>
export const isRightClickPan = (panOnDrag: boolean | number[], usedButton: number) => export const isRightClickPan = (panOnDrag: boolean | number[], usedButton: number) =>
usedButton === 2 && Array.isArray(panOnDrag) && panOnDrag.includes(2); 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; const hasDuration = typeof duration === 'number' && duration > 0;
if (!hasDuration) { if (!hasDuration) {
onEnd(); 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) => { export const wheelDelta = (event: any) => {
+6
View File
@@ -372,6 +372,9 @@ importers:
'@types/d3-drag': '@types/d3-drag':
specifier: ^3.0.7 specifier: ^3.0.7
version: 3.0.7 version: 3.0.7
'@types/d3-interpolate':
specifier: ^3.0.4
version: 3.0.4
'@types/d3-selection': '@types/d3-selection':
specifier: ^3.0.10 specifier: ^3.0.10
version: 3.0.10 version: 3.0.10
@@ -384,6 +387,9 @@ importers:
d3-drag: d3-drag:
specifier: ^3.0.0 specifier: ^3.0.0
version: 3.0.0 version: 3.0.0
d3-interpolate:
specifier: ^3.0.1
version: 3.0.1
d3-selection: d3-selection:
specifier: ^3.0.0 specifier: ^3.0.0
version: 3.0.0 version: 3.0.0