Merge pull request #5326 from xyflow/fix/node-resizer-controls

correct node resizer control size for viewport zoom
This commit is contained in:
Peter Kogo
2025-06-11 12:16:48 +02:00
committed by GitHub
9 changed files with 67 additions and 18 deletions

View File

@@ -0,0 +1,7 @@
---
'@xyflow/react': minor
'@xyflow/svelte': minor
'@xyflow/system': patch
---
Prevent NodeResizer controls to become too small when zooming out

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}

View File

@@ -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'

View File

@@ -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}

View File

@@ -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?.()}

View File

@@ -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'

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 {