From 9c2108f44331b4b827f7d5d4e0463b320791a9fe Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 10 Jun 2025 16:39:51 +0200 Subject: [PATCH 1/6] correct node resizer control size for viewport zoom --- .../NodeResizer/NodeResizeControl.tsx | 12 ++++++++++-- .../src/lib/plugins/NodeResizer/ResizeControl.svelte | 1 + packages/system/src/styles/node-resizer.css | 6 +++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx index beaeb527..98bf766a 100644 --- a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx +++ b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx @@ -1,5 +1,6 @@ import { useRef, useEffect, memo } from 'react'; import cc from 'classcat'; +import { shallow } from 'zustand/shallow'; import { XYResizer, ResizeControlVariant, @@ -15,9 +16,12 @@ import { XYPosition, } 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 selector = (store: ReactFlowState) => store.transform[2]; function ResizeControl({ nodeId, @@ -47,6 +51,8 @@ function ResizeControl({ const resizer = useRef(null); + const zoom = useStore(selector, shallow); + useEffect(() => { if (!resizeControlRef.current || !id) { return; @@ -193,7 +199,9 @@ function ResizeControl({ const positionClassNames = controlPosition.split('-'); const colorStyleProp = variant === ResizeControlVariant.Line ? 'borderColor' : 'backgroundColor'; - const controlStyle = color ? { ...style, [colorStyleProp]: color } : style; + + const styleWithTransform = { ...style, '--xy-view-zoom-inverse': 1 / zoom }; + const controlStyle = color ? { ...styleWithTransform, [colorStyleProp]: color } : styleWithTransform; return (
{@render children?.()} diff --git a/packages/system/src/styles/node-resizer.css b/packages/system/src/styles/node-resizer.css index 1c031d45..196bc3a5 100644 --- a/packages/system/src/styles/node-resizer.css +++ b/packages/system/src/styles/node-resizer.css @@ -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%); + transform: translate(-50%, -50%) scale(max(var(--xy-view-zoom-inverse, 1), 1)); } .xy-flow__resize-control.handle.left { From 050b511cd6966ba526299f7ca11f9ca4791fd2cf Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 10 Jun 2025 16:42:01 +0200 Subject: [PATCH 2/6] chore(changeset) --- .changeset/short-bugs-stare.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/short-bugs-stare.md diff --git a/.changeset/short-bugs-stare.md b/.changeset/short-bugs-stare.md new file mode 100644 index 00000000..1ac80cac --- /dev/null +++ b/.changeset/short-bugs-stare.md @@ -0,0 +1,7 @@ +--- +'@xyflow/react': minor +'@xyflow/svelte': minor +'@xyflow/system': patch +--- + +Prevent NodeResizer controls to become too small when zooming out From 178ad01f51080a0121b13d95330444dcf0e9e303 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Tue, 10 Jun 2025 21:22:29 +0200 Subject: [PATCH 3/6] remove css variable from node resizer --- .../NodeResizer/NodeResizeControl.tsx | 7 ++++--- .../src/lib/plugins/NodeResizer/ResizeControl.svelte | 12 ++++++------ packages/system/src/styles/node-resizer.css | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx index 98bf766a..fc758d8d 100644 --- a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx +++ b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx @@ -46,7 +46,8 @@ function ResizeControl({ const id = typeof nodeId === 'string' ? nodeId : contextNodeId; const store = useStoreApi(); const resizeControlRef = useRef(null); - const defaultPosition = variant === ResizeControlVariant.Line ? 'right' : 'bottom-right'; + const isLineVariant = variant === ResizeControlVariant.Line; + const defaultPosition = isLineVariant ? 'right' : 'bottom-right'; const controlPosition = position ?? defaultPosition; const resizer = useRef(null); @@ -198,9 +199,9 @@ function ResizeControl({ ]); const positionClassNames = controlPosition.split('-'); - const colorStyleProp = variant === ResizeControlVariant.Line ? 'borderColor' : 'backgroundColor'; + const colorStyleProp = isLineVariant ? 'borderColor' : 'backgroundColor'; - const styleWithTransform = { ...style, '--xy-view-zoom-inverse': 1 / zoom }; + const styleWithTransform = { ...style, scale: isLineVariant ? undefined : `${Math.max(1 / zoom, 1)}` }; const controlStyle = color ? { ...styleWithTransform, [colorStyleProp]: color } : styleWithTransform; return ( diff --git a/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte b/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte index 6dea30fa..47d7db5a 100644 --- a/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte +++ b/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte @@ -40,10 +40,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,9 +119,9 @@
{@render children?.()} diff --git a/packages/system/src/styles/node-resizer.css b/packages/system/src/styles/node-resizer.css index 196bc3a5..4c69084b 100644 --- a/packages/system/src/styles/node-resizer.css +++ b/packages/system/src/styles/node-resizer.css @@ -33,7 +33,7 @@ border: 1px solid #fff; border-radius: 1px; background-color: var(--xy-resize-background-color, var(--xy-resize-background-color-default)); - transform: translate(-50%, -50%) scale(max(var(--xy-view-zoom-inverse, 1), 1)); + translate: -50% -50%; } .xy-flow__resize-control.handle.left { From ec3ef27c90f99637f8928f6c9f1eb491a35558f0 Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 11 Jun 2025 09:59:49 +0200 Subject: [PATCH 4/6] chore(resizer): cleanup and add scaleControls --- .../NodeResizer/BottomRightResizer.tsx | 2 ++ .../NodeResizer/NodeResizeControl.tsx | 33 +++++++++++-------- .../NodeResizer/NodeResizer.tsx | 3 ++ .../NodeResizer/types.ts | 6 ++++ 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/examples/react/src/examples/NodeResizer/BottomRightResizer.tsx b/examples/react/src/examples/NodeResizer/BottomRightResizer.tsx index ca860fdf..9809ef61 100644 --- a/examples/react/src/examples/NodeResizer/BottomRightResizer.tsx +++ b/examples/react/src/examples/NodeResizer/BottomRightResizer.tsx @@ -10,6 +10,8 @@ const CustomResizerNode: FC = ({ data }) => { resizeDirection="horizontal" minWidth={100} maxWidth={500} + color="orange" + scaleControls />
{data.label}
diff --git a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx index fc758d8d..1bd1f3d3 100644 --- a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx +++ b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx @@ -1,4 +1,4 @@ -import { useRef, useEffect, memo } from 'react'; +import { useRef, useEffect, memo, useCallback } from 'react'; import cc from 'classcat'; import { shallow } from 'zustand/shallow'; import { @@ -14,6 +14,7 @@ import { evaluateAbsolutePosition, ParentExpandChild, XYPosition, + ControlPosition, } from '@xyflow/system'; import { useStoreApi, useStore } from '../../hooks/useStore'; @@ -21,14 +22,20 @@ import { useNodeId } from '../../contexts/NodeIdContext'; import type { ResizeControlProps, ResizeControlLineProps } from './types'; import { ReactFlowState } from '../../types'; -const selector = (store: ReactFlowState) => store.transform[2]; +const scaleSelector = (calculateScale: boolean) => (store: ReactFlowState) => + calculateScale ? `${Math.max(1 / store.transform[2], 1)}` : undefined; + +const defaultPositions: Record = { + [ResizeControlVariant.Line]: 'right', + [ResizeControlVariant.Handle]: 'bottom-right', +}; function ResizeControl({ nodeId, position, variant = ResizeControlVariant.Handle, className, - style = {}, + style = undefined, children, color, minWidth = 10, @@ -37,6 +44,7 @@ function ResizeControl({ maxHeight = Number.MAX_VALUE, keepAspectRatio = false, resizeDirection, + scaleControls = false, shouldResize, onResizeStart, onResize, @@ -46,13 +54,10 @@ function ResizeControl({ const id = typeof nodeId === 'string' ? nodeId : contextNodeId; const store = useStoreApi(); const resizeControlRef = useRef(null); - const isLineVariant = variant === ResizeControlVariant.Line; - const defaultPosition = isLineVariant ? 'right' : 'bottom-right'; - const controlPosition = position ?? defaultPosition; - + const isHandleControl = variant === ResizeControlVariant.Handle; + const scale = useStore(useCallback(scaleSelector(isHandleControl && !scaleControls), [isHandleControl]), shallow); const resizer = useRef(null); - - const zoom = useStore(selector, shallow); + const controlPosition = position ?? defaultPositions[variant]; useEffect(() => { if (!resizeControlRef.current || !id) { @@ -199,16 +204,16 @@ function ResizeControl({ ]); const positionClassNames = controlPosition.split('-'); - const colorStyleProp = isLineVariant ? 'borderColor' : 'backgroundColor'; - - const styleWithTransform = { ...style, scale: isLineVariant ? undefined : `${Math.max(1 / zoom, 1)}` }; - const controlStyle = color ? { ...styleWithTransform, [colorStyleProp]: color } : styleWithTransform; return (
{children}
diff --git a/packages/react/src/additional-components/NodeResizer/NodeResizer.tsx b/packages/react/src/additional-components/NodeResizer/NodeResizer.tsx index 434be4b7..efde9724 100644 --- a/packages/react/src/additional-components/NodeResizer/NodeResizer.tsx +++ b/packages/react/src/additional-components/NodeResizer/NodeResizer.tsx @@ -40,6 +40,7 @@ export function NodeResizer({ maxWidth = Number.MAX_VALUE, maxHeight = Number.MAX_VALUE, keepAspectRatio = false, + scaleControls = false, shouldResize, onResizeStart, onResize, @@ -66,6 +67,7 @@ export function NodeResizer({ maxHeight={maxHeight} onResizeStart={onResizeStart} keepAspectRatio={keepAspectRatio} + scaleControls={scaleControls} shouldResize={shouldResize} onResize={onResize} onResizeEnd={onResizeEnd} @@ -85,6 +87,7 @@ export function NodeResizer({ maxHeight={maxHeight} onResizeStart={onResizeStart} keepAspectRatio={keepAspectRatio} + scaleControls={scaleControls} shouldResize={shouldResize} onResize={onResize} onResizeEnd={onResizeEnd} diff --git a/packages/react/src/additional-components/NodeResizer/types.ts b/packages/react/src/additional-components/NodeResizer/types.ts index e6dd9d36..1e3b737b 100644 --- a/packages/react/src/additional-components/NodeResizer/types.ts +++ b/packages/react/src/additional-components/NodeResizer/types.ts @@ -59,6 +59,11 @@ export type NodeResizerProps = { * @default false */ keepAspectRatio?: boolean; + /** + * Scale the controls with the zoom level. + * @default false + */ + scaleControls?: 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' + | 'scaleControls' | 'onResizeStart' | 'onResize' | 'onResizeEnd' From 120a12baf424f8d6cad0fe20ce3802136d2279fd Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 11 Jun 2025 10:44:59 +0200 Subject: [PATCH 5/6] chore(resizer): cleanup --- .../NodeResizer/NodeResizeControl.tsx | 7 +++++-- .../src/additional-components/NodeResizer/NodeResizer.tsx | 6 +++--- .../react/src/additional-components/NodeResizer/types.ts | 6 +++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx index 1bd1f3d3..d75c2606 100644 --- a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx +++ b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx @@ -44,7 +44,7 @@ function ResizeControl({ maxHeight = Number.MAX_VALUE, keepAspectRatio = false, resizeDirection, - scaleControls = false, + autoScale = true, shouldResize, onResizeStart, onResize, @@ -55,7 +55,10 @@ function ResizeControl({ const store = useStoreApi(); const resizeControlRef = useRef(null); const isHandleControl = variant === ResizeControlVariant.Handle; - const scale = useStore(useCallback(scaleSelector(isHandleControl && !scaleControls), [isHandleControl]), shallow); + const scale = useStore( + useCallback(scaleSelector(isHandleControl && autoScale), [isHandleControl, autoScale]), + shallow + ); const resizer = useRef(null); const controlPosition = position ?? defaultPositions[variant]; diff --git a/packages/react/src/additional-components/NodeResizer/NodeResizer.tsx b/packages/react/src/additional-components/NodeResizer/NodeResizer.tsx index efde9724..b869d207 100644 --- a/packages/react/src/additional-components/NodeResizer/NodeResizer.tsx +++ b/packages/react/src/additional-components/NodeResizer/NodeResizer.tsx @@ -40,7 +40,7 @@ export function NodeResizer({ maxWidth = Number.MAX_VALUE, maxHeight = Number.MAX_VALUE, keepAspectRatio = false, - scaleControls = false, + autoScale = true, shouldResize, onResizeStart, onResize, @@ -67,7 +67,7 @@ export function NodeResizer({ maxHeight={maxHeight} onResizeStart={onResizeStart} keepAspectRatio={keepAspectRatio} - scaleControls={scaleControls} + autoScale={autoScale} shouldResize={shouldResize} onResize={onResize} onResizeEnd={onResizeEnd} @@ -87,7 +87,7 @@ export function NodeResizer({ maxHeight={maxHeight} onResizeStart={onResizeStart} keepAspectRatio={keepAspectRatio} - scaleControls={scaleControls} + autoScale={autoScale} shouldResize={shouldResize} onResize={onResize} onResizeEnd={onResizeEnd} diff --git a/packages/react/src/additional-components/NodeResizer/types.ts b/packages/react/src/additional-components/NodeResizer/types.ts index 1e3b737b..eb70a115 100644 --- a/packages/react/src/additional-components/NodeResizer/types.ts +++ b/packages/react/src/additional-components/NodeResizer/types.ts @@ -61,9 +61,9 @@ export type NodeResizerProps = { keepAspectRatio?: boolean; /** * Scale the controls with the zoom level. - * @default false + * @default true */ - scaleControls?: boolean; + autoScale?: boolean; /** Callback to determine if node should resize. */ shouldResize?: ShouldResize; /** Callback called when resizing starts. */ @@ -87,7 +87,7 @@ export type ResizeControlProps = Pick< | 'maxHeight' | 'keepAspectRatio' | 'shouldResize' - | 'scaleControls' + | 'autoScale' | 'onResizeStart' | 'onResize' | 'onResizeEnd' From 49e8c9329a0e0f38a2c601a85661b3b13ef775ce Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 11 Jun 2025 10:48:49 +0200 Subject: [PATCH 6/6] feat(svelte): add autoScale for node resizer --- .../src/examples/NodeResizer/BottomRightResizer.tsx | 2 +- .../src/lib/plugins/NodeResizer/NodeResizer.svelte | 11 ++++++++++- .../src/lib/plugins/NodeResizer/ResizeControl.svelte | 3 ++- packages/svelte/src/lib/plugins/NodeResizer/types.ts | 3 +++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/examples/react/src/examples/NodeResizer/BottomRightResizer.tsx b/examples/react/src/examples/NodeResizer/BottomRightResizer.tsx index 9809ef61..38ad14cd 100644 --- a/examples/react/src/examples/NodeResizer/BottomRightResizer.tsx +++ b/examples/react/src/examples/NodeResizer/BottomRightResizer.tsx @@ -11,7 +11,7 @@ const CustomResizerNode: FC = ({ data }) => { minWidth={100} maxWidth={500} color="orange" - scaleControls + autoScale={false} />
{data.label}
diff --git a/packages/svelte/src/lib/plugins/NodeResizer/NodeResizer.svelte b/packages/svelte/src/lib/plugins/NodeResizer/NodeResizer.svelte index 8ced5ba3..0fa4bd3d 100644 --- a/packages/svelte/src/lib/plugins/NodeResizer/NodeResizer.svelte +++ b/packages/svelte/src/lib/plugins/NodeResizer/NodeResizer.svelte @@ -14,6 +14,7 @@ handleStyle, lineClass, lineStyle, + autoScale = true, ...rest }: NodeResizerProps = $props(); @@ -25,11 +26,19 @@ style={lineStyle} {nodeId} {position} + {autoScale} variant={ResizeControlVariant.Line} {...rest} /> {/each} {#each XY_RESIZER_HANDLE_POSITIONS as position (position)} - + {/each} {/if} diff --git a/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte b/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte index 47d7db5a..b9f38214 100644 --- a/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte +++ b/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte @@ -22,6 +22,7 @@ maxWidth = Number.MAX_VALUE, maxHeight = Number.MAX_VALUE, keepAspectRatio = false, + autoScale = true, shouldResize, onResizeStart, onResize, @@ -121,7 +122,7 @@ bind:this={resizeControlRef} style:border-color={isLineVariant ? color : undefined} style:background-color={isLineVariant ? undefined : color} - style:scale={isLineVariant ? undefined : Math.max(1 / store.viewport.zoom, 1)} + style:scale={isLineVariant || !autoScale ? undefined : Math.max(1 / store.viewport.zoom, 1)} {...rest} > {@render children?.()} diff --git a/packages/svelte/src/lib/plugins/NodeResizer/types.ts b/packages/svelte/src/lib/plugins/NodeResizer/types.ts index 23140c52..5f9bb95e 100644 --- a/packages/svelte/src/lib/plugins/NodeResizer/types.ts +++ b/packages/svelte/src/lib/plugins/NodeResizer/types.ts @@ -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'