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
+7
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
@@ -10,6 +10,8 @@ const CustomResizerNode: FC<NodeProps> = ({ data }) => {
resizeDirection="horizontal" resizeDirection="horizontal"
minWidth={100} minWidth={100}
maxWidth={500} maxWidth={500}
color="orange"
autoScale={false}
/> />
<Handle type="target" position={Position.Left} /> <Handle type="target" position={Position.Left} />
<div>{data.label}</div> <div>{data.label}</div>
@@ -1,5 +1,6 @@
import { useRef, useEffect, memo } from 'react'; import { useRef, useEffect, memo, useCallback } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { shallow } from 'zustand/shallow';
import { import {
XYResizer, XYResizer,
ResizeControlVariant, ResizeControlVariant,
@@ -13,18 +14,28 @@ import {
evaluateAbsolutePosition, evaluateAbsolutePosition,
ParentExpandChild, ParentExpandChild,
XYPosition, XYPosition,
ControlPosition,
} from '@xyflow/system'; } from '@xyflow/system';
import { useStoreApi } from '../../hooks/useStore'; import { useStoreApi, useStore } from '../../hooks/useStore';
import { useNodeId } from '../../contexts/NodeIdContext'; import { useNodeId } from '../../contexts/NodeIdContext';
import type { ResizeControlProps, ResizeControlLineProps } from './types'; 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({ function ResizeControl({
nodeId, nodeId,
position, position,
variant = ResizeControlVariant.Handle, variant = ResizeControlVariant.Handle,
className, className,
style = {}, style = undefined,
children, children,
color, color,
minWidth = 10, minWidth = 10,
@@ -33,6 +44,7 @@ function ResizeControl({
maxHeight = Number.MAX_VALUE, maxHeight = Number.MAX_VALUE,
keepAspectRatio = false, keepAspectRatio = false,
resizeDirection, resizeDirection,
autoScale = true,
shouldResize, shouldResize,
onResizeStart, onResizeStart,
onResize, onResize,
@@ -42,10 +54,13 @@ function ResizeControl({
const id = typeof nodeId === 'string' ? nodeId : contextNodeId; const id = typeof nodeId === 'string' ? nodeId : contextNodeId;
const store = useStoreApi(); const store = useStoreApi();
const resizeControlRef = useRef<HTMLDivElement>(null); const resizeControlRef = useRef<HTMLDivElement>(null);
const defaultPosition = variant === ResizeControlVariant.Line ? 'right' : 'bottom-right'; const isHandleControl = variant === ResizeControlVariant.Handle;
const controlPosition = position ?? defaultPosition; const scale = useStore(
useCallback(scaleSelector(isHandleControl && autoScale), [isHandleControl, autoScale]),
shallow
);
const resizer = useRef<XYResizerInstance | null>(null); const resizer = useRef<XYResizerInstance | null>(null);
const controlPosition = position ?? defaultPositions[variant];
useEffect(() => { useEffect(() => {
if (!resizeControlRef.current || !id) { if (!resizeControlRef.current || !id) {
@@ -192,14 +207,16 @@ function ResizeControl({
]); ]);
const positionClassNames = controlPosition.split('-'); const positionClassNames = controlPosition.split('-');
const colorStyleProp = variant === ResizeControlVariant.Line ? 'borderColor' : 'backgroundColor';
const controlStyle = color ? { ...style, [colorStyleProp]: color } : style;
return ( return (
<div <div
className={cc(['react-flow__resize-control', 'nodrag', ...positionClassNames, variant, className])} className={cc(['react-flow__resize-control', 'nodrag', ...positionClassNames, variant, className])}
ref={resizeControlRef} ref={resizeControlRef}
style={controlStyle} style={{
...style,
scale,
...(color && { [isHandleControl ? 'backgroundColor' : 'borderColor']: color }),
}}
> >
{children} {children}
</div> </div>
@@ -40,6 +40,7 @@ export function NodeResizer({
maxWidth = Number.MAX_VALUE, maxWidth = Number.MAX_VALUE,
maxHeight = Number.MAX_VALUE, maxHeight = Number.MAX_VALUE,
keepAspectRatio = false, keepAspectRatio = false,
autoScale = true,
shouldResize, shouldResize,
onResizeStart, onResizeStart,
onResize, onResize,
@@ -66,6 +67,7 @@ export function NodeResizer({
maxHeight={maxHeight} maxHeight={maxHeight}
onResizeStart={onResizeStart} onResizeStart={onResizeStart}
keepAspectRatio={keepAspectRatio} keepAspectRatio={keepAspectRatio}
autoScale={autoScale}
shouldResize={shouldResize} shouldResize={shouldResize}
onResize={onResize} onResize={onResize}
onResizeEnd={onResizeEnd} onResizeEnd={onResizeEnd}
@@ -85,6 +87,7 @@ export function NodeResizer({
maxHeight={maxHeight} maxHeight={maxHeight}
onResizeStart={onResizeStart} onResizeStart={onResizeStart}
keepAspectRatio={keepAspectRatio} keepAspectRatio={keepAspectRatio}
autoScale={autoScale}
shouldResize={shouldResize} shouldResize={shouldResize}
onResize={onResize} onResize={onResize}
onResizeEnd={onResizeEnd} onResizeEnd={onResizeEnd}
@@ -59,6 +59,11 @@ export type NodeResizerProps = {
* @default false * @default false
*/ */
keepAspectRatio?: boolean; keepAspectRatio?: boolean;
/**
* Scale the controls with the zoom level.
* @default true
*/
autoScale?: boolean;
/** Callback to determine if node should resize. */ /** Callback to determine if node should resize. */
shouldResize?: ShouldResize; shouldResize?: ShouldResize;
/** Callback called when resizing starts. */ /** Callback called when resizing starts. */
@@ -82,6 +87,7 @@ export type ResizeControlProps = Pick<
| 'maxHeight' | 'maxHeight'
| 'keepAspectRatio' | 'keepAspectRatio'
| 'shouldResize' | 'shouldResize'
| 'autoScale'
| 'onResizeStart' | 'onResizeStart'
| 'onResize' | 'onResize'
| 'onResizeEnd' | 'onResizeEnd'
@@ -14,6 +14,7 @@
handleStyle, handleStyle,
lineClass, lineClass,
lineStyle, lineStyle,
autoScale = true,
...rest ...rest
}: NodeResizerProps = $props(); }: NodeResizerProps = $props();
</script> </script>
@@ -25,11 +26,19 @@
style={lineStyle} style={lineStyle}
{nodeId} {nodeId}
{position} {position}
{autoScale}
variant={ResizeControlVariant.Line} variant={ResizeControlVariant.Line}
{...rest} {...rest}
/> />
{/each} {/each}
{#each XY_RESIZER_HANDLE_POSITIONS as position (position)} {#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} {/each}
{/if} {/if}
@@ -22,6 +22,7 @@
maxWidth = Number.MAX_VALUE, maxWidth = Number.MAX_VALUE,
maxHeight = Number.MAX_VALUE, maxHeight = Number.MAX_VALUE,
keepAspectRatio = false, keepAspectRatio = false,
autoScale = true,
shouldResize, shouldResize,
onResizeStart, onResizeStart,
onResize, onResize,
@@ -40,10 +41,10 @@
let resizeControlRef: HTMLDivElement; let resizeControlRef: HTMLDivElement;
let resizer: XYResizerInstance | null = $state(null); let resizer: XYResizerInstance | null = $state(null);
let isLineVariant = $derived(variant === ResizeControlVariant.Line);
let controlPosition = $derived.by(() => { let controlPosition = $derived.by(() => {
let defaultPosition = ( let defaultPosition = (isLineVariant ? 'right' : 'bottom-right') as ControlPosition;
variant === ResizeControlVariant.Line ? 'right' : 'bottom-right'
) as ControlPosition;
return position ?? defaultPosition; return position ?? defaultPosition;
}); });
@@ -119,8 +120,9 @@
<div <div
class={['svelte-flow__resize-control', store.noDragClass, ...positionClasses, variant, className]} class={['svelte-flow__resize-control', store.noDragClass, ...positionClasses, variant, className]}
bind:this={resizeControlRef} bind:this={resizeControlRef}
style:border-color={variant === ResizeControlVariant.Line ? color : undefined} style:border-color={isLineVariant ? color : undefined}
style:background-color={variant === ResizeControlVariant.Line ? undefined : color} style:background-color={isLineVariant ? undefined : color}
style:scale={isLineVariant || !autoScale ? undefined : Math.max(1 / store.viewport.zoom, 1)}
{...rest} {...rest}
> >
{@render children?.()} {@render children?.()}
@@ -36,6 +36,8 @@ export type NodeResizerProps = {
maxHeight?: number; maxHeight?: number;
/** Keep aspect ratio when resizing */ /** Keep aspect ratio when resizing */
keepAspectRatio?: boolean; keepAspectRatio?: boolean;
/** Automatically scale the node when resizing */
autoScale?: boolean;
/** Callback to determine if node should resize */ /** Callback to determine if node should resize */
shouldResize?: ShouldResize; shouldResize?: ShouldResize;
/** Callback called when resizing starts */ /** Callback called when resizing starts */
@@ -55,6 +57,7 @@ export type ResizeControlProps = Pick<
| 'maxWidth' | 'maxWidth'
| 'maxHeight' | 'maxHeight'
| 'keepAspectRatio' | 'keepAspectRatio'
| 'autoScale'
| 'shouldResize' | 'shouldResize'
| 'onResizeStart' | 'onResizeStart'
| 'onResize' | 'onResize'
+3 -3
View File
@@ -28,12 +28,12 @@
/* handle styles */ /* handle styles */
.xy-flow__resize-control.handle { .xy-flow__resize-control.handle {
width: 4px; width: 5px;
height: 4px; height: 5px;
border: 1px solid #fff; border: 1px solid #fff;
border-radius: 1px; border-radius: 1px;
background-color: var(--xy-resize-background-color, var(--xy-resize-background-color-default)); 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 { .xy-flow__resize-control.handle.left {