Merge branch 'main' into feat/nodes-html-attributes
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
getNodeDimensions,
|
||||
isInputDOMNode,
|
||||
nodeHasDimensions,
|
||||
getNodesInside,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||
@@ -158,6 +159,28 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onFocus = () => {
|
||||
if (disableKeyboardA11y || !nodeRef.current?.matches(':focus-visible')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { transform, width, height, autoPanOnNodeFocus, setCenter } = store.getState();
|
||||
|
||||
if (!autoPanOnNodeFocus) {
|
||||
return;
|
||||
}
|
||||
|
||||
const withinViewport =
|
||||
getNodesInside(new Map([[id, node]]), { x: 0, y: 0, width, height }, transform, true).length > 0;
|
||||
|
||||
if (!withinViewport) {
|
||||
setCenter(node.position.x + nodeDimensions.width / 2, node.position.y + nodeDimensions.height / 2, {
|
||||
zoom: transform[2],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cc([
|
||||
@@ -195,6 +218,7 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
onDoubleClick={onDoubleClickHandler}
|
||||
onKeyDown={isFocusable ? onKeyDown : undefined}
|
||||
tabIndex={isFocusable ? 0 : undefined}
|
||||
onFocus={isFocusable ? onFocus : undefined}
|
||||
role={node.ariaRole ?? (isFocusable ? 'group' : undefined)}
|
||||
aria-roledescription="node"
|
||||
aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`}
|
||||
|
||||
@@ -23,6 +23,7 @@ const reactFlowFieldsToTrack = [
|
||||
'onClickConnectStart',
|
||||
'onClickConnectEnd',
|
||||
'nodesDraggable',
|
||||
'autoPanOnNodeFocus',
|
||||
'nodesConnectable',
|
||||
'nodesFocusable',
|
||||
'edgesFocusable',
|
||||
|
||||
@@ -48,13 +48,13 @@ function NodeRendererComponent<NodeType extends Node>(props: NodeRendererProps<N
|
||||
* The split of responsibilities between NodeRenderer and
|
||||
* NodeComponentWrapper may appear weird. However, it’s designed to
|
||||
* minimize the cost of updates when individual nodes change.
|
||||
*
|
||||
*
|
||||
* For example, when you’re 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
|
||||
|
||||
@@ -77,6 +77,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
||||
onlyRenderVisibleElements = false,
|
||||
selectNodesOnDrag,
|
||||
nodesDraggable,
|
||||
autoPanOnNodeFocus,
|
||||
nodesConnectable,
|
||||
nodesFocusable,
|
||||
nodeOrigin = defaultNodeOrigin,
|
||||
@@ -261,6 +262,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
||||
onClickConnectStart={onClickConnectStart}
|
||||
onClickConnectEnd={onClickConnectEnd}
|
||||
nodesDraggable={nodesDraggable}
|
||||
autoPanOnNodeFocus={autoPanOnNodeFocus}
|
||||
nodesConnectable={nodesConnectable}
|
||||
nodesFocusable={nodesFocusable}
|
||||
edgesFocusable={edgesFocusable}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -134,7 +134,9 @@ const getInitialState = ({
|
||||
ariaLiveMessage: '',
|
||||
autoPanOnConnect: true,
|
||||
autoPanOnNodeDrag: true,
|
||||
autoPanOnNodeFocus: true,
|
||||
autoPanSpeed: 15,
|
||||
|
||||
connectionRadius: 20,
|
||||
onError: devWarn,
|
||||
isValidConnection: undefined,
|
||||
|
||||
@@ -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 true
|
||||
*/
|
||||
autoPanOnNodeFocus?: boolean;
|
||||
/**
|
||||
* Controls whether all nodes should be connectable or not. Individual nodes can override this
|
||||
* setting by setting their `connectable` prop.
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
type EdgeChange,
|
||||
type ParentLookup,
|
||||
type AriaLabelConfig,
|
||||
SetCenter,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type {
|
||||
@@ -88,6 +89,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
|
||||
snapGrid: SnapGrid;
|
||||
|
||||
nodesDraggable: boolean;
|
||||
autoPanOnNodeFocus: boolean;
|
||||
nodesConnectable: boolean;
|
||||
nodesFocusable: boolean;
|
||||
edgesFocusable: boolean;
|
||||
@@ -171,6 +173,7 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
|
||||
triggerNodeChanges: (changes: NodeChange<NodeType>[]) => void;
|
||||
triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void;
|
||||
panBy: PanBy;
|
||||
setCenter: SetCenter;
|
||||
setPaneClickDistance: (distance: number) => void;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user