From fa7bf16ce38f51a275784f9d67d0853396b66d29 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Mon, 27 Oct 2025 15:35:03 +0100 Subject: [PATCH] allow paneClick events to happen more leniantly --- examples/react/src/examples/Figma/index.tsx | 3 +- .../src/routes/examples/figma/+page.svelte | 7 +- packages/react/src/container/Pane/index.tsx | 40 +++++---- .../svelte/src/lib/container/Pane/Pane.svelte | 88 +++++++++++-------- 4 files changed, 79 insertions(+), 59 deletions(-) diff --git a/examples/react/src/examples/Figma/index.tsx b/examples/react/src/examples/Figma/index.tsx index f6daab75..136d0629 100644 --- a/examples/react/src/examples/Figma/index.tsx +++ b/examples/react/src/examples/Figma/index.tsx @@ -34,7 +34,7 @@ const BasicFlow = () => { selectionMode={SelectionMode.Partial} panOnDrag={panOnDrag} panOnScroll - paneClickDistance={2} + paneClickDistance={100} zoomActivationKeyCode="Meta" multiSelectionKeyCode={MULTI_SELECT_KEY} onPaneContextMenu={onPaneContextMenu} @@ -44,6 +44,7 @@ const BasicFlow = () => { onMoveStart={onMoveStart} onMove={onMove} onMoveEnd={onMoveEnd} + onPaneClick={(e) => console.log('pane click', e)} > diff --git a/examples/svelte/src/routes/examples/figma/+page.svelte b/examples/svelte/src/routes/examples/figma/+page.svelte index 35a694ac..0b4c0b6a 100644 --- a/examples/svelte/src/routes/examples/figma/+page.svelte +++ b/examples/svelte/src/routes/examples/figma/+page.svelte @@ -9,11 +9,6 @@ import '@xyflow/svelte/dist/style.css'; - const onPaneContextMenu = (e: any) => { - e.preventDefault(); - console.log('context menu'); - }; - const panOnDrag = [1, 2]; const onmovestart = (e: any) => console.log('move start', e); @@ -46,10 +41,12 @@ selectionMode={SelectionMode.Partial} selectionOnDrag panOnScroll + paneClickDistance={100} {panOnDrag} {onmovestart} {onmove} {onmoveend} + onpaneclick={(e) => console.log('on pane click', e)} > diff --git a/packages/react/src/container/Pane/index.tsx b/packages/react/src/container/Pane/index.tsx index aadac2e0..f952434b 100644 --- a/packages/react/src/container/Pane/index.tsx +++ b/packages/react/src/container/Pane/index.tsx @@ -67,6 +67,7 @@ export function Pane({ selectionKeyPressed, selectionMode = SelectionMode.Full, panOnDrag, + paneClickDistance = 0, selectionOnDrag, onSelectionStart, onSelectionEnd, @@ -117,14 +118,9 @@ export function Pane({ const onWheel = onPaneScroll ? (event: React.WheelEvent) => onPaneScroll(event) : undefined; const onClickCapture = (event: ReactMouseEvent) => { - const isSelectionOnDragActive = - (selectionOnDrag && container.current === event.target) || !selectionOnDrag || selectionKeyPressed; - - if (!isSelectionOnDragActive) { - return; + if (selectionInProgress.current) { + event.stopPropagation(); } - - event.stopPropagation(); }; // We are using capture here in order to prevent other pointer events @@ -149,9 +145,6 @@ export function Pane({ return; } - event.stopPropagation(); - event.preventDefault(); - (event.target as Partial)?.setPointerCapture?.(event.pointerId); selectionStarted.current = true; @@ -159,8 +152,6 @@ export function Pane({ const { x, y } = getEventPosition(event.nativeEvent, containerBounds.current); - resetSelectedElements(); - store.setState({ userSelectionRect: { width: 0, @@ -172,7 +163,14 @@ export function Pane({ }, }); - onSelectionStart?.(event); + if (event.target !== container.current || paneClickDistance === 0) { + event.stopPropagation(); + event.preventDefault(); + + resetSelectedElements(); + + onSelectionStart?.(event); + } }; const onPointerMove = (event: ReactPointerEvent): void => { @@ -191,11 +189,23 @@ export function Pane({ return; } - selectionInProgress.current = true; - const { x: mouseX, y: mouseY } = getEventPosition(event.nativeEvent, containerBounds.current); const { startX, startY } = userSelectionRect; + if ( + !selectionInProgress.current && + event.target === container.current && + !selectionKeyPressed && + paneClickDistance > 0 + ) { + const distance = Math.hypot(mouseX - startX, mouseY - startY); + if (distance <= paneClickDistance) { + return; + } + } + + selectionInProgress.current = true; + const nextUserSelectRect = { startX, startY, diff --git a/packages/svelte/src/lib/container/Pane/Pane.svelte b/packages/svelte/src/lib/container/Pane/Pane.svelte index ef77b1de..fed9d005 100644 --- a/packages/svelte/src/lib/container/Pane/Pane.svelte +++ b/packages/svelte/src/lib/container/Pane/Pane.svelte @@ -47,6 +47,7 @@ let { store = $bindable(), panOnDrag = true, + paneClickDistance = 1, selectionOnDrag, onpaneclick, onpanecontextmenu, @@ -67,7 +68,7 @@ let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag); let isSelecting = $derived( store.selectionKeyPressed || - store.selectionRect || + !!store.selectionRect || (selectionOnDrag && panOnDragActive !== true) ); let hasActiveSelection = $derived( @@ -76,19 +77,7 @@ // Used to prevent click events when the user lets go of the selectionKey during a selection let selectionInProgress = false; - - function onClick(event: MouseEvent) { - // We prevent click events when the user let go of the selectionKey during a selection - // We also prevent click events when a connection is in progress - if (selectionInProgress || store.connection.inProgress) { - selectionInProgress = false; - return; - } - - onpaneclick?.({ event }); - store.unselectNodesAndEdges(); - store.selectionRectMode = null; - } + let selectionStarted = false; // We start the selection process when the user clicks down on the pane function onPointerDownCapture(event: PointerEvent) { @@ -114,14 +103,12 @@ return; } - event.stopPropagation(); - event.preventDefault(); - (event.target as Partial)?.setPointerCapture?.(event.pointerId); - const { x, y } = getEventPosition(event, containerBounds); + selectionStarted = true; + selectionInProgress = false; - store.unselectNodesAndEdges(); + const { x, y } = getEventPosition(event, containerBounds); store.selectionRect = { width: 0, @@ -132,7 +119,14 @@ y }; - onselectionstart?.(event); + if (event.target !== container || paneClickDistance === 0) { + event.stopPropagation(); + event.preventDefault(); + + store.unselectNodesAndEdges(); + + onselectionstart?.(event); + } } function onPointerMove(event: PointerEvent) { @@ -140,11 +134,23 @@ return; } - selectionInProgress = true; - const mousePos = getEventPosition(event, containerBounds); const { startX = 0, startY = 0 } = store.selectionRect; + if ( + !selectionInProgress && + event.target === container && + !store.selectionKeyPressed && + paneClickDistance > 0 + ) { + const distance = Math.hypot(mousePos.x - startX, mousePos.y - startY); + if (distance <= paneClickDistance) { + return; + } + } + + selectionInProgress = true; + const nextUserSelectRect = { ...store.selectionRect, x: mousePos.x < startX ? mousePos.x : startX, @@ -195,7 +201,7 @@ } function onPointerUp(event: PointerEvent) { - if (event.button !== 0) { + if (event.button !== 0 || !selectionStarted) { return; } @@ -203,22 +209,20 @@ // We only want to trigger click functions when in selection mode if // the user did not move the mouse. - if (!isSelecting && store.selectionRectMode === 'user' && event.target === container) { + + if (!selectionInProgress && event.target === container) { onClick?.(event); } + store.selectionRect = null; - if (selectedNodeIds.size > 0) { + if (selectionInProgress && selectedNodeIds.size > 0) { store.selectionRectMode = 'nodes'; } - // If the user kept holding the selectionKey during the selection, - // we need to reset the selectionInProgress, so the next click event is not prevented - if (store.selectionKeyPressed) { - selectionInProgress = false; - } - onselectionend?.(event); + + selectionStarted = false; } const onContextMenu = (event: MouseEvent) => { @@ -231,17 +235,25 @@ }; const onClickCapture = (event: MouseEvent) => { - const isSelectionActive = - (selectionOnDrag && container === event.target) || - !selectionOnDrag || - store.selectionKeyPressed; + if (selectionInProgress) { + event.stopPropagation(); + selectionInProgress = false; + } + }; - if (!isSelectionActive) { + function onClick(event: MouseEvent) { + // We prevent click events when the user let go of the selectionKey during a selection + // We also prevent click events when a connection is in progress + if (selectionInProgress || store.connection.inProgress) { + selectionInProgress = false; return; } - event.stopPropagation(); - }; + onpaneclick?.({ event }); + store.unselectNodesAndEdges(); + store.selectionRectMode = null; + store.selectionRect = null; + }