fix: restore connection-handle magnet area (unclip node root)
The overflow-hidden added to the BaseNode root (to tame the oversized 16px
NodeResizer handles) also clipped the connection handles, which sit centred on
the node edge with half their box outside it. CSS overflow:hidden disables
pointer events in the clipped region, so the outer half of every handle — the
side an edge approaches from — became non-interactive, halving the magnet area.
- Remove overflow-hidden from the BaseNode root so handles stay fully grabbable.
- Shrink the NodeResizer handles 16px -> 8px (rounded): the reason the clip was
added in the first place, so the resizer no longer looks oversized when zoomed
out without relying on clipping.
- Enlarge the invisible target (magnet) hit area 12px -> 20px.
- Set connectionRadius={30} on ReactFlow for stronger distance-based snapping.
ha-relevant: yes
This commit is contained in:
@@ -192,6 +192,7 @@ export function CanvasContainer({ onConnect: onConnectProp, onEdgeDoubleClick, o
|
||||
colorMode={theme.colors.reactFlowColorMode}
|
||||
elevateNodesOnSelect={false}
|
||||
connectionMode={ConnectionMode.Loose}
|
||||
connectionRadius={30}
|
||||
isValidConnection={isValidConnection}
|
||||
>
|
||||
<Background
|
||||
|
||||
@@ -119,6 +119,14 @@ describe('BaseNode — borderWidth zoom scaling', () => {
|
||||
expect((container.firstChild as HTMLElement).style.borderWidth).toBe('1px')
|
||||
})
|
||||
|
||||
// Regression: the node root must NOT clip its own bounds, otherwise the outer
|
||||
// half of each connection handle (which sits centred on the edge) becomes
|
||||
// non-interactive and the "magnet" snap area is halved.
|
||||
it('root does not clip overflow (keeps handles grabbable)', () => {
|
||||
const { container } = renderBaseNode({})
|
||||
expect((container.firstChild as HTMLElement).className).not.toContain('overflow-hidden')
|
||||
})
|
||||
|
||||
it('boxShadow glow ring uses borderWidth when selected + online at zoom=0.5', () => {
|
||||
mockZoom = 0.5
|
||||
const node = makeNode({ status: 'online' })
|
||||
|
||||
@@ -56,7 +56,7 @@ export function BaseNode({ id, data, selected, icon: typeIcon, width, height }:
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex flex-col rounded-lg border transition-all duration-200 overflow-hidden"
|
||||
className="relative flex flex-col rounded-lg border transition-all duration-200"
|
||||
style={{
|
||||
background: colors.background,
|
||||
borderColor: colors.border,
|
||||
@@ -81,7 +81,7 @@ export function BaseNode({ id, data, selected, icon: typeIcon, width, height }:
|
||||
minWidth={140}
|
||||
minHeight={50}
|
||||
lineStyle={{ borderColor: 'transparent' }}
|
||||
handleStyle={{ borderColor: colors.border, background: colors.border, width: 16, height: 16 }}
|
||||
handleStyle={{ borderColor: colors.border, background: colors.border, width: 8, height: 8, borderRadius: 2 }}
|
||||
/>
|
||||
<SideHandles
|
||||
data={data}
|
||||
|
||||
@@ -73,7 +73,7 @@ export function SideHandles({
|
||||
type="target"
|
||||
position={POSITION[side]}
|
||||
id={targetId}
|
||||
style={{ ...offset, opacity: 0, width: 12, height: 12 }}
|
||||
style={{ ...offset, opacity: 0, width: 20, height: 20 }}
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { render } from '@testing-library/react'
|
||||
import { ReactFlowProvider } from '@xyflow/react'
|
||||
import { SideHandles } from '../SideHandles'
|
||||
import type { NodeData } from '@/types'
|
||||
|
||||
function renderHandles(data: Partial<NodeData> = {}) {
|
||||
const full: NodeData = { label: 'n', type: 'server', status: 'online', services: [], ...data }
|
||||
return render(
|
||||
<ReactFlowProvider>
|
||||
<SideHandles
|
||||
data={full}
|
||||
handleBackground="#30363d"
|
||||
handleBorder="#30363d"
|
||||
labelColor="#8b949e"
|
||||
/>
|
||||
</ReactFlowProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('SideHandles', () => {
|
||||
it('renders a source + invisible target handle per slot', () => {
|
||||
// default node: top=1, bottom=1, left=0, right=0
|
||||
const { container } = renderHandles({})
|
||||
expect(container.querySelectorAll('.react-flow__handle.source').length).toBe(2)
|
||||
expect(container.querySelectorAll('.react-flow__handle.target').length).toBe(2)
|
||||
})
|
||||
|
||||
it('target (magnet) handle hit area is large enough to snap onto (20px)', () => {
|
||||
const { container } = renderHandles({})
|
||||
const target = container.querySelector('.react-flow__handle.target') as HTMLElement
|
||||
expect(target.style.width).toBe('20px')
|
||||
expect(target.style.height).toBe('20px')
|
||||
expect(target.style.opacity).toBe('0')
|
||||
})
|
||||
|
||||
it('renders configured per-side counts', () => {
|
||||
const { container } = renderHandles({ top_handles: 2, left_handles: 3, right_handles: 1, bottom_handles: 1 })
|
||||
expect(container.querySelectorAll('.react-flow__handle.source').length).toBe(7)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user