feat: create enablePanOnFocus prop, update onFocus func

This commit is contained in:
Abbey Yacoe
2025-06-05 12:22:06 +02:00
parent 343ed4364b
commit 6693c7300e
14 changed files with 125 additions and 59 deletions
@@ -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<NodeType extends Node>({
onContextMenu,
onDoubleClick,
nodesDraggable,
enablePanOnFocus,
elementsSelectable,
nodesConnectable,
nodesFocusable,
@@ -163,28 +165,26 @@ export function NodeWrapper<NodeType extends Node>({
};
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,
});
}
};
@@ -23,6 +23,7 @@ const reactFlowFieldsToTrack = [
'onClickConnectStart',
'onClickConnectEnd',
'nodesDraggable',
'enablePanOnFocus',
'nodesConnectable',
'nodesFocusable',
'edgesFocusable',
@@ -29,6 +29,7 @@ export type NodeRendererProps<NodeType extends Node> = 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<NodeType extends Node>(props: NodeRendererProps<NodeType>) {
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<NodeType extends Node>(props: NodeRendererProps<N
* The split of responsibilities between NodeRenderer and
* NodeComponentWrapper may appear weird. However, its designed to
* minimize the cost of updates when individual nodes change.
*
*
* For example, when youre dragging a single node, that node gets
* updated multiple times per second. If `NodeRenderer` were to update
* every time, it would have to re-run the `nodes.map()` loop every
* time. This gets pricey with hundreds of nodes, especially if every
* loop cycle does more than just rendering a JSX element!
*
*
* As a result of this choice, we took the following implementation
* decisions:
* - NodeRenderer subscribes *only* to node IDs and therefore
@@ -86,6 +90,7 @@ function NodeRendererComponent<NodeType extends Node>(props: NodeRendererProps<N
disableKeyboardA11y={props.disableKeyboardA11y}
resizeObserver={resizeObserver}
nodesDraggable={nodesDraggable}
enablePanOnFocus={enablePanOnFocus}
nodesConnectable={nodesConnectable}
nodesFocusable={nodesFocusable}
elementsSelectable={elementsSelectable}
@@ -77,6 +77,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
onlyRenderVisibleElements = false,
selectNodesOnDrag,
nodesDraggable,
enablePanOnFocus,
nodesConnectable,
nodesFocusable,
nodeOrigin = defaultNodeOrigin,
@@ -261,6 +262,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
onClickConnectStart={onClickConnectStart}
onClickConnectEnd={onClickConnectEnd}
nodesDraggable={nodesDraggable}
enablePanOnFocus={enablePanOnFocus}
nodesConnectable={nodesConnectable}
nodesFocusable={nodesFocusable}
edgesFocusable={edgesFocusable}
+1
View File
@@ -112,6 +112,7 @@ const getInitialState = ({
snapToGrid: false,
nodesDraggable: true,
enablePanOnFocus: false,
nodesConnectable: true,
nodesFocusable: true,
edgesFocusable: true,
@@ -383,6 +383,11 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* @default true
*/
nodesDraggable?: boolean;
/**
* When `true`, the viewport will pan when a node is focused.
* @default false
*/
enablePanOnFocus?: boolean;
/**
* Controls whether all nodes should be connectable or not. Individual nodes can override this
* setting by setting their `connectable` prop.
+1
View File
@@ -48,6 +48,7 @@ export type NodeWrapperProps<NodeType extends Node> = {
nodesConnectable: boolean;
elementsSelectable: boolean;
nodesDraggable: boolean;
enablePanOnFocus: boolean;
nodesFocusable: boolean;
onClick?: NodeMouseHandler<NodeType>;
onDoubleClick?: NodeMouseHandler<NodeType>;
+1
View File
@@ -88,6 +88,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
snapGrid: SnapGrid;
nodesDraggable: boolean;
enablePanOnFocus: boolean;
nodesConnectable: boolean;
nodesFocusable: boolean;
edgesFocusable: boolean;