From a488322c551c2990a40c6faa4485282ec656d2f3 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Mon, 2 Jun 2025 16:00:42 +0200 Subject: [PATCH 01/14] feat(onFocus): pan nodes into the viewport on tab --- .../src/components/NodeWrapper/index.tsx | 14 ++++++++++++ .../components/NodeWrapper/NodeWrapper.svelte | 22 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 5c0ce08b..e507a02c 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -155,6 +155,19 @@ export function NodeWrapper({ } }; + const onFocus = () => { + const { panZoom } = store.getState(); + const zoom = panZoom?.getViewport().zoom ?? 1; + panZoom?.setViewport( + { + x: -(internals.positionAbsolute.x + nodeDimensions.width / 2) * zoom + window.innerWidth / 2, + y: -(internals.positionAbsolute.y + nodeDimensions.height / 2) * zoom + window.innerHeight / 2, + zoom: zoom, + }, + { duration: 100 } + ); + }; + return (
({ onDoubleClick={onDoubleClickHandler} onKeyDown={isFocusable ? onKeyDown : undefined} tabIndex={isFocusable ? 0 : undefined} + onFocus={isFocusable ? onFocus : undefined} role={isFocusable ? 'button' : undefined} aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`} aria-label={node.ariaLabel} diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index f053cd82..24556d42 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -196,6 +196,27 @@ store.moveSelectedNodes(arrowKeyDiffs[event.key], event.shiftKey ? 4 : 1); } } + + function onFocus() { + if (store.disableKeyboardA11y) { + return; + } + + const zoom = store.panZoom?.getViewport().zoom ?? 1; + + // Get node dimensions + const nodeWidth = nodeRef?.offsetWidth || 0; + const nodeHeight = nodeRef?.offsetHeight || 0; + + store.panZoom?.setViewport( + { + x: -(positionX + nodeWidth / 2) * zoom + window.innerWidth / 2, + y: -(positionY + nodeHeight / 2) * zoom + window.innerHeight / 2, + zoom: zoom, + }, + { duration: 100 } + ); + } {#if !hidden} @@ -251,6 +272,7 @@ ? (event) => onnodecontextmenu({ node: userNode, event }) : undefined} onkeydown={focusable ? onKeyDown : undefined} + onfocus={focusable ? onFocus : undefined} tabIndex={focusable ? 0 : undefined} role={focusable ? 'button' : undefined} aria-describedby={store.disableKeyboardA11y From 69ff04335041ace8f10b31794d267f1bc4a3946c Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Mon, 2 Jun 2025 16:04:00 +0200 Subject: [PATCH 02/14] chore(onFocus): add disableKeyboardA11y --- packages/react/src/components/NodeWrapper/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index e507a02c..afdd9de5 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -156,6 +156,10 @@ export function NodeWrapper({ }; const onFocus = () => { + if (disableKeyboardA11y) { + return; + } + const { panZoom } = store.getState(); const zoom = panZoom?.getViewport().zoom ?? 1; panZoom?.setViewport( From 09fab6794031410c9e9465281d038c3520afe783 Mon Sep 17 00:00:00 2001 From: printer_scanner Date: Mon, 2 Jun 2025 16:05:55 +0200 Subject: [PATCH 03/14] chore: create changeset --- .changeset/thirty-snakes-float.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/thirty-snakes-float.md diff --git a/.changeset/thirty-snakes-float.md b/.changeset/thirty-snakes-float.md new file mode 100644 index 00000000..dd729387 --- /dev/null +++ b/.changeset/thirty-snakes-float.md @@ -0,0 +1,6 @@ +--- +"@xyflow/react": minor +"@xyflow/svelte": minor +--- + +feat(NodeWrapper): focus nodes in the viewport on tab From e6fe3dcd471f42de34f3f22b8b521b04133a9354 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Tue, 3 Jun 2025 16:55:24 +0200 Subject: [PATCH 04/14] chore: only pan focus when out of viewport --- .../src/components/NodeWrapper/index.tsx | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index afdd9de5..3560fe13 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -18,6 +18,7 @@ import { handleNodeClick } from '../Nodes/utils'; import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from './utils'; import { useNodeObserver } from './useNodeObserver'; import type { InternalNode, Node, NodeWrapperProps } from '../../types'; +import { useReactFlow } from '../../hooks/useReactFlow'; export function NodeWrapper({ id, @@ -78,6 +79,8 @@ export function NodeWrapper({ nodeClickDistance, }); const moveSelectedNodes = useMoveSelectedNodes(); + const { fitView } = useReactFlow(); + const { getViewport } = useReactFlow(); if (node.hidden) { return null; @@ -160,16 +163,26 @@ export function NodeWrapper({ return; } - const { panZoom } = store.getState(); - const zoom = panZoom?.getViewport().zoom ?? 1; - panZoom?.setViewport( - { - x: -(internals.positionAbsolute.x + nodeDimensions.width / 2) * zoom + window.innerWidth / 2, - y: -(internals.positionAbsolute.y + nodeDimensions.height / 2) * zoom + window.innerHeight / 2, - zoom: zoom, - }, - { duration: 100 } - ); + // Return early if focus is not from keyboard navigation (i.e., was clicked) + if (!nodeRef.current?.matches(':focus-visible')) { + return; + } + const { x, y, zoom } = getViewport(); + + const isNodeVisible = + node.position.x >= x && + node.position.x <= x + window.innerWidth && + node.position.y >= y && + node.position.y <= y + window.innerHeight; + + if (!isNodeVisible) { + fitView({ + nodes: [{ id }], + duration: 100, + minZoom: zoom, + maxZoom: zoom, + }); + } }; return ( From 343ed4364bd752ab7cda40bd74ee8ad6940fb468 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Wed, 4 Jun 2025 12:08:11 +0200 Subject: [PATCH 05/14] chore: update a11y example for testing --- examples/react/src/examples/A11y/index.tsx | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/examples/react/src/examples/A11y/index.tsx b/examples/react/src/examples/A11y/index.tsx index f7f0b5b2..a4d0f2ad 100644 --- a/examples/react/src/examples/A11y/index.tsx +++ b/examples/react/src/examples/A11y/index.tsx @@ -23,25 +23,34 @@ const initialNodes: Node[] = [ type: 'input', data: { label: 'A11y Node 1' }, position: { x: 250, y: 5 }, - className: 'light', }, { id: '2', data: { label: 'Node 2' }, - position: { x: 100, y: 100 }, - className: 'light', + position: { x: 1000, y: 100 }, }, { id: '3', data: { label: 'Node 3' }, - position: { x: 400, y: 100 }, + position: { x: 100, y: 100 }, className: 'light', + ariaRoleDescription: 'custom node role', + ariaRole: 'button', + }, + { + id: '4', + data: { label: 'Node 4' }, + position: { x: 300, y: 100 }, + className: 'light', + ariaRoleDescription: 'custom node role', + ariaRole: 'button', }, ]; const initialEdges: Edge[] = [ { id: 'e1-2', source: '1', target: '2', animated: true }, { id: 'e1-3', source: '1', target: '3' }, + { id: 'e1-4', source: '1', target: '4' }, ]; const ariaLabelConfig: Partial = { @@ -69,9 +78,9 @@ const A11y = () => { onNodeDragStart={onNodeDragStart} onNodeDrag={onNodeDrag} className="react-flow-basic-example" - minZoom={0.2} + minZoom={2} maxZoom={4} - fitView + // fitView selectNodesOnDrag={false} elevateEdgesOnSelect elevateNodesOnSelect={false} From 6693c7300e41609cb50c693af46c3ca6891aa244 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Thu, 5 Jun 2025 12:22:06 +0200 Subject: [PATCH 06/14] feat: create enablePanOnFocus prop, update onFocus func --- examples/react/src/examples/A11y/index.tsx | 46 +++++++++++------ .../src/routes/examples/a11y/+page.svelte | 39 +++++++++++---- .../src/components/NodeWrapper/index.tsx | 20 ++++---- .../src/components/StoreUpdater/index.tsx | 1 + .../src/container/NodeRenderer/index.tsx | 11 ++-- .../react/src/container/ReactFlow/index.tsx | 2 + packages/react/src/store/initialState.ts | 1 + packages/react/src/types/component-props.ts | 5 ++ packages/react/src/types/nodes.ts | 1 + packages/react/src/types/store.ts | 1 + .../components/NodeWrapper/NodeWrapper.svelte | 50 ++++++++++++------- .../lib/container/SvelteFlow/Wrapper.svelte | 1 + .../src/lib/container/SvelteFlow/types.ts | 5 ++ .../src/lib/store/initial-store.svelte.ts | 1 + 14 files changed, 125 insertions(+), 59 deletions(-) diff --git a/examples/react/src/examples/A11y/index.tsx b/examples/react/src/examples/A11y/index.tsx index a4d0f2ad..daf3ce47 100644 --- a/examples/react/src/examples/A11y/index.tsx +++ b/examples/react/src/examples/A11y/index.tsx @@ -1,4 +1,4 @@ -import { MouseEvent } from 'react'; +import { MouseEvent, useState } from 'react'; import { ReactFlow, MiniMap, @@ -10,13 +10,9 @@ import { Edge, OnNodeDrag, AriaLabelConfig, + Panel, } from '@xyflow/react'; -const onNodeDrag: OnNodeDrag = (_, node: Node, nodes: Node[]) => console.log('drag', node, nodes); -const onNodeDragStart = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes); -const onNodeDragStop = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag stop', node, nodes); -const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); - const initialNodes: Node[] = [ { id: '1', @@ -41,9 +37,16 @@ const initialNodes: Node[] = [ id: '4', data: { label: 'Node 4' }, position: { x: 300, y: 100 }, - className: 'light', - ariaRoleDescription: 'custom node role', - ariaRole: 'button', + }, + { + id: '5', + data: { label: 'Node 5' }, + position: { x: 400, y: 200 }, + }, + { + id: '6', + data: { label: 'Node 6' }, + position: { x: -1000, y: 200 }, }, ]; @@ -51,6 +54,8 @@ const initialEdges: Edge[] = [ { id: 'e1-2', source: '1', target: '2', animated: true }, { id: 'e1-3', source: '1', target: '3' }, { id: 'e1-4', source: '1', target: '4' }, + { id: 'e1-5', source: '4', target: '5' }, + { id: 'e1-6', source: '3', target: '6' }, ]; const ariaLabelConfig: Partial = { @@ -68,19 +73,14 @@ const ariaLabelConfig: Partial = { }; const A11y = () => { + const [isFocusPannable, setEnablePanOnFocus] = useState(true); return ( { + +
+ +
+
); }; diff --git a/examples/svelte/src/routes/examples/a11y/+page.svelte b/examples/svelte/src/routes/examples/a11y/+page.svelte index 07f53110..962cd726 100644 --- a/examples/svelte/src/routes/examples/a11y/+page.svelte +++ b/examples/svelte/src/routes/examples/a11y/+page.svelte @@ -1,5 +1,5 @@ - - + 'node.a11yDescription.ariaLiveMessage': ({ direction, x, y }: { direction: string; x: number; y: number }) => `Custom Moved selected node ${direction}. New position, x: ${x}, y: ${y}`, 'edge.a11yDescription.default': 'Svelte Custom Edge Desc.', 'controls.ariaLabel': 'Svelte Custom Control Aria Label', @@ -37,9 +33,30 @@ // 'controls.fitView.ariaLabel': 'Svelte Custom Fit View', 'controls.interactive.ariaLabel': 'Svelte Custom Toggle Interactivity', 'minimap.ariaLabel': 'Svelte Custom Minimap' - }} + } + ); + + + + +
+ +
+
diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 49b74174..f3542b22 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -7,6 +7,7 @@ import { getNodeDimensions, isInputDOMNode, nodeHasDimensions, + getNodesInside, } from '@xyflow/system'; import { useStore, useStoreApi } from '../../hooks/useStore'; @@ -29,6 +30,7 @@ export function NodeWrapper({ onContextMenu, onDoubleClick, nodesDraggable, + enablePanOnFocus, elementsSelectable, nodesConnectable, nodesFocusable, @@ -163,28 +165,26 @@ export function NodeWrapper({ }; const onFocus = () => { - if (disableKeyboardA11y) { + if (disableKeyboardA11y || !enablePanOnFocus) { return; } - // Return early if focus is not from keyboard navigation (i.e., was clicked) if (!nodeRef.current?.matches(':focus-visible')) { return; } - const { x, y, zoom } = getViewport(); - const isNodeVisible = - node.position.x >= x && - node.position.x <= x + window.innerWidth && - node.position.y >= y && - node.position.y <= y + window.innerHeight; + const { transform, width, height } = store.getState(); + const visibleNodes = getNodesInside(new Map([[id, node]]), { x: 0, y: 0, width, height }, transform, true); + + const isNodeVisible = visibleNodes.length > 0; if (!isNodeVisible) { + const zoomLevel = transform[2]; fitView({ nodes: [{ id }], duration: 100, - minZoom: zoom, - maxZoom: zoom, + minZoom: zoomLevel, + maxZoom: zoomLevel, }); } }; diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index 95b713af..ba0d66bf 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -23,6 +23,7 @@ const reactFlowFieldsToTrack = [ 'onClickConnectStart', 'onClickConnectEnd', 'nodesDraggable', + 'enablePanOnFocus', 'nodesConnectable', 'nodesFocusable', 'edgesFocusable', diff --git a/packages/react/src/container/NodeRenderer/index.tsx b/packages/react/src/container/NodeRenderer/index.tsx index 02126d73..093e3f95 100644 --- a/packages/react/src/container/NodeRenderer/index.tsx +++ b/packages/react/src/container/NodeRenderer/index.tsx @@ -29,6 +29,7 @@ export type NodeRendererProps = Pick< const selector = (s: ReactFlowState) => ({ nodesDraggable: s.nodesDraggable, + enablePanOnFocus: s.enablePanOnFocus, nodesConnectable: s.nodesConnectable, nodesFocusable: s.nodesFocusable, elementsSelectable: s.elementsSelectable, @@ -36,7 +37,10 @@ const selector = (s: ReactFlowState) => ({ }); function NodeRendererComponent(props: NodeRendererProps) { - const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, onError } = useStore(selector, shallow); + const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, enablePanOnFocus, onError } = useStore( + selector, + shallow + ); const nodeIds = useVisibleNodeIds(props.onlyRenderVisibleElements); const resizeObserver = useResizeObserver(); @@ -48,13 +52,13 @@ function NodeRendererComponent(props: NodeRendererProps(props: NodeRendererProps( onlyRenderVisibleElements = false, selectNodesOnDrag, nodesDraggable, + enablePanOnFocus, nodesConnectable, nodesFocusable, nodeOrigin = defaultNodeOrigin, @@ -261,6 +262,7 @@ function ReactFlow( onClickConnectStart={onClickConnectStart} onClickConnectEnd={onClickConnectEnd} nodesDraggable={nodesDraggable} + enablePanOnFocus={enablePanOnFocus} nodesConnectable={nodesConnectable} nodesFocusable={nodesFocusable} edgesFocusable={edgesFocusable} diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 15bfc1d8..77d10769 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -112,6 +112,7 @@ const getInitialState = ({ snapToGrid: false, nodesDraggable: true, + enablePanOnFocus: false, nodesConnectable: true, nodesFocusable: true, edgesFocusable: true, diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index d088c26b..f09995bf 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -383,6 +383,11 @@ export interface ReactFlowProps = { nodesConnectable: boolean; elementsSelectable: boolean; nodesDraggable: boolean; + enablePanOnFocus: boolean; nodesFocusable: boolean; onClick?: NodeMouseHandler; onDoubleClick?: NodeMouseHandler; diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index ba909869..25d8f42e 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -88,6 +88,7 @@ export type ReactFlowStore { + console.log('before', store.enablePanOnFocus); + if (store.disableKeyboardA11y || !store.enablePanOnFocus) { + console.log("should return early", store.enablePanOnFocus); return; } - - const zoom = store.panZoom?.getViewport().zoom ?? 1; - - // Get node dimensions - const nodeWidth = nodeRef?.offsetWidth || 0; - const nodeHeight = nodeRef?.offsetHeight || 0; - - store.panZoom?.setViewport( - { - x: -(positionX + nodeWidth / 2) * zoom + window.innerWidth / 2, - y: -(positionY + nodeHeight / 2) * zoom + window.innerHeight / 2, - zoom: zoom, - }, - { duration: 100 } - ); - } + + if (!nodeRef?.matches(':focus-visible')) { + return; + } + const width = store.width; + const height = store.height; + const viewport: [number, number, number] = [store.viewport.x, store.viewport.y, store.viewport.zoom]; + const zoom = store.viewport.zoom; + + const visibleNodes = getNodesInside(new Map([[id, node]]), { x: 0, y: 0, width, height }, viewport, true); + + const isNodeVisible = visibleNodes.length > 0; + + if (!isNodeVisible) { + console.log('About to call fitView - this should NOT happen when enablePanOnFocus is false'); + + console.log('after', store.enablePanOnFocus); + store.fitView({ + nodes: [{ id }], + duration: 100, + minZoom: zoom, + maxZoom: zoom, + }); + } + }; {#if !hidden} diff --git a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte index 0f7e39f6..80b6e0dd 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte @@ -84,6 +84,7 @@ elevateNodesOnSelect, elevateEdgesOnSelect, nodesDraggable, + enablePanOnFocus, nodesConnectable, elementsSelectable, nodesFocusable, diff --git a/packages/svelte/src/lib/container/SvelteFlow/types.ts b/packages/svelte/src/lib/container/SvelteFlow/types.ts index 38e07f4a..0d67197c 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/types.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/types.ts @@ -232,6 +232,11 @@ export type SvelteFlowProps< * @default true */ nodesDraggable?: boolean; + /** + * When `true`, the viewport will pan when a node is focused. + * @default false + */ + enablePanOnFocus?: boolean; /** * Controls if all nodes should be connectable to each other * @default true diff --git a/packages/svelte/src/lib/store/initial-store.svelte.ts b/packages/svelte/src/lib/store/initial-store.svelte.ts index 7971f320..bedb2e2d 100644 --- a/packages/svelte/src/lib/store/initial-store.svelte.ts +++ b/packages/svelte/src/lib/store/initial-store.svelte.ts @@ -247,6 +247,7 @@ export function getInitialStore Date: Thu, 5 Jun 2025 13:42:27 +0200 Subject: [PATCH 07/14] fix(SvelteFlow): fix bug that auto-scrolls the pane --- .../src/lib/container/SvelteFlow/Wrapper.svelte | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte index 80b6e0dd..bd42d658 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte @@ -101,6 +101,16 @@ type OnlyDivAttributes = { [K in keyof T]: K extends keyof HTMLAttributes ? T[K] : never; }; + + // Undo scroll events, preventing viewport from shifting when nodes outside of it are focused + function wrapperOnScroll(e: UIEvent & { currentTarget: EventTarget & HTMLDivElement }) { + e.currentTarget.scrollTo({ top: 0, left: 0, behavior: 'auto' }); + + // Forward the event to any existing onscroll handler if needed + if (rest.onscroll) { + rest.onscroll(e); + } + }
} > {@render children?.()} From 59eb0df26ea0f3bda1a6c563e2780c429abb1564 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Thu, 5 Jun 2025 14:01:47 +0200 Subject: [PATCH 08/14] chore: remove console.logs --- .../src/lib/components/NodeWrapper/NodeWrapper.svelte | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index 538b74b4..1b5b1db7 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -200,15 +200,10 @@ } const onFocus = () => { - console.log('before', store.enablePanOnFocus); - if (store.disableKeyboardA11y || !store.enablePanOnFocus) { - console.log("should return early", store.enablePanOnFocus); + if (store.disableKeyboardA11y || !store.enablePanOnFocus || !nodeRef?.matches(':focus-visible')) { return; } - if (!nodeRef?.matches(':focus-visible')) { - return; - } const width = store.width; const height = store.height; const viewport: [number, number, number] = [store.viewport.x, store.viewport.y, store.viewport.zoom]; @@ -219,9 +214,6 @@ const isNodeVisible = visibleNodes.length > 0; if (!isNodeVisible) { - console.log('About to call fitView - this should NOT happen when enablePanOnFocus is false'); - - console.log('after', store.enablePanOnFocus); store.fitView({ nodes: [{ id }], duration: 100, From d434f0cde7111e4977e0b0733911e41ebdcb86f1 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 5 Jun 2025 16:28:02 +0200 Subject: [PATCH 09/14] chore(nodewrapper): cleanup --- examples/react/src/examples/A11y/index.tsx | 2 -- .../src/components/NodeWrapper/index.tsx | 26 +++++-------------- packages/react/src/hooks/useViewportHelper.ts | 20 +------------- packages/react/src/store/index.ts | 20 ++++++++++++++ packages/react/src/types/store.ts | 2 ++ tooling/eslint-config/src/index.js | 1 - 6 files changed, 30 insertions(+), 41 deletions(-) diff --git a/examples/react/src/examples/A11y/index.tsx b/examples/react/src/examples/A11y/index.tsx index daf3ce47..344a20e4 100644 --- a/examples/react/src/examples/A11y/index.tsx +++ b/examples/react/src/examples/A11y/index.tsx @@ -78,8 +78,6 @@ const A11y = () => { ({ id, @@ -81,8 +80,6 @@ export function NodeWrapper({ nodeClickDistance, }); const moveSelectedNodes = useMoveSelectedNodes(); - const { fitView } = useReactFlow(); - const { getViewport } = useReactFlow(); if (node.hidden) { return null; @@ -165,26 +162,17 @@ export function NodeWrapper({ }; const onFocus = () => { - if (disableKeyboardA11y || !enablePanOnFocus) { + if (disableKeyboardA11y || !enablePanOnFocus || !nodeRef.current?.matches(':focus-visible')) { return; } - if (!nodeRef.current?.matches(':focus-visible')) { - return; - } + const { transform, width, height, setCenter } = store.getState(); + const withinViewport = + getNodesInside(new Map([[id, node]]), { x: 0, y: 0, width, height }, transform, true).length > 0; - const { transform, width, height } = store.getState(); - const visibleNodes = getNodesInside(new Map([[id, node]]), { x: 0, y: 0, width, height }, transform, true); - - const isNodeVisible = visibleNodes.length > 0; - - if (!isNodeVisible) { - const zoomLevel = transform[2]; - fitView({ - nodes: [{ id }], - duration: 100, - minZoom: zoomLevel, - maxZoom: zoomLevel, + if (!withinViewport) { + setCenter(node.position.x + nodeDimensions.width / 2, node.position.y + nodeDimensions.height / 2, { + zoom: transform[2], }); } }; diff --git a/packages/react/src/hooks/useViewportHelper.ts b/packages/react/src/hooks/useViewportHelper.ts index fc01a150..eee97484 100644 --- a/packages/react/src/hooks/useViewportHelper.ts +++ b/packages/react/src/hooks/useViewportHelper.ts @@ -63,25 +63,7 @@ const useViewportHelper = (): ViewportHelperFunctions => { return { x, y, zoom }; }, setCenter: async (x, y, options) => { - const { width, height, maxZoom, panZoom } = store.getState(); - const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom; - const centerX = width / 2 - x * nextZoom; - const centerY = height / 2 - y * nextZoom; - - if (!panZoom) { - return Promise.resolve(false); - } - - await panZoom.setViewport( - { - x: centerX, - y: centerY, - zoom: nextZoom, - }, - { duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate } - ); - - return Promise.resolve(true); + return store.getState().setCenter(x, y, options); }, fitBounds: async (bounds, options) => { const { width, height, minZoom, maxZoom, panZoom } = store.getState(); diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 39702bd0..085f8b1e 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -360,6 +360,26 @@ const createStore = ({ return panBySystem({ delta, panZoom, transform, translateExtent, width, height }); }, + setCenter: async (x, y, options) => { + const { width, height, maxZoom, panZoom } = get(); + + if (!panZoom) { + return Promise.resolve(false); + } + + const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom; + + await panZoom.setViewport( + { + x: width / 2 - x * nextZoom, + y: height / 2 - y * nextZoom, + zoom: nextZoom, + }, + { duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate } + ); + + return Promise.resolve(true); + }, cancelConnection: () => { set({ connection: { ...initialConnection }, diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 25d8f42e..fd9eb91e 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -29,6 +29,7 @@ import { type EdgeChange, type ParentLookup, type AriaLabelConfig, + SetCenter, } from '@xyflow/system'; import type { @@ -172,6 +173,7 @@ export type ReactFlowActions = { triggerNodeChanges: (changes: NodeChange[]) => void; triggerEdgeChanges: (changes: EdgeChange[]) => void; panBy: PanBy; + setCenter: SetCenter; setPaneClickDistance: (distance: number) => void; }; diff --git a/tooling/eslint-config/src/index.js b/tooling/eslint-config/src/index.js index 60f82b0e..0a80797e 100644 --- a/tooling/eslint-config/src/index.js +++ b/tooling/eslint-config/src/index.js @@ -36,7 +36,6 @@ module.exports = { projectService: true, }, rules: { - '@typescript-eslint/no-deprecated': 'error', '@typescript-eslint/no-unnecessary-type-assertion': 'error', }, }, From c44f0c61564f152763a60f07f6b5c08041fcb4d8 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 5 Jun 2025 16:40:16 +0200 Subject: [PATCH 10/14] chore(svelte/nodewrapper): cleanup --- .../components/NodeWrapper/NodeWrapper.svelte | 36 ++++++++++--------- .../src/lib/hooks/useSvelteFlow.svelte.ts | 24 ++----------- packages/svelte/src/lib/store/index.ts | 24 ++++++++++++- packages/svelte/src/lib/store/types.ts | 4 ++- 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index 1b5b1db7..a04f7dc7 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -6,7 +6,7 @@ isInputDOMNode, nodeHasDimensions, Position, - getNodesInside, + getNodesInside } from '@xyflow/system'; import drag from '$lib/actions/drag'; @@ -200,26 +200,30 @@ } const onFocus = () => { - if (store.disableKeyboardA11y || !store.enablePanOnFocus || !nodeRef?.matches(':focus-visible')) { + if ( + store.disableKeyboardA11y || + !store.enablePanOnFocus || + !nodeRef?.matches(':focus-visible') + ) { return; } - const width = store.width; - const height = store.height; - const viewport: [number, number, number] = [store.viewport.x, store.viewport.y, store.viewport.zoom]; - const zoom = store.viewport.zoom; - - const visibleNodes = getNodesInside(new Map([[id, node]]), { x: 0, y: 0, width, height }, viewport, true); + const { width, height, viewport } = store; - const isNodeVisible = visibleNodes.length > 0; + const withinViewport = + getNodesInside( + new Map([[id, node]]), + { x: 0, y: 0, width, height }, + [viewport.x, viewport.y, viewport.zoom], + true + ).length > 0; - if (!isNodeVisible) { - store.fitView({ - nodes: [{ id }], - duration: 100, - minZoom: zoom, - maxZoom: zoom, - }); + if (!withinViewport) { + store.setCenter( + node.position.x + (node.measured.width ?? 0) / 2, + node.position.y + (node.measured.height ?? 0) / 2, + { zoom: viewport.zoom } + ); } }; diff --git a/packages/svelte/src/lib/hooks/useSvelteFlow.svelte.ts b/packages/svelte/src/lib/hooks/useSvelteFlow.svelte.ts index 82c66064..c4869c33 100644 --- a/packages/svelte/src/lib/hooks/useSvelteFlow.svelte.ts +++ b/packages/svelte/src/lib/hooks/useSvelteFlow.svelte.ts @@ -366,28 +366,8 @@ export function useSvelteFlow $state.snapshot(store.viewport), - setCenter: async (x, y, options) => { - const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : store.maxZoom; - const currentPanZoom = store.panZoom; - - if (!currentPanZoom) { - return Promise.resolve(false); - } - - await currentPanZoom.setViewport( - { - x: store.width / 2 - x * nextZoom, - y: store.height / 2 - y * nextZoom, - zoom: nextZoom - }, - { duration: options?.duration, ease: options?.ease, interpolate: options?.interpolate } - ); - - return Promise.resolve(true); - }, - fitView: (options?: FitViewOptions) => { - return store.fitView(options); - }, + setCenter: async (x, y, options) => store.setCenter(x, y, options), + fitView: (options?: FitViewOptions) => store.fitView(options), fitBounds: async (bounds: Rect, options?: FitBoundsOptions) => { if (!store.panZoom) { return Promise.resolve(false); diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index 657fdce2..a1f220bb 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -14,7 +14,8 @@ import { type ConnectionState, updateAbsolutePositions, snapPosition, - calculateNodePosition + calculateNodePosition, + type SetCenterOptions } from '@xyflow/system'; import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types'; @@ -126,6 +127,26 @@ export function createStore void; setPaneClickDistance: (distance: number) => void; fitView: (options?: FitViewOptions) => Promise; + setCenter: SetCenter; updateNodePositions: UpdateNodePositions; updateNodeInternals: (updates: Map) => void; unselectNodesAndEdges: (params?: { nodes?: NodeType[]; edges?: EdgeType[] }) => void; From 91b22482eb07877603bc9c8a1409b377e28d7e61 Mon Sep 17 00:00:00 2001 From: Moritz Klack Date: Thu, 5 Jun 2025 16:42:12 +0200 Subject: [PATCH 11/14] chore(changeset): update --- .changeset/thirty-snakes-float.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/thirty-snakes-float.md b/.changeset/thirty-snakes-float.md index dd729387..e22e0e41 100644 --- a/.changeset/thirty-snakes-float.md +++ b/.changeset/thirty-snakes-float.md @@ -3,4 +3,4 @@ "@xyflow/svelte": minor --- -feat(NodeWrapper): focus nodes in the viewport on tab +Focus nodes on tab if not within the viewport and add a new prop `enablePanOnFocus` From 19cd285458d7120aeb8776c1b8abb9a143d101e2 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 5 Jun 2025 19:37:14 +0200 Subject: [PATCH 12/14] chore(autoFocus): rename to autoPanOnNodeFocus --- examples/react/src/examples/A11y/index.tsx | 12 +++---- .../src/routes/examples/a11y/+page.svelte | 32 +++++++++---------- .../src/components/NodeWrapper/index.tsx | 4 +-- .../src/components/StoreUpdater/index.tsx | 2 +- .../src/container/NodeRenderer/index.tsx | 10 +++--- .../react/src/container/ReactFlow/index.tsx | 4 +-- packages/react/src/store/initialState.ts | 3 +- packages/react/src/types/component-props.ts | 2 +- packages/react/src/types/nodes.ts | 2 +- packages/react/src/types/store.ts | 2 +- .../components/NodeWrapper/NodeWrapper.svelte | 2 +- .../lib/container/SvelteFlow/Wrapper.svelte | 4 +-- .../src/lib/container/SvelteFlow/types.ts | 2 +- .../src/lib/store/initial-store.svelte.ts | 2 +- 14 files changed, 41 insertions(+), 42 deletions(-) diff --git a/examples/react/src/examples/A11y/index.tsx b/examples/react/src/examples/A11y/index.tsx index 344a20e4..389b275c 100644 --- a/examples/react/src/examples/A11y/index.tsx +++ b/examples/react/src/examples/A11y/index.tsx @@ -8,7 +8,6 @@ import { ReactFlowProvider, Node, Edge, - OnNodeDrag, AriaLabelConfig, Panel, } from '@xyflow/react'; @@ -73,12 +72,13 @@ const ariaLabelConfig: Partial = { }; const A11y = () => { - const [isFocusPannable, setEnablePanOnFocus] = useState(true); + const [autoPanOnNodeFocus, setAutoPanOnNodeFocus] = useState(true); + return ( { setEnablePanOnFocus(event.target.checked)} + checked={autoPanOnNodeFocus} + onChange={(event) => setAutoPanOnNodeFocus(event.target.checked)} className="xy-theme__checkbox" /> - enablePanOnFocus + autoPanOnNodeFocus
diff --git a/examples/svelte/src/routes/examples/a11y/+page.svelte b/examples/svelte/src/routes/examples/a11y/+page.svelte index 962cd726..aa7dc938 100644 --- a/examples/svelte/src/routes/examples/a11y/+page.svelte +++ b/examples/svelte/src/routes/examples/a11y/+page.svelte @@ -19,13 +19,19 @@ { id: 'A-C', source: 'A', target: 'C' }, { id: 'A-D', source: 'A', target: 'D' } ]); - let isFocusPannable = $state(true); - const ariaLabelConfig = $state( - { + let autoPanOnNodeFocus = $state(true); + const ariaLabelConfig = $state({ 'node.a11yDescription.default': 'Svelte Custom Node Desc.', 'node.a11yDescription.keyboardDisabled': 'Svelte Custom Keyboard Desc.', - 'node.a11yDescription.ariaLiveMessage': ({ direction, x, y }: { direction: string; x: number; y: number }) => - `Custom Moved selected node ${direction}. New position, x: ${x}, y: ${y}`, + 'node.a11yDescription.ariaLiveMessage': ({ + direction, + x, + y + }: { + direction: string; + x: number; + y: number; + }) => `Custom Moved selected node ${direction}. New position, x: ${x}, y: ${y}`, 'edge.a11yDescription.default': 'Svelte Custom Edge Desc.', 'controls.ariaLabel': 'Svelte Custom Control Aria Label', 'controls.zoomIn.ariaLabel': 'Svelte Custom Zoom in', @@ -33,30 +39,24 @@ // 'controls.fitView.ariaLabel': 'Svelte Custom Fit View', 'controls.interactive.ariaLabel': 'Svelte Custom Toggle Interactivity', 'minimap.ariaLabel': 'Svelte Custom Minimap' - } - ); + }); - + - +
-
+
diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 14cf3d54..6d3b17b0 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -29,7 +29,7 @@ export function NodeWrapper({ onContextMenu, onDoubleClick, nodesDraggable, - enablePanOnFocus, + autoPanOnNodeFocus, elementsSelectable, nodesConnectable, nodesFocusable, @@ -162,7 +162,7 @@ export function NodeWrapper({ }; const onFocus = () => { - if (disableKeyboardA11y || !enablePanOnFocus || !nodeRef.current?.matches(':focus-visible')) { + if (disableKeyboardA11y || !autoPanOnNodeFocus || !nodeRef.current?.matches(':focus-visible')) { return; } diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index ba0d66bf..5a5878c3 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -23,7 +23,7 @@ const reactFlowFieldsToTrack = [ 'onClickConnectStart', 'onClickConnectEnd', 'nodesDraggable', - 'enablePanOnFocus', + 'autoPanOnNodeFocus', 'nodesConnectable', 'nodesFocusable', 'edgesFocusable', diff --git a/packages/react/src/container/NodeRenderer/index.tsx b/packages/react/src/container/NodeRenderer/index.tsx index 093e3f95..22d7a54f 100644 --- a/packages/react/src/container/NodeRenderer/index.tsx +++ b/packages/react/src/container/NodeRenderer/index.tsx @@ -29,7 +29,7 @@ export type NodeRendererProps = Pick< const selector = (s: ReactFlowState) => ({ nodesDraggable: s.nodesDraggable, - enablePanOnFocus: s.enablePanOnFocus, + autoPanOnNodeFocus: s.autoPanOnNodeFocus, nodesConnectable: s.nodesConnectable, nodesFocusable: s.nodesFocusable, elementsSelectable: s.elementsSelectable, @@ -37,10 +37,8 @@ const selector = (s: ReactFlowState) => ({ }); function NodeRendererComponent(props: NodeRendererProps) { - const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, enablePanOnFocus, onError } = useStore( - selector, - shallow - ); + const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, autoPanOnNodeFocus, onError } = + useStore(selector, shallow); const nodeIds = useVisibleNodeIds(props.onlyRenderVisibleElements); const resizeObserver = useResizeObserver(); @@ -90,7 +88,7 @@ function NodeRendererComponent(props: NodeRendererProps( onlyRenderVisibleElements = false, selectNodesOnDrag, nodesDraggable, - enablePanOnFocus, + autoPanOnNodeFocus, nodesConnectable, nodesFocusable, nodeOrigin = defaultNodeOrigin, @@ -262,7 +262,7 @@ function ReactFlow( onClickConnectStart={onClickConnectStart} onClickConnectEnd={onClickConnectEnd} nodesDraggable={nodesDraggable} - enablePanOnFocus={enablePanOnFocus} + autoPanOnNodeFocus={autoPanOnNodeFocus} nodesConnectable={nodesConnectable} nodesFocusable={nodesFocusable} edgesFocusable={edgesFocusable} diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 77d10769..2532b5ad 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -112,7 +112,6 @@ const getInitialState = ({ snapToGrid: false, nodesDraggable: true, - enablePanOnFocus: false, nodesConnectable: true, nodesFocusable: true, edgesFocusable: true, @@ -135,7 +134,9 @@ const getInitialState = ({ ariaLiveMessage: '', autoPanOnConnect: true, autoPanOnNodeDrag: true, + autoPanOnNodeFocus: true, autoPanSpeed: 15, + connectionRadius: 20, onError: devWarn, isValidConnection: undefined, diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index f09995bf..7168e334 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -387,7 +387,7 @@ export interface ReactFlowProps = { nodesConnectable: boolean; elementsSelectable: boolean; nodesDraggable: boolean; - enablePanOnFocus: boolean; + autoPanOnNodeFocus: boolean; nodesFocusable: boolean; onClick?: NodeMouseHandler; onDoubleClick?: NodeMouseHandler; diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index fd9eb91e..d5e8902b 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -89,7 +89,7 @@ export type ReactFlowStore { if ( store.disableKeyboardA11y || - !store.enablePanOnFocus || + !store.autoPanOnNodeFocus || !nodeRef?.matches(':focus-visible') ) { return; diff --git a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte index bd42d658..206fdc8c 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte @@ -84,7 +84,7 @@ elevateNodesOnSelect, elevateEdgesOnSelect, nodesDraggable, - enablePanOnFocus, + autoPanOnNodeFocus, nodesConnectable, elementsSelectable, nodesFocusable, @@ -105,7 +105,7 @@ // Undo scroll events, preventing viewport from shifting when nodes outside of it are focused function wrapperOnScroll(e: UIEvent & { currentTarget: EventTarget & HTMLDivElement }) { e.currentTarget.scrollTo({ top: 0, left: 0, behavior: 'auto' }); - + // Forward the event to any existing onscroll handler if needed if (rest.onscroll) { rest.onscroll(e); diff --git a/packages/svelte/src/lib/container/SvelteFlow/types.ts b/packages/svelte/src/lib/container/SvelteFlow/types.ts index 0d67197c..b3dcb6cb 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/types.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/types.ts @@ -236,7 +236,7 @@ export type SvelteFlowProps< * When `true`, the viewport will pan when a node is focused. * @default false */ - enablePanOnFocus?: boolean; + autoPanOnNodeFocus?: boolean; /** * Controls if all nodes should be connectable to each other * @default true diff --git a/packages/svelte/src/lib/store/initial-store.svelte.ts b/packages/svelte/src/lib/store/initial-store.svelte.ts index bedb2e2d..90b251a6 100644 --- a/packages/svelte/src/lib/store/initial-store.svelte.ts +++ b/packages/svelte/src/lib/store/initial-store.svelte.ts @@ -247,7 +247,6 @@ export function getInitialStore Date: Thu, 5 Jun 2025 19:37:47 +0200 Subject: [PATCH 13/14] chore(changeset): update --- .changeset/thirty-snakes-float.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/thirty-snakes-float.md b/.changeset/thirty-snakes-float.md index e22e0e41..8b39c4f3 100644 --- a/.changeset/thirty-snakes-float.md +++ b/.changeset/thirty-snakes-float.md @@ -3,4 +3,4 @@ "@xyflow/svelte": minor --- -Focus nodes on tab if not within the viewport and add a new prop `enablePanOnFocus` +Focus nodes on tab if not within the viewport and add a new prop `autoPanOnNodeFocus` From 34274eb44d9c70bb7ac12f8d4901399770b0a1d3 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 10 Jun 2025 09:19:23 +0200 Subject: [PATCH 14/14] chore(autoPanOnNodeFocus): cleanup --- packages/react/src/components/NodeWrapper/index.tsx | 10 +++++++--- packages/react/src/container/NodeRenderer/index.tsx | 5 +---- packages/react/src/types/component-props.ts | 2 +- packages/react/src/types/nodes.ts | 1 - packages/svelte/src/lib/container/SvelteFlow/types.ts | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 6d3b17b0..46e1ee01 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -29,7 +29,6 @@ export function NodeWrapper({ onContextMenu, onDoubleClick, nodesDraggable, - autoPanOnNodeFocus, elementsSelectable, nodesConnectable, nodesFocusable, @@ -162,11 +161,16 @@ export function NodeWrapper({ }; const onFocus = () => { - if (disableKeyboardA11y || !autoPanOnNodeFocus || !nodeRef.current?.matches(':focus-visible')) { + if (disableKeyboardA11y || !nodeRef.current?.matches(':focus-visible')) { + return; + } + + const { transform, width, height, autoPanOnNodeFocus, setCenter } = store.getState(); + + if (!autoPanOnNodeFocus) { return; } - const { transform, width, height, setCenter } = store.getState(); const withinViewport = getNodesInside(new Map([[id, node]]), { x: 0, y: 0, width, height }, transform, true).length > 0; diff --git a/packages/react/src/container/NodeRenderer/index.tsx b/packages/react/src/container/NodeRenderer/index.tsx index 22d7a54f..0b055bdb 100644 --- a/packages/react/src/container/NodeRenderer/index.tsx +++ b/packages/react/src/container/NodeRenderer/index.tsx @@ -29,7 +29,6 @@ export type NodeRendererProps = Pick< const selector = (s: ReactFlowState) => ({ nodesDraggable: s.nodesDraggable, - autoPanOnNodeFocus: s.autoPanOnNodeFocus, nodesConnectable: s.nodesConnectable, nodesFocusable: s.nodesFocusable, elementsSelectable: s.elementsSelectable, @@ -37,8 +36,7 @@ const selector = (s: ReactFlowState) => ({ }); function NodeRendererComponent(props: NodeRendererProps) { - const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, autoPanOnNodeFocus, onError } = - useStore(selector, shallow); + const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, onError } = useStore(selector, shallow); const nodeIds = useVisibleNodeIds(props.onlyRenderVisibleElements); const resizeObserver = useResizeObserver(); @@ -88,7 +86,6 @@ function NodeRendererComponent(props: NodeRendererProps = { nodesConnectable: boolean; elementsSelectable: boolean; nodesDraggable: boolean; - autoPanOnNodeFocus: boolean; nodesFocusable: boolean; onClick?: NodeMouseHandler; onDoubleClick?: NodeMouseHandler; diff --git a/packages/svelte/src/lib/container/SvelteFlow/types.ts b/packages/svelte/src/lib/container/SvelteFlow/types.ts index b3dcb6cb..d575db1a 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/types.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/types.ts @@ -234,7 +234,7 @@ export type SvelteFlowProps< nodesDraggable?: boolean; /** * When `true`, the viewport will pan when a node is focused. - * @default false + * @default true */ autoPanOnNodeFocus?: boolean; /**