diff --git a/.changeset/brown-apples-deliver.md b/.changeset/brown-apples-deliver.md new file mode 100644 index 00000000..cf1754ad --- /dev/null +++ b/.changeset/brown-apples-deliver.md @@ -0,0 +1,7 @@ +--- +'@xyflow/react': patch +'@xyflow/svelte': patch +'@xyflow/system': patch +--- + +Add more TSDocs to components, hooks, utils funcs and types diff --git a/examples/react/src/examples/UseOnSelectionChange/index.tsx b/examples/react/src/examples/UseOnSelectionChange/index.tsx index b9795951..fdbe124a 100644 --- a/examples/react/src/examples/UseOnSelectionChange/index.tsx +++ b/examples/react/src/examples/UseOnSelectionChange/index.tsx @@ -10,7 +10,6 @@ import { useEdgesState, useOnSelectionChange, OnSelectionChangeParams, - OnSelectionChangeFunc, } from '@xyflow/react'; const initialNodes: Node[] = [ diff --git a/packages/react/src/additional-components/Background/Background.tsx b/packages/react/src/additional-components/Background/Background.tsx index e4470f10..ee89624d 100644 --- a/packages/react/src/additional-components/Background/Background.tsx +++ b/packages/react/src/additional-components/Background/Background.tsx @@ -90,4 +90,57 @@ function BackgroundComponent({ BackgroundComponent.displayName = 'Background'; +/** + * The `` component makes it convenient to render different types of backgrounds common in node-based UIs. It comes with three variants: lines, dots and cross. + * + * @example + * + * A simple example of how to use the Background component. + * + * ```tsx + * import { useState } from 'react'; + * import { ReactFlow, Background, BackgroundVariant } from '@xyflow/react'; + * + * export default function Flow() { + * return ( + * + * + * + * ); + * } + * ``` + * + * @example + * + * In this example you can see how to combine multiple backgrounds + * + * ```tsx + * import { ReactFlow, Background, BackgroundVariant } from '@xyflow/react'; + * import '@xyflow/react/dist/style.css'; + * + * export default function Flow() { + * return ( + * + * + * + * + * ); + * } + * ``` + * + * @remarks + * + * When combining multiple components it’s important to give each of them a unique id prop! + * + */ export const Background = memo(BackgroundComponent); diff --git a/packages/react/src/additional-components/Background/types.ts b/packages/react/src/additional-components/Background/types.ts index 0a035aae..50219e2d 100644 --- a/packages/react/src/additional-components/Background/types.ts +++ b/packages/react/src/additional-components/Background/types.ts @@ -1,11 +1,20 @@ import { CSSProperties } from 'react'; +/** + * The three variants are exported as an enum for convenience. You can either import + * the enum and use it like `BackgroundVariant.Lines` or you can use the raw string + * value directly. + * @public + */ export enum BackgroundVariant { Lines = 'lines', Dots = 'dots', Cross = 'cross', } +/** + * @expand + */ export type BackgroundProps = { id?: string; /** Color of the pattern */ @@ -24,7 +33,8 @@ export type BackgroundProps = { offset?: number | [number, number]; /** Line width of the Line pattern */ lineWidth?: number; - /** Variant of the pattern + /** + * Variant of the pattern * @example BackgroundVariant.Lines, BackgroundVariant.Dots, BackgroundVariant.Cross * 'lines', 'dots', 'cross' */ diff --git a/packages/react/src/additional-components/Controls/ControlButton.tsx b/packages/react/src/additional-components/Controls/ControlButton.tsx index 744f46d4..a3cc81ad 100644 --- a/packages/react/src/additional-components/Controls/ControlButton.tsx +++ b/packages/react/src/additional-components/Controls/ControlButton.tsx @@ -2,6 +2,29 @@ import cc from 'classcat'; import type { ControlButtonProps } from './types'; +/** + * You can add buttons to the control panel by using the `` component + * and pass it as a child to the [``](/api-reference/components/controls) component. + * + * @public + * @example + *```jsx + *import { MagicWand } from '@radix-ui/react-icons' + *import { ReactFlow, Controls, ControlButton } from '@xyflow/react' + * + *export default function Flow() { + * return ( + * + * + * alert('Something magical just happened. ✨')}> + * + * + * + * + * ) + *} + *``` + */ export function ControlButton({ children, className, ...rest }: ControlButtonProps) { return ( + * + * + * + * + *
+ * {data.label} + *
+ * + * + * + * + * ); + *}; + * + *export default memo(CustomNode); + *``` + * @remarks By default, the toolbar is only visible when a node is selected. If multiple + * nodes are selected it will not be visible to prevent overlapping toolbars or + * clutter. You can override this behavior by setting the `isVisible` prop to `true`. + */ export function NodeToolbar({ nodeId, children, @@ -74,7 +109,7 @@ export function NodeToolbar({ const isActive = typeof isVisible === 'boolean' ? isVisible - : nodes.size === 1 && nodes.values().next().value.selected && selectedNodesCount === 1; + : nodes.size === 1 && nodes.values().next().value?.selected && selectedNodesCount === 1; if (!isActive || !nodes.size) { return null; diff --git a/packages/react/src/additional-components/NodeToolbar/types.ts b/packages/react/src/additional-components/NodeToolbar/types.ts index 4de679bd..91a717da 100644 --- a/packages/react/src/additional-components/NodeToolbar/types.ts +++ b/packages/react/src/additional-components/NodeToolbar/types.ts @@ -1,19 +1,24 @@ import type { HTMLAttributes } from 'react'; import type { Position, Align } from '@xyflow/system'; +/** + * @expand + */ export type NodeToolbarProps = HTMLAttributes & { /** Id of the node, or array of ids the toolbar should be displayed at */ nodeId?: string | string[]; /** If true, node toolbar is visible even if node is not selected */ isVisible?: boolean; - /** Position of the toolbar relative to the node + /** + * Position of the toolbar relative to the node * @example Position.TopLeft, Position.TopRight, * Position.BottomLeft, Position.BottomRight */ position?: Position; /** Offset the toolbar from the node */ offset?: number; - /** Align the toolbar relative to the node + /** + * Align the toolbar relative to the node * @example Align.Start, Align.Center, Align.End */ align?: Align; diff --git a/packages/react/src/components/BatchProvider/index.tsx b/packages/react/src/components/BatchProvider/index.tsx index 1dbab718..5a5d1ed3 100644 --- a/packages/react/src/components/BatchProvider/index.tsx +++ b/packages/react/src/components/BatchProvider/index.tsx @@ -30,9 +30,11 @@ export function BatchProvider[]) => { const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState(); - // This is essentially an `Array.reduce` in imperative clothing. Processing - // this queue is a relatively hot path so we'd like to avoid the overhead of - // array methods where we can. + /* + * This is essentially an `Array.reduce` in imperative clothing. Processing + * this queue is a relatively hot path so we'd like to avoid the overhead of + * array methods where we can. + */ let next = nodes as NodeType[]; for (const payload of queueItems) { next = typeof payload === 'function' ? payload(next) : payload; diff --git a/packages/react/src/components/BatchProvider/useQueue.ts b/packages/react/src/components/BatchProvider/useQueue.ts index c33208d7..b1f9f793 100644 --- a/packages/react/src/components/BatchProvider/useQueue.ts +++ b/packages/react/src/components/BatchProvider/useQueue.ts @@ -12,21 +12,27 @@ import { Queue, QueueItem } from './types'; * @returns a Queue object */ export function useQueue(runQueue: (items: QueueItem[]) => void) { - // Because we're using a ref above, we need some way to let React know when to - // actually process the queue. We increment this number any time we mutate the - // queue, creating a new state to trigger the layout effect below. - // Using a boolean dirty flag here instead would lead to issues related to - // automatic batching. (https://github.com/xyflow/xyflow/issues/4779) + /* + * Because we're using a ref above, we need some way to let React know when to + * actually process the queue. We increment this number any time we mutate the + * queue, creating a new state to trigger the layout effect below. + * Using a boolean dirty flag here instead would lead to issues related to + * automatic batching. (https://github.com/xyflow/xyflow/issues/4779) + */ const [serial, setSerial] = useState(BigInt(0)); - // A reference of all the batched updates to process before the next render. We - // want a reference here so multiple synchronous calls to `setNodes` etc can be - // batched together. + /* + * A reference of all the batched updates to process before the next render. We + * want a reference here so multiple synchronous calls to `setNodes` etc can be + * batched together. + */ const [queue] = useState(() => createQueue(() => setSerial(n => n + BigInt(1)))); - // Layout effects are guaranteed to run before the next render which means we - // shouldn't run into any issues with stale state or weird issues that come from - // rendering things one frame later than expected (we used to use `setTimeout`). + /* + * Layout effects are guaranteed to run before the next render which means we + * shouldn't run into any issues with stale state or weird issues that come from + * rendering things one frame later than expected (we used to use `setTimeout`). + */ useIsomorphicLayoutEffect(() => { const queueItems = queue.get(); diff --git a/packages/react/src/components/EdgeLabelRenderer/index.tsx b/packages/react/src/components/EdgeLabelRenderer/index.tsx index c66b70ae..647811cb 100644 --- a/packages/react/src/components/EdgeLabelRenderer/index.tsx +++ b/packages/react/src/components/EdgeLabelRenderer/index.tsx @@ -6,6 +6,46 @@ import type { ReactFlowState } from '../../types'; const selector = (s: ReactFlowState) => s.domNode?.querySelector('.react-flow__edgelabel-renderer'); +/** + * Edges are SVG-based. If you want to render more complex labels you can use the + * `` component to access a div based renderer. This component + * is a portal that renders the label in a `
` that is positioned on top of + * the edges. You can see an example usage of the component in the [edge label renderer](/examples/edges/edge-label-renderer) example. + * @public + * + * @example + *```jsx + *import React from 'react'; + *import { getBezierPath, EdgeLabelRenderer, BaseEdge } from '@xyflow/react'; + * + *export function CustomEdge({ id, data, ...props }) { + * const [edgePath, labelX, labelY] = getBezierPath(props); + * + * return ( + * <> + * + * + *
+ * {data.label} + *
+ *
+ * + * ); + *}; + *``` + * + * @remarks The `` has no pointer events by default. If you want to + * add mouse interactions you need to set the style `pointerEvents: all` and add + * the `nopan` class on the label or the element you want to interact with. + */ export function EdgeLabelRenderer({ children }: { children: ReactNode }) { const edgeLabelRenderer = useStore(selector); diff --git a/packages/react/src/components/EdgeWrapper/index.tsx b/packages/react/src/components/EdgeWrapper/index.tsx index f50ed4b9..2d3754fd 100644 --- a/packages/react/src/components/EdgeWrapper/index.tsx +++ b/packages/react/src/components/EdgeWrapper/index.tsx @@ -136,28 +136,28 @@ export function EdgeWrapper({ const onEdgeDoubleClick = onDoubleClick ? (event: React.MouseEvent) => { - onDoubleClick(event, { ...edge }); - } + onDoubleClick(event, { ...edge }); + } : undefined; const onEdgeContextMenu = onContextMenu ? (event: React.MouseEvent) => { - onContextMenu(event, { ...edge }); - } + onContextMenu(event, { ...edge }); + } : undefined; const onEdgeMouseEnter = onMouseEnter ? (event: React.MouseEvent) => { - onMouseEnter(event, { ...edge }); - } + onMouseEnter(event, { ...edge }); + } : undefined; const onEdgeMouseMove = onMouseMove ? (event: React.MouseEvent) => { - onMouseMove(event, { ...edge }); - } + onMouseMove(event, { ...edge }); + } : undefined; const onEdgeMouseLeave = onMouseLeave ? (event: React.MouseEvent) => { - onMouseLeave(event, { ...edge }); - } + onMouseLeave(event, { ...edge }); + } : undefined; const onKeyDown = (event: KeyboardEvent) => { diff --git a/packages/react/src/components/Edges/BaseEdge.tsx b/packages/react/src/components/Edges/BaseEdge.tsx index 3d951796..4d1ac1da 100644 --- a/packages/react/src/components/Edges/BaseEdge.tsx +++ b/packages/react/src/components/Edges/BaseEdge.tsx @@ -4,6 +4,33 @@ import cc from 'classcat'; import { EdgeText } from './EdgeText'; import type { BaseEdgeProps } from '../../types'; +/** + * The `` component gets used internally for all the edges. It can be + * used inside a custom edge and handles the invisible helper edge and the edge label + * for you. + * + * @public + * @example + * ```jsx + *import { BaseEdge } from '@xyflow/react'; + * + *export function CustomEdge({ sourceX, sourceY, targetX, targetY, ...props }) { + * const [edgePath] = getStraightPath({ + * sourceX, + * sourceY, + * targetX, + * targetY, + * }); + * + * return ; + *} + *``` + * + * @remarks If you want to use an edge marker with the [``](/api-reference/components/base-edge) component, + * you can pass the `markerStart` or `markerEnd` props passed to your custom edge + * through to the [``](/api-reference/components/base-edge) component. + * You can see all the props passed to a custom edge by looking at the [`EdgeProps`](/api-reference/types/edge-props) type. + */ export function BaseEdge({ path, labelX, diff --git a/packages/react/src/components/Edges/EdgeText.tsx b/packages/react/src/components/Edges/EdgeText.tsx index 2e3ad041..2c79f718 100644 --- a/packages/react/src/components/Edges/EdgeText.tsx +++ b/packages/react/src/components/Edges/EdgeText.tsx @@ -73,4 +73,30 @@ function EdgeTextComponent({ EdgeTextComponent.displayName = 'EdgeText'; +/** + * You can use the `` component as a helper component to display text + * within your custom edges. + * + *@public + * + *@example + *```jsx + *import { EdgeText } from '@xyflow/react'; + * + *export function CustomEdgeLabel({ label }) { + * return ( + * + * ); + *} + *``` + */ export const EdgeText = memo(EdgeTextComponent); diff --git a/packages/react/src/components/Edges/SimpleBezierEdge.tsx b/packages/react/src/components/Edges/SimpleBezierEdge.tsx index b3378c48..a11cbc1d 100644 --- a/packages/react/src/components/Edges/SimpleBezierEdge.tsx +++ b/packages/react/src/components/Edges/SimpleBezierEdge.tsx @@ -29,6 +29,11 @@ function getControl({ pos, x1, y1, x2, y2 }: GetControlParams): [number, number] return [x1, 0.5 * (y1 + y2)]; } +/** + * The `getSimpleBezierPath` util returns everything you need to render a simple + * bezier edge between two nodes. + * @public + */ export function getSimpleBezierPath({ sourceX, sourceY, diff --git a/packages/react/src/components/Edges/index.ts b/packages/react/src/components/Edges/index.ts index 21af9667..88b274eb 100644 --- a/packages/react/src/components/Edges/index.ts +++ b/packages/react/src/components/Edges/index.ts @@ -1,6 +1,8 @@ -// We distinguish between internal and exported edges -// The internal edges are used directly like custom edges and always get an id, source and target props -// If you import an edge from the library, the id is optional and source and target are not used at all +/* + * We distinguish between internal and exported edges + * The internal edges are used directly like custom edges and always get an id, source and target props + * If you import an edge from the library, the id is optional and source and target are not used at all + */ export { SimpleBezierEdge, SimpleBezierEdgeInternal } from './SimpleBezierEdge'; export { SmoothStepEdge, SmoothStepEdgeInternal } from './SmoothStepEdge'; diff --git a/packages/react/src/components/Handle/index.tsx b/packages/react/src/components/Handle/index.tsx index cc86e189..df7b428b 100644 --- a/packages/react/src/components/Handle/index.tsx +++ b/packages/react/src/components/Handle/index.tsx @@ -28,10 +28,14 @@ import { useNodeId } from '../../contexts/NodeIdContext'; import { type ReactFlowState } from '../../types'; import { fixedForwardRef } from '../../utils'; -export interface HandleProps extends HandlePropsSystem, Omit, 'id'> { - /** Callback called when connection is made */ - onConnect?: OnConnect; -} +/** + * @expand + */ +export type HandleProps = HandlePropsSystem & + Omit, 'id'> & { + /** Callback called when connection is made */ + onConnect?: OnConnect; + }; const selector = (s: ReactFlowState) => ({ connectOnClick: s.connectOnClick, @@ -228,8 +232,10 @@ function HandleComponent( connectingfrom: connectingFrom, connectingto: connectingTo, valid, - // shows where you can start a connection from - // and where you can end it while connecting + /* + * shows where you can start a connection from + * and where you can end it while connecting + */ connectionindicator: isConnectable && (!connectionInProcess || isPossibleEndHandle) && @@ -248,6 +254,28 @@ function HandleComponent( } /** - * The Handle component is a UI element that is used to connect nodes. + * The `` component is used in your [custom nodes](/learn/customization/custom-nodes) + * to define connection points. + * + *@public + * + *@example + * + *```jsx + *import { Handle, Position } from '@xyflow/react'; + * + *export function CustomNode({ data }) { + * return ( + * <> + *
+ * {data.label} + *
+ * + * + * + * + * ); + *}; + *``` */ export const Handle = memo(fixedForwardRef(HandleComponent)); diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index bb136161..5c0ce08b 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -108,8 +108,10 @@ export function NodeWrapper({ const { selectNodesOnDrag, nodeDragThreshold } = store.getState(); if (isSelectable && (!selectNodesOnDrag || !isDraggable || nodeDragThreshold > 0)) { - // this handler gets called by XYDrag on drag start when selectNodesOnDrag=true - // here we only need to call it when selectNodesOnDrag=false + /* + * this handler gets called by XYDrag on drag start when selectNodesOnDrag=true + * here we only need to call it when selectNodesOnDrag=false + */ handleNodeClick({ id, store, diff --git a/packages/react/src/components/NodeWrapper/useNodeObserver.ts b/packages/react/src/components/NodeWrapper/useNodeObserver.ts index 2c2910fd..de1d31e7 100644 --- a/packages/react/src/components/NodeWrapper/useNodeObserver.ts +++ b/packages/react/src/components/NodeWrapper/useNodeObserver.ts @@ -49,8 +49,10 @@ export function useNodeObserver({ useEffect(() => { if (nodeRef.current) { - // when the user programmatically changes the source or handle position, we need to update the internals - // to make sure the edges are updated correctly + /* + * when the user programmatically changes the source or handle position, we need to update the internals + * to make sure the edges are updated correctly + */ const typeChanged = prevType.current !== nodeType; const sourcePosChanged = prevSourcePosition.current !== node.sourcePosition; const targetPosChanged = prevTargetPosition.current !== node.targetPosition; diff --git a/packages/react/src/components/NodeWrapper/utils.tsx b/packages/react/src/components/NodeWrapper/utils.tsx index c8c7d5c7..8c699c64 100644 --- a/packages/react/src/components/NodeWrapper/utils.tsx +++ b/packages/react/src/components/NodeWrapper/utils.tsx @@ -23,9 +23,9 @@ export const builtinNodeTypes: NodeTypes = { export function getNodeInlineStyleDimensions( node: InternalNode ): { - width: number | string | undefined; - height: number | string | undefined; -} { + width: number | string | undefined; + height: number | string | undefined; + } { if (node.internals.handleBounds === undefined) { return { width: node.width ?? node.initialWidth ?? node.style?.width, diff --git a/packages/react/src/components/Nodes/utils.ts b/packages/react/src/components/Nodes/utils.ts index 81939bbc..146e05f8 100644 --- a/packages/react/src/components/Nodes/utils.ts +++ b/packages/react/src/components/Nodes/utils.ts @@ -4,10 +4,12 @@ import { errorMessages } from '@xyflow/system'; import type { ReactFlowState } from '../../types'; -// this handler is called by -// 1. the click handler when node is not draggable or selectNodesOnDrag = false -// or -// 2. the on drag start handler when node is draggable and selectNodesOnDrag = true +/* + * this handler is called by + * 1. the click handler when node is not draggable or selectNodesOnDrag = false + * or + * 2. the on drag start handler when node is draggable and selectNodesOnDrag = true + */ export function handleNodeClick({ id, store, diff --git a/packages/react/src/components/NodesSelection/index.tsx b/packages/react/src/components/NodesSelection/index.tsx index ecaef17b..ae587b56 100644 --- a/packages/react/src/components/NodesSelection/index.tsx +++ b/packages/react/src/components/NodesSelection/index.tsx @@ -61,9 +61,9 @@ export function NodesSelection({ const onContextMenu = onSelectionContextMenu ? (event: MouseEvent) => { - const selectedNodes = store.getState().nodes.filter((n) => n.selected); - onSelectionContextMenu(event, selectedNodes); - } + const selectedNodes = store.getState().nodes.filter((n) => n.selected); + onSelectionContextMenu(event, selectedNodes); + } : undefined; const onKeyDown = (event: KeyboardEvent) => { diff --git a/packages/react/src/components/Panel/index.tsx b/packages/react/src/components/Panel/index.tsx index e6dd9edb..8d854b91 100644 --- a/packages/react/src/components/Panel/index.tsx +++ b/packages/react/src/components/Panel/index.tsx @@ -5,9 +5,12 @@ import type { PanelPosition } from '@xyflow/system'; import { useStore } from '../../hooks/useStore'; import type { ReactFlowState } from '../../types'; +/** + * @expand + */ export type PanelProps = HTMLAttributes & { - /** Set position of the panel - * @example 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' + /** + * The position of the panel */ position?: PanelPosition; children: ReactNode; @@ -15,6 +18,31 @@ export type PanelProps = HTMLAttributes & { const selector = (s: ReactFlowState) => (s.userSelectionActive ? 'none' : 'all'); +/** + * The `` component helps you position content above the viewport. + * It is used internally by the [``](/api-reference/components/minimap) + * and [``](/api-reference/components/controls) components. + * + * @public + * + * @example + * ```jsx + *import { ReactFlow, Background, Panel } from '@xyflow/react'; + * + *export default function Flow() { + * return ( + * + * top-left + * top-center + * top-right + * bottom-left + * bottom-center + * bottom-right + * + * ); + *} + *``` + */ export const Panel = forwardRef( ({ position = 'top-left', children, className, style, ...rest }, ref) => { const pointerEvents = useStore(selector); @@ -33,4 +61,4 @@ export const Panel = forwardRef( } ); -Panel.displayName = 'Panel' +Panel.displayName = 'Panel'; diff --git a/packages/react/src/components/ReactFlowProvider/index.tsx b/packages/react/src/components/ReactFlowProvider/index.tsx index b29f5f17..1492ca3f 100644 --- a/packages/react/src/components/ReactFlowProvider/index.tsx +++ b/packages/react/src/components/ReactFlowProvider/index.tsx @@ -19,6 +19,40 @@ export type ReactFlowProviderProps = { children: ReactNode; }; +/** + * The `` component is a [context provider](https://react.dev/learn/passing-data-deeply-with-context#) + * that makes it possible to access a flow's internal state outside of the + * [``](/api-reference/react-flow) component. Many of the hooks we + * provide rely on this component to work. + * @public + * + * @example + * ```tsx + *import { ReactFlow, ReactFlowProvider, useNodes } from '@xyflow/react' + * + *export default function Flow() { + * return ( + * + * + * + * + * ); + *} + * + *function Sidebar() { + * // This hook will only work if the component it's used in is a child of a + * // . + * const nodes = useNodes() + * + * return ; + *} + *``` + * + * @remarks If you're using a router and want your flow's state to persist across routes, + * it's vital that you place the `` component _outside_ of + * your router. If you have multiple flows on the same page you will need to use a separate + * `` for each flow. + */ export function ReactFlowProvider({ initialNodes: nodes, initialEdges: edges, diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index cac8df0e..e3a61ab2 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -94,9 +94,11 @@ const selector = (s: ReactFlowState) => ({ }); const initPrevValues = { - // these are values that are also passed directly to other components - // than the StoreUpdater. We can reduce the number of setStore calls - // by setting the same values here as prev fields. + /* + * these are values that are also passed directly to other components + * than the StoreUpdater. We can reduce the number of setStore calls + * by setting the same values here as prev fields. + */ translateExtent: infiniteExtent, nodeOrigin: defaultNodeOrigin, minZoom: 0.5, diff --git a/packages/react/src/components/ViewportPortal/index.tsx b/packages/react/src/components/ViewportPortal/index.tsx index c1b9b406..1040d796 100644 --- a/packages/react/src/components/ViewportPortal/index.tsx +++ b/packages/react/src/components/ViewportPortal/index.tsx @@ -6,6 +6,31 @@ import type { ReactFlowState } from '../../types'; const selector = (s: ReactFlowState) => s.domNode?.querySelector('.react-flow__viewport-portal'); +/** + * The `` component can be used to add components to the same viewport + * of the flow where nodes and edges are rendered. This is useful when you want to render + * your own components that are adhere to the same coordinate system as the nodes & edges + * and are also affected by zooming and panning + * @public + * @example + * + * ```jsx + *import React from 'react'; + *import { ViewportPortal } from '@xyflow/react'; + * + *export default function () { + * return ( + * + *
+ * This div is positioned at [100, 100] on the flow. + *
+ *
+ * ); + *} + *``` + */ export function ViewportPortal({ children }: { children: ReactNode }) { const viewPortalDiv = useStore(selector); diff --git a/packages/react/src/container/EdgeRenderer/MarkerDefinitions.tsx b/packages/react/src/container/EdgeRenderer/MarkerDefinitions.tsx index 5b6194c5..17b5950c 100644 --- a/packages/react/src/container/EdgeRenderer/MarkerDefinitions.tsx +++ b/packages/react/src/container/EdgeRenderer/MarkerDefinitions.tsx @@ -42,9 +42,11 @@ const Marker = ({ ); }; -// when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore -// when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper -// that we can then use for creating our unique marker ids +/* + * when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore + * when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper + * that we can then use for creating our unique marker ids + */ const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => { const edges = useStore((s) => s.edges); const defaultEdgeOptions = useStore((s) => s.defaultEdgeOptions); diff --git a/packages/react/src/container/NodeRenderer/index.tsx b/packages/react/src/container/NodeRenderer/index.tsx index 7b0bd131..02126d73 100644 --- a/packages/react/src/container/NodeRenderer/index.tsx +++ b/packages/react/src/container/NodeRenderer/index.tsx @@ -44,29 +44,31 @@ function NodeRendererComponent(props: NodeRendererProps {nodeIds.map((nodeId) => { return ( - // 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 - // rerender *only* when visible nodes are added or removed. - // - NodeRenderer performs all operations the result of which can be - // shared between nodes (such as creating the `ResizeObserver` - // instance, or subscribing to `selector`). This means extra prop - // drilling into `NodeComponentWrapper`, but it means we need to run - // these operations only once – instead of once per node. - // - Any operations that you’d normally write inside `nodes.map` are - // moved into `NodeComponentWrapper`. This ensures they are - // memorized – so if `NodeRenderer` *has* to rerender, it only - // needs to regenerate the list of nodes, nothing else. + /* + * 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 + * rerender *only* when visible nodes are added or removed. + * - NodeRenderer performs all operations the result of which can be + * shared between nodes (such as creating the `ResizeObserver` + * instance, or subscribing to `selector`). This means extra prop + * drilling into `NodeComponentWrapper`, but it means we need to run + * these operations only once – instead of once per node. + * - Any operations that you’d normally write inside `nodes.map` are + * moved into `NodeComponentWrapper`. This ensures they are + * memorized – so if `NodeRenderer` *has* to rerender, it only + * needs to regenerate the list of nodes, nothing else. + */ key={nodeId} id={nodeId} diff --git a/packages/react/src/container/Pane/index.tsx b/packages/react/src/container/Pane/index.tsx index aeadde03..91459539 100644 --- a/packages/react/src/container/Pane/index.tsx +++ b/packages/react/src/container/Pane/index.tsx @@ -233,8 +233,10 @@ export function Pane({ (event.target as Partial)?.releasePointerCapture?.(event.pointerId); const { userSelectionRect } = store.getState(); - // We only want to trigger click functions when in selection mode if - // the user did not move the mouse. + /* + * We only want to trigger click functions when in selection mode if + * the user did not move the mouse. + */ if (!userSelectionActive && userSelectionRect && event.target === container.current) { onClick?.(event); } @@ -246,8 +248,10 @@ export function Pane({ }); onSelectionEnd?.(event); - // 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 the user kept holding the selectionKey during the selection, + * we need to reset the selectionInProgress, so the next click event is not prevented + */ if (selectionKeyPressed || selectionOnDrag) { selectionInProgress.current = false; } diff --git a/packages/react/src/container/ReactFlow/Wrapper.tsx b/packages/react/src/container/ReactFlow/Wrapper.tsx index 5856809a..71a84316 100644 --- a/packages/react/src/container/ReactFlow/Wrapper.tsx +++ b/packages/react/src/container/ReactFlow/Wrapper.tsx @@ -31,8 +31,10 @@ export function Wrapper({ const isWrapped = useContext(StoreContext); if (isWrapped) { - // we need to wrap it with a fragment because it's not allowed for children to be a ReactNode - // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 + /* + * we need to wrap it with a fragment because it's not allowed for children to be a ReactNode + * https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 + */ return <>{children}; } diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index cf250628..cfc4f40c 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -312,4 +312,24 @@ function ReactFlow( ); } +/** + * The `` component is the heart of your React Flow application. + * It renders your nodes and edges and handles user interaction + * + * @public + * + * @example + * ```tsx + *import { ReactFlow } from '@xyflow/react' + * + *export default function Flow() { + * return (); + *} + *``` + */ export default fixedForwardRef(ReactFlow); diff --git a/packages/react/src/contexts/NodeIdContext.ts b/packages/react/src/contexts/NodeIdContext.ts index 28a14b78..5458c98c 100644 --- a/packages/react/src/contexts/NodeIdContext.ts +++ b/packages/react/src/contexts/NodeIdContext.ts @@ -4,6 +4,34 @@ export const NodeIdContext = createContext(null); export const Provider = NodeIdContext.Provider; export const Consumer = NodeIdContext.Consumer; +/** + * You can use this hook to get the id of the node it is used inside. It is useful + * if you need the node's id deeper in the render tree but don't want to manually + * drill down the id as a prop. + * + * @public + * @returns id of the node + * + * @example + *```jsx + *import { useNodeId } from '@xyflow/react'; + * + *export default function CustomNode() { + * return ( + *
+ * This node has an id of + * + *
+ * ); + *} + * + *function NodeIdDisplay() { + * const nodeId = useNodeId(); + * + * return {nodeId}; + *} + *``` + */ export const useNodeId = (): string | null => { const nodeId = useContext(NodeIdContext); return nodeId; diff --git a/packages/react/src/hooks/useConnection.ts b/packages/react/src/hooks/useConnection.ts index 94db5140..9ed14b67 100644 --- a/packages/react/src/hooks/useConnection.ts +++ b/packages/react/src/hooks/useConnection.ts @@ -24,9 +24,28 @@ function getSelector {connection ? `Someone is trying to make a connection from ${connection.fromNode} to this one.` : 'There are currently no incoming connections!'} + * + *
+ * ); + * } + * ``` + * * @returns ConnectionState */ export function useConnection>>( diff --git a/packages/react/src/hooks/useEdges.ts b/packages/react/src/hooks/useEdges.ts index 3ed9de39..45263849 100644 --- a/packages/react/src/hooks/useEdges.ts +++ b/packages/react/src/hooks/useEdges.ts @@ -6,10 +6,22 @@ import type { Edge, ReactFlowState } from '../types'; const edgesSelector = (state: ReactFlowState) => state.edges; /** - * Hook for getting the current edges from the store. + * This hook returns an array of the current edges. Components that use this hook + * will re-render **whenever any edge changes**. * * @public * @returns An array of edges + * + * @example + * ```tsx + *import { useEdges } from '@xyflow/react'; + * + *export default function () { + * const edges = useEdges(); + * + * return
There are currently {edges.length} edges!
; + *} + *``` */ export function useEdges(): EdgeType[] { const edges = useStore(edgesSelector, shallow) as EdgeType[]; diff --git a/packages/react/src/hooks/useInternalNode.ts b/packages/react/src/hooks/useInternalNode.ts index d16d697b..ecd83897 100644 --- a/packages/react/src/hooks/useInternalNode.ts +++ b/packages/react/src/hooks/useInternalNode.ts @@ -5,11 +5,31 @@ import { useStore } from './useStore'; import type { InternalNode, Node } from '../types'; /** - * Hook for getting an internal node by id + * This hook returns the internal representation of a specific node. + * Components that use this hook will re-render **whenever the node changes**, + * including when a node is selected or moved. * * @public * @param id - id of the node * @returns array with visible node ids + * + * @example + * ```tsx + *import { useInternalNode } from '@xyflow/react'; + * + *export default function () { + * const internalNode = useInternalNode('node-1'); + * const absolutePosition = internalNode.internals.positionAbsolute; + * + * return ( + *
+ * The absolute position of the node is at: + *

x: {absolutePosition.x}

+ *

y: {absolutePosition.y}

+ *
+ * ); + *} + *``` */ export function useInternalNode(id: string): InternalNode | undefined { const node = useStore( diff --git a/packages/react/src/hooks/useKeyPress.ts b/packages/react/src/hooks/useKeyPress.ts index 8bce0564..00cd012b 100644 --- a/packages/react/src/hooks/useKeyPress.ts +++ b/packages/react/src/hooks/useKeyPress.ts @@ -13,18 +13,38 @@ export type UseKeyPressOptions = { const defaultDoc = typeof document !== 'undefined' ? document : null; /** - * Hook for handling key events. + * This hook lets you listen for specific key codes and tells you whether they are + * currently pressed or not. * * @public * @param param.keyCode - The key code (string or array of strings) to use * @param param.options - Options * @returns boolean + * + * @example + * ```tsx + *import { useKeyPress } from '@xyflow/react'; + * + *export default function () { + * const spacePressed = useKeyPress('Space'); + * const cmdAndSPressed = useKeyPress(['Meta+s', 'Strg+s']); + * + * return ( + *
+ * {spacePressed &&

Space pressed!

} + * {cmdAndSPressed &&

Cmd + S pressed!

} + *
+ * ); + *} + *``` */ export function useKeyPress( - // the keycode can be a string 'a' or an array of strings ['a', 'a+d'] - // a string means a single key 'a' or a combination when '+' is used 'a+d' - // an array means different possibilites. Explainer: ['a', 'd+s'] here the - // user can use the single key 'a' or the combination 'd' + 's' + /* + * the keycode can be a string 'a' or an array of strings ['a', 'a+d'] + * a string means a single key 'a' or a combination when '+' is used 'a+d' + * an array means different possibilites. Explainer: ['a', 'd+s'] here the + * user can use the single key 'a' or the combination 'd' + 's' + */ keyCode: KeyCode | null = null, options: UseKeyPressOptions = { target: defaultDoc, actInsideInputWithModifier: true } ): boolean { @@ -36,20 +56,24 @@ export function useKeyPress( // we need to remember the pressed keys in order to support combinations const pressedKeys = useRef(new Set([])); - // keyCodes = array with single keys [['a']] or key combinations [['a', 's']] - // keysToWatch = array with all keys flattened ['a', 'd', 'ShiftLeft'] - // used to check if we store event.code or event.key. When the code is in the list of keysToWatch - // we use the code otherwise the key. Explainer: When you press the left "command" key, the code is "MetaLeft" - // and the key is "Meta". We want users to be able to pass keys and codes so we assume that the key is meant when - // we can't find it in the list of keysToWatch. + /* + * keyCodes = array with single keys [['a']] or key combinations [['a', 's']] + * keysToWatch = array with all keys flattened ['a', 'd', 'ShiftLeft'] + * used to check if we store event.code or event.key. When the code is in the list of keysToWatch + * we use the code otherwise the key. Explainer: When you press the left "command" key, the code is "MetaLeft" + * and the key is "Meta". We want users to be able to pass keys and codes so we assume that the key is meant when + * we can't find it in the list of keysToWatch. + */ const [keyCodes, keysToWatch] = useMemo<[Array, Keys]>(() => { if (keyCode !== null) { const keyCodeArr = Array.isArray(keyCode) ? keyCode : [keyCode]; const keys = keyCodeArr .filter((kc) => typeof kc === 'string') - // we first replace all '+' with '\n' which we will use to split the keys on - // then we replace '\n\n' with '\n+', this way we can also support the combination 'key++' - // in the end we simply split on '\n' to get the key array + /* + * we first replace all '+' with '\n' which we will use to split the keys on + * then we replace '\n\n' with '\n+', this way we can also support the combination 'key++' + * in the end we simply split on '\n' to get the key array + */ .map((kc) => kc.replace('+', '\n').replace('\n\n', '\n+').split('\n')); const keysFlat = keys.reduce((res: Keys, item) => res.concat(...item), []); @@ -133,12 +157,16 @@ export function useKeyPress( function isMatchingKey(keyCodes: Array, pressedKeys: PressedKeys, isUp: boolean): boolean { return ( keyCodes - // we only want to compare same sizes of keyCode definitions - // and pressed keys. When the user specified 'Meta' as a key somewhere - // this would also be truthy without this filter when user presses 'Meta' + 'r' + /* + * we only want to compare same sizes of keyCode definitions + * and pressed keys. When the user specified 'Meta' as a key somewhere + * this would also be truthy without this filter when user presses 'Meta' + 'r' + */ .filter((keys) => isUp || keys.length === pressedKeys.size) - // since we want to support multiple possibilities only one of the - // combinations need to be part of the pressed keys + /* + * since we want to support multiple possibilities only one of the + * combinations need to be part of the pressed keys + */ .some((keys) => keys.every((k) => pressedKeys.has(k))) ); } diff --git a/packages/react/src/hooks/useMoveSelectedNodes.ts b/packages/react/src/hooks/useMoveSelectedNodes.ts index 52503c04..0b2dacc9 100644 --- a/packages/react/src/hooks/useMoveSelectedNodes.ts +++ b/packages/react/src/hooks/useMoveSelectedNodes.ts @@ -22,8 +22,10 @@ export function useMoveSelectedNodes() { const nodeUpdates = new Map(); const isSelected = selectedAndDraggable(nodesDraggable); - // by default a node moves 5px on each key press - // if snap grid is enabled, we use that for the velocity + /* + * by default a node moves 5px on each key press + * if snap grid is enabled, we use that for the velocity + */ const xVelo = snapToGrid ? snapGrid[0] : 5; const yVelo = snapToGrid ? snapGrid[1] : 5; diff --git a/packages/react/src/hooks/useNodeConnections.ts b/packages/react/src/hooks/useNodeConnections.ts index c83a7366..60ba894c 100644 --- a/packages/react/src/hooks/useNodeConnections.ts +++ b/packages/react/src/hooks/useNodeConnections.ts @@ -22,7 +22,7 @@ type UseNodeConnectionsParams = { }; /** - * Hook to retrieve all edges connected to a node. Can be filtered by handle type and id. + * This hook returns an array of connections on a specific node, handle type ('source', 'target') or handle ID. * * @public * @param param.id - node id - optional if called inside a custom node @@ -31,6 +31,22 @@ type UseNodeConnectionsParams = { * @param param.onConnect - gets called when a connection is established * @param param.onDisconnect - gets called when a connection is removed * @returns an array with connections + * + * @example + * ```jsx + *import { useNodeConnections } from '@xyflow/react'; + * + *export default function () { + * const connections = useNodeConnections({ + * type: 'target', + * handleId: 'my-handle', + * }); + * + * return ( + *
There are currently {connections.length} incoming connections!
+ * ); + *} + *``` */ export function useNodeConnections({ id, diff --git a/packages/react/src/hooks/useNodes.ts b/packages/react/src/hooks/useNodes.ts index 5e558e90..9fa14bcb 100644 --- a/packages/react/src/hooks/useNodes.ts +++ b/packages/react/src/hooks/useNodes.ts @@ -6,10 +6,23 @@ import type { Node, ReactFlowState } from '../types'; const nodesSelector = (state: ReactFlowState) => state.nodes; /** - * Hook for getting the current nodes from the store. + * This hook returns an array of the current nodes. Components that use this hook + * will re-render **whenever any node changes**, including when a node is selected + * or moved. * * @public * @returns An array of nodes + * + * @example + * ```jsx + *import { useNodes } from '@xyflow/react'; + * + *export default function() { + * const nodes = useNodes(); + * + * return
There are currently {nodes.length} nodes!
; + *} + *``` */ export function useNodes(): NodeType[] { const nodes = useStore(nodesSelector, shallow) as NodeType[]; diff --git a/packages/react/src/hooks/useNodesData.ts b/packages/react/src/hooks/useNodesData.ts index 7472cbf7..1d2aa37e 100644 --- a/packages/react/src/hooks/useNodesData.ts +++ b/packages/react/src/hooks/useNodesData.ts @@ -5,12 +5,24 @@ import { useStore } from '../hooks/useStore'; import type { Node } from '../types'; /** - * Hook for receiving data of one or multiple nodes + * This hook lets you subscribe to changes of a specific nodes `data` object. * * @public * @param nodeId - The id (or ids) of the node to get the data from - * @param guard - Optional guard function to narrow down the node type * @returns An object (or array of object) with {id, type, data} representing each node + * + * @example + * + *```jsx + *import { useNodesData } from '@xyflow/react'; + * + *export default function() { + * const nodeData = useNodesData('nodeId-1'); + * const nodesData = useNodesData(['nodeId-1', 'nodeId-2']); + * + * return null; + *} + *``` */ export function useNodesData( nodeId: string diff --git a/packages/react/src/hooks/useNodesEdgesState.ts b/packages/react/src/hooks/useNodesEdgesState.ts index fae87c02..b5919e5e 100644 --- a/packages/react/src/hooks/useNodesEdgesState.ts +++ b/packages/react/src/hooks/useNodesEdgesState.ts @@ -4,11 +4,41 @@ import { applyNodeChanges, applyEdgeChanges } from '../utils/changes'; import type { Node, Edge, OnNodesChange, OnEdgesChange } from '../types'; /** - * Hook for managing the state of nodes - should only be used for prototyping / simple use cases. + * This hook makes it easy to prototype a controlled flow where you manage the + * state of nodes and edges outside the `ReactFlowInstance`. You can think of it + * like React's `useState` hook with an additional helper callback. * * @public * @param initialNodes * @returns an array [nodes, setNodes, onNodesChange] + * @example + * + *```tsx + *import { ReactFlow, useNodesState, useEdgesState } from '@xyflow/react'; + * + *const initialNodes = []; + *const initialEdges = []; + * + *export default function () { + * const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + * const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + * + * return ( + * + * ); + *} + *``` + * + * @remarks This hook was created to make prototyping easier and our documentation + * examples clearer. Although it is OK to use this hook in production, in + * practice you may want to use a more sophisticated state management solution + * like Zustand {@link https://reactflow.dev/docs/guides/state-management/} instead. + * */ export function useNodesState( initialNodes: NodeType[] @@ -23,11 +53,41 @@ export function useNodesState( } /** - * Hook for managing the state of edges - should only be used for prototyping / simple use cases. + * This hook makes it easy to prototype a controlled flow where you manage the + * state of nodes and edges outside the `ReactFlowInstance`. You can think of it + * like React's `useState` hook with an additional helper callback. * * @public * @param initialEdges * @returns an array [edges, setEdges, onEdgesChange] + * @example + * + *```tsx + *import { ReactFlow, useNodesState, useEdgesState } from '@xyflow/react'; + * + *const initialNodes = []; + *const initialEdges = []; + * + *export default function () { + * const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + * const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + * + * return ( + * + * ); + *} + *``` + * + * @remarks This hook was created to make prototyping easier and our documentation + * examples clearer. Although it is OK to use this hook in production, in + * practice you may want to use a more sophisticated state management solution + * like Zustand {@link https://reactflow.dev/docs/guides/state-management/} instead. + * */ export function useEdgesState( initialEdges: EdgeType[] diff --git a/packages/react/src/hooks/useNodesInitialized.ts b/packages/react/src/hooks/useNodesInitialized.ts index 98bfa6b7..38f15a60 100644 --- a/packages/react/src/hooks/useNodesInitialized.ts +++ b/packages/react/src/hooks/useNodesInitialized.ts @@ -1,6 +1,7 @@ +import { nodeHasDimensions } from '@xyflow/system'; + import { useStore } from './useStore'; import type { ReactFlowState } from '../types'; -import { nodeHasDimensions } from '@xyflow/system'; export type UseNodesInitializedOptions = { includeHiddenNodes?: boolean; @@ -22,18 +23,44 @@ const selector = (options: UseNodesInitializedOptions) => (s: ReactFlowState) => return true; }; -const defaultOptions = { - includeHiddenNodes: false, -}; - /** - * Hook which returns true when all nodes are initialized. + * This hook tells you whether all the nodes in a flow have been measured and given + *a width and height. When you add a node to the flow, this hook will return + *`false` and then `true` again once the node has been measured. * * @public * @param options.includeHiddenNodes - defaults to false * @returns boolean indicating whether all nodes are initialized + * + * @example + * ```jsx + *import { useReactFlow, useNodesInitialized } from '@xyflow/react'; + *import { useEffect, useState } from 'react'; + * + *const options = { + * includeHiddenNodes: false, + *}; + * + *export default function useLayout() { + * const { getNodes } = useReactFlow(); + * const nodesInitialized = useNodesInitialized(options); + * const [layoutedNodes, setLayoutedNodes] = useState(getNodes()); + * + * useEffect(() => { + * if (nodesInitialized) { + * setLayoutedNodes(yourLayoutingFunction(getNodes())); + * } + * }, [nodesInitialized]); + * + * return layoutedNodes; + *} + *``` */ -export function useNodesInitialized(options: UseNodesInitializedOptions = defaultOptions): boolean { +export function useNodesInitialized( + options: UseNodesInitializedOptions = { + includeHiddenNodes: false, + } +): boolean { const initialized = useStore(selector(options)); return initialized; diff --git a/packages/react/src/hooks/useOnSelectionChange.ts b/packages/react/src/hooks/useOnSelectionChange.ts index 8e08191b..65cad6fd 100644 --- a/packages/react/src/hooks/useOnSelectionChange.ts +++ b/packages/react/src/hooks/useOnSelectionChange.ts @@ -8,10 +8,42 @@ export type UseOnSelectionChangeOptions = { }; /** - * Hook for registering an onSelectionChange handler. + * This hook lets you listen for changes to both node and edge selection. As the + *name implies, the callback you provide will be called whenever the selection of + *_either_ nodes or edges changes. * * @public * @param params.onChange - The handler to register + * + * @example + * ```jsx + *import { useState } from 'react'; + *import { ReactFlow, useOnSelectionChange } from '@xyflow/react'; + * + *function SelectionDisplay() { + * const [selectedNodes, setSelectedNodes] = useState([]); + * const [selectedEdges, setSelectedEdges] = useState([]); + * + * // the passed handler has to be memoized, otherwise the hook will not work correctly + * const onChange = useCallback(({ nodes, edges }) => { + * setSelectedNodes(nodes.map((node) => node.id)); + * setSelectedEdges(edges.map((edge) => edge.id)); + * }, []); + * + * useOnSelectionChange({ + * onChange, + * }); + * + * return ( + *
+ *

Selected nodes: {selectedNodes.join(', ')}

+ *

Selected edges: {selectedEdges.join(', ')}

+ *
+ * ); + *} + *``` + * + * @remarks You need to memoize the passed `onChange` handler, otherwise the hook will not work correctly. */ export function useOnSelectionChange({ onChange }: UseOnSelectionChangeOptions) { const store = useStoreApi(); diff --git a/packages/react/src/hooks/useOnViewportChange.ts b/packages/react/src/hooks/useOnViewportChange.ts index 61e4d64f..c82eb344 100644 --- a/packages/react/src/hooks/useOnViewportChange.ts +++ b/packages/react/src/hooks/useOnViewportChange.ts @@ -10,12 +10,30 @@ export type UseOnViewportChangeOptions = { }; /** - * Hook for registering an onViewportChange handler. + * The `useOnViewportChange` hook lets you listen for changes to the viewport such + *as panning and zooming. You can provide a callback for each phase of a viewport + *change: `onStart`, `onChange`, and `onEnd`. * * @public * @param params.onStart - gets called when the viewport starts changing * @param params.onChange - gets called when the viewport changes * @param params.onEnd - gets called when the viewport stops changing + * + * @example + * ```jsx + *import { useCallback } from 'react'; + *import { useOnViewportChange } from '@xyflow/react'; + * + *function ViewportChangeLogger() { + * useOnViewportChange({ + * onStart: (viewport: Viewport) => console.log('start', viewport), + * onChange: (viewport: Viewport) => console.log('change', viewport), + * onEnd: (viewport: Viewport) => console.log('end', viewport), + * }); + * + * return null; + *} + *``` */ export function useOnViewportChange({ onStart, onChange, onEnd }: UseOnViewportChangeOptions) { const store = useStoreApi(); diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 3d857cfa..57b2e001 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -20,10 +20,33 @@ import type { ReactFlowInstance, Node, Edge, InternalNode, ReactFlowState, Gener const selector = (s: ReactFlowState) => !!s.panZoom; /** - * Hook for accessing the ReactFlow instance. + * This hook returns a ReactFlowInstance that can be used to update nodes and edges, manipulate the viewport, or query the current state of the flow. * * @public * @returns ReactFlowInstance + * + * @example + * ```jsx + *import { useCallback, useState } from 'react'; + *import { useReactFlow } from '@xyflow/react'; + * + *export function NodeCounter() { + * const reactFlow = useReactFlow(); + * const [count, setCount] = useState(0); + * const countNodes = useCallback(() => { + * setCount(reactFlow.getNodes().length); + * // you need to pass it as a dependency if you are using it with useEffect or useCallback + * // because at the first render, it's not initialized yet and some functions might not work. + * }, [reactFlow]); + * + * return ( + *
+ * + *

There are {count} nodes in the flow.

+ *
+ * ); + *} + *``` */ export function useReactFlow(): ReactFlowInstance< NodeType, diff --git a/packages/react/src/hooks/useStore.ts b/packages/react/src/hooks/useStore.ts index 88c1abad..3b0a2778 100644 --- a/packages/react/src/hooks/useStore.ts +++ b/packages/react/src/hooks/useStore.ts @@ -9,7 +9,9 @@ import type { Edge, Node, ReactFlowState } from '../types'; const zustandErrorMessage = errorMessages['error001'](); /** - * Hook for accessing the internal store. Should only be used in rare cases. + * This hook can be used to subscribe to internal state changes of the React Flow + * component. The `useStore` hook is re-exported from the [Zustand](https://github.com/pmndrs/zustand) + * state management library, so you should check out their docs for more details. * * @public * @param selector @@ -17,8 +19,13 @@ const zustandErrorMessage = errorMessages['error001'](); * @returns The selected state slice * * @example - * const nodes = useStore((state: ReactFlowState) => state.nodes); + * ```ts + * const nodes = useStore((state) => state.nodes); + * ``` * + * @remarks This hook should only be used if there is no other way to access the internal + * state. For many of the common use cases, there are dedicated hooks available + * such as {@link useReactFlow}, {@link useViewport}, etc. */ function useStore( selector: (state: ReactFlowState) => StateSlice, @@ -33,6 +40,20 @@ function useStore( return useZustandStore(store, selector, equalityFn); } +/** + * In some cases, you might need to access the store directly. This hook returns the store object which can be used on demand to access the state or dispatch actions. + * + * @returns The store object + * + * @example + * ```ts + * const store = useStoreApi(); + * ``` + * + * @remarks This hook should only be used if there is no other way to access the internal + * state. For many of the common use cases, there are dedicated hooks available + * such as {@link useReactFlow}, {@link useViewport}, etc. + */ function useStoreApi() { const store = useContext(StoreContext) as UseBoundStoreWithEqualityFn< StoreApi> diff --git a/packages/react/src/hooks/useUpdateNodeInternals.ts b/packages/react/src/hooks/useUpdateNodeInternals.ts index 6486afd8..4463ed2a 100644 --- a/packages/react/src/hooks/useUpdateNodeInternals.ts +++ b/packages/react/src/hooks/useUpdateNodeInternals.ts @@ -4,10 +4,48 @@ import type { UpdateNodeInternals, InternalNodeUpdate } from '@xyflow/system'; import { useStoreApi } from '../hooks/useStore'; /** - * Hook for updating node internals. + * When you programmatically add or remove handles to a node or update a node's + *handle position, you need to let React Flow know about it using this hook. This + *will update the internal dimensions of the node and properly reposition handles + *on the canvas if necessary. * * @public * @returns function for updating node internals + * + * @example + * ```jsx + *import { useCallback, useState } from 'react'; + *import { Handle, useUpdateNodeInternals } from '@xyflow/react'; + * + *export default function RandomHandleNode({ id }) { + * const updateNodeInternals = useUpdateNodeInternals(); + * const [handleCount, setHandleCount] = useState(0); + * const randomizeHandleCount = useCallback(() => { + * setHandleCount(Math.floor(Math.random() * 10)); + * updateNodeInternals(id); + * }, [id, updateNodeInternals]); + * + * return ( + * <> + * {Array.from({ length: handleCount }).map((_, index) => ( + * + * ))} + * + *
+ * + *

There are {handleCount} handles on this node.

+ *
+ * + * ); + *} + *``` + * @remarks This hook can only be used in a component that is a child of a + *{@link ReactFlowProvider} or a {@link ReactFlow} component. */ export function useUpdateNodeInternals(): UpdateNodeInternals { const store = useStoreApi(); diff --git a/packages/react/src/hooks/useViewport.ts b/packages/react/src/hooks/useViewport.ts index 0b9758c1..a892e5a8 100644 --- a/packages/react/src/hooks/useViewport.ts +++ b/packages/react/src/hooks/useViewport.ts @@ -11,10 +11,33 @@ const viewportSelector = (state: ReactFlowState) => ({ }); /** - * Hook for getting the current viewport from the store. + * The `useViewport` hook is a convenient way to read the current state of the + *{@link Viewport} in a component. Components that use this hook + *will re-render **whenever the viewport changes**. * * @public * @returns The current viewport + * + * @example + * + *```jsx + *import { useViewport } from '@xyflow/react'; + * + *export default function ViewportDisplay() { + * const { x, y, zoom } = useViewport(); + * + * return ( + *
+ *

+ * The viewport is currently at ({x}, {y}) and zoomed to {zoom}. + *

+ *
+ * ); + *} + *``` + * + * @remarks This hook can only be used in a component that is a child of a + *{@link ReactFlowProvider} or a {@link ReactFlow} component. */ export function useViewport(): Viewport { const viewport = useStore(viewportSelector, shallow); diff --git a/packages/react/src/hooks/useVisibleNodeIds.ts b/packages/react/src/hooks/useVisibleNodeIds.ts index e8d14c8e..a0006940 100644 --- a/packages/react/src/hooks/useVisibleNodeIds.ts +++ b/packages/react/src/hooks/useVisibleNodeIds.ts @@ -8,8 +8,8 @@ import type { Node, ReactFlowState } from '../types'; const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => { return onlyRenderVisible ? getNodesInside(s.nodeLookup, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map( - (node) => node.id - ) + (node) => node.id + ) : Array.from(s.nodeLookup.keys()); }; diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 2466baa5..0f07d0f1 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -47,12 +47,14 @@ const createStore = ({ ...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, nodeExtent, defaultNodes, defaultEdges }), setNodes: (nodes: Node[]) => { const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect } = get(); - // setNodes() is called exclusively in response to user actions: - // - either when the `` prop is updated in the controlled ReactFlow setup, - // - or when the user calls something like `reactFlowInstance.setNodes()` in an uncontrolled ReactFlow setup. - // - // When this happens, we take the note objects passed by the user and extend them with fields - // relevant for internal React Flow operations. + /* + * setNodes() is called exclusively in response to user actions: + * - either when the `` prop is updated in the controlled ReactFlow setup, + * - or when the user calls something like `reactFlowInstance.setNodes()` in an uncontrolled ReactFlow setup. + * + * When this happens, we take the note objects passed by the user and extend them with fields + * relevant for internal React Flow operations. + */ adoptUserNodes(nodes, nodeLookup, parentLookup, { nodeOrigin, nodeExtent, @@ -81,9 +83,11 @@ const createStore = ({ set({ hasDefaultEdges: true }); } }, - // Every node gets registerd at a ResizeObserver. Whenever a node - // changes its dimensions, this function is called to measure the - // new dimensions and update the nodes. + /* + * Every node gets registerd at a ResizeObserver. Whenever a node + * changes its dimensions, this function is called to measure the + * new dimensions and update the nodes. + */ updateNodeInternals: (updates, params = { triggerFitView: true }) => { const { triggerNodeChanges, @@ -125,11 +129,13 @@ const createStore = ({ }); } - // here we are cirmumventing the onNodesChange handler - // in order to be able to display nodes even if the user - // has not provided an onNodesChange handler. - // Nodes are only rendered if they have a width and height - // attribute which they get from this handler. + /* + * here we are cirmumventing the onNodesChange handler + * in order to be able to display nodes even if the user + * has not provided an onNodesChange handler. + * Nodes are only rendered if they have a width and height + * attribute which they get from this handler. + */ set({ fitViewDone: nextFitViewDone }); } else { // we always want to trigger useStore calls whenever updateNodeInternals is called @@ -155,9 +161,9 @@ const createStore = ({ type: 'position', position: expandParent ? { - x: Math.max(0, dragItem.position.x), - y: Math.max(0, dragItem.position.y), - } + x: Math.max(0, dragItem.position.x), + y: Math.max(0, dragItem.position.y), + } : dragItem.position, dragging, }; @@ -248,8 +254,10 @@ const createStore = ({ const nodeChanges = nodesToUnselect.map((n) => { const internalNode = nodeLookup.get(n.id); if (internalNode) { - // we need to unselect the internal node that was selected previously before we - // send the change to the user to prevent it to be selected while dragging the new node + /* + * we need to unselect the internal node that was selected previously before we + * send the change to the user to prevent it to be selected while dragging the new node + */ internalNode.selected = false; } @@ -342,8 +350,10 @@ const createStore = ({ options ); }, - // we can't call an asnychronous function in updateNodeInternals - // for that we created this sync version of fitView + /* + * we can't call an asnychronous function in updateNodeInternals + * for that we created this sync version of fitView + */ fitViewSync: (options?: FitViewOptions): boolean => { const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get(); diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index a5807687..b7c414f1 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -52,7 +52,8 @@ import type { */ export interface ReactFlowProps extends Omit, 'onError'> { - /** An array of nodes to render in a controlled flow. + /** + * An array of nodes to render in a controlled flow. * @example * const nodes = [ * { @@ -64,7 +65,8 @@ export interface ReactFlowProps; onReconnectStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void; onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void; - /** This event handler is called when a Node is updated + /** + * This event handler is called when a Node is updated * @example // Use NodesState hook to create edges and get onNodesChange handler * import ReactFlow, { useNodesState } from '@xyflow/react'; * const [edges, setNodes, onNodesChange] = useNodesState(initialNodes); @@ -149,7 +153,8 @@ export interface ReactFlowProps) */ onNodesChange?: OnNodesChange; - /** This event handler is called when a Edge is updated + /** + * This event handler is called when a Edge is updated * @example // Use EdgesState hook to create edges and get onEdgesChange handler * import ReactFlow, { useEdgesState } from '@xyflow/react'; * const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); @@ -181,7 +186,8 @@ export interface ReactFlowProps void; onSelectionEnd?: (event: ReactMouseEvent) => void; onSelectionContextMenu?: (event: ReactMouseEvent, nodes: NodeType[]) => void; - /** When a connection line is completed and two nodes are connected by the user, this event fires with the new connection. + /** + * When a connection line is completed and two nodes are connected by the user, this event fires with the new connection. * * You can use the addEdge utility to convert the connection to a complete edge. * @example // Use helper function to update edges onConnect @@ -223,17 +229,20 @@ export interface ReactFlowProps void; /** This event handler gets called when mouse leaves the pane */ onPaneMouseLeave?: (event: ReactMouseEvent) => void; - /** Distance that the mouse can move between mousedown/up that will trigger a click + /** + * Distance that the mouse can move between mousedown/up that will trigger a click * @default 0 */ paneClickDistance?: number; - /** Distance that the mouse can move between mousedown/up that will trigger a click + /** + * Distance that the mouse can move between mousedown/up that will trigger a click * @default 0 */ nodeClickDistance?: number; /** This handler gets called before the user deletes nodes or edges and provides a way to abort the deletion by returning false. */ onBeforeDelete?: OnBeforeDelete; - /** Custom node types to be available in a flow. + /** + * Custom node types to be available in a flow. * * React Flow matches a node's type to a component in the nodeTypes object. * @example @@ -242,7 +251,8 @@ export interface ReactFlowProps; /** Styles to be applied to the container of the connection line */ connectionLineContainerStyle?: CSSProperties; - /** 'strict' connection mode will only allow you to connect source handles to target handles. + /** + * 'strict' connection mode will only allow you to connect source handles to target handles. * * 'loose' connection mode will allow you to connect handles of any type to one another. * @default 'strict' */ connectionMode?: ConnectionMode; - /** Pressing down this key deletes all selected nodes & edges. + /** + * Pressing down this key deletes all selected nodes & edges. * @default 'Backspace' */ deleteKeyCode?: KeyCode | null; - /** If a key is set, you can pan the viewport while that key is held down even if panOnScroll is set to false. + /** + * If a key is set, you can pan the viewport while that key is held down even if panOnScroll is set to false. * * By setting this prop to null you can disable this functionality. * @default 'Space' @@ -280,91 +294,110 @@ export interface ReactFlowProps void; - /** By default the viewport extends infinitely. You can use this prop to set a boundary. + /** + * By default the viewport extends infinitely. You can use this prop to set a boundary. * * The first pair of coordinates is the top left boundary and the second pair is the bottom right. * @example [[-1000, -10000], [1000, 1000]] */ translateExtent?: CoordinateExtent; - /** Disabling this prop will allow the user to scroll the page even when their pointer is over the flow. + /** + * Disabling this prop will allow the user to scroll the page even when their pointer is over the flow. * @default true */ preventScrolling?: boolean; - /** By default nodes can be placed on an infinite flow. You can use this prop to set a boundary. + /** + * By default nodes can be placed on an infinite flow. You can use this prop to set a boundary. * * The first pair of coordinates is the top left boundary and the second pair is the bottom right. * @example [[-1000, -10000], [1000, 1000]] */ nodeExtent?: CoordinateExtent; - /** Color of edge markers + /** + * Color of edge markers * @example "#b1b1b7" */ defaultMarkerColor?: string; @@ -402,17 +439,20 @@ export interface ReactFlowProps true */ isValidConnection?: IsValidConnection; - /** With a threshold greater than zero you can control the distinction between node drag and click events. + /** + * With a threshold greater than zero you can control the distinction between node drag and click events. * * If threshold equals 1, you need to drag the node 1 pixel before a drag event is fired. * @default 1 @@ -510,12 +563,14 @@ export interface ReactFlowProps = { disableKeyboardA11y?: boolean; }; +/** + * Many properties on an [`Edge`](/api-reference/types/edge) are optional. When a new edge is created, + * the properties that are not provided will be filled in with the default values + * passed to the `defaultEdgeOptions` prop of the [``](/api-reference/react-flow#defaultedgeoptions) component. + */ export type DefaultEdgeOptions = DefaultEdgeOptionsBase; export type EdgeTextProps = SVGAttributes & @@ -100,8 +109,10 @@ export type EdgeTextProps = SVGAttributes & }; /** - * Custom edge component props + * When you implement a custom edge it is wrapped in a component that enables some + * basic functionality. The `EdgeProps` type is the props that are passed to this. * @public + * @expand */ export type EdgeProps = Pick< EdgeType, @@ -121,6 +132,7 @@ export type EdgeProps = Pick< /** * BaseEdge component props * @public + * @expand */ export type BaseEdgeProps = Omit, 'd'> & EdgeLabelOptions & { @@ -137,6 +149,7 @@ export type BaseEdgeProps = Omit, 'd'> & /** * Helper type for edge components that get exported by the library * @public + * @expand */ export type EdgeComponentProps = EdgePosition & EdgeLabelOptions & { @@ -156,35 +169,47 @@ export type EdgeComponentWithPathOptions = EdgeComponentProps & { /** * BezierEdge component props * @public + * @expand */ export type BezierEdgeProps = EdgeComponentWithPathOptions; /** * SmoothStepEdge component props * @public + * @expand */ export type SmoothStepEdgeProps = EdgeComponentWithPathOptions; /** * StepEdge component props * @public + * @expand */ export type StepEdgeProps = EdgeComponentWithPathOptions; /** * StraightEdge component props * @public + * @expand */ export type StraightEdgeProps = Omit; /** * SimpleBezier component props * @public + * @expand */ export type SimpleBezierEdgeProps = EdgeComponentProps; export type OnReconnect = (oldEdge: EdgeType, newConnection: Connection) => void; +/** + * If you want to render a custom component for connection lines, you can set the + * `connectionLineComponent` prop on the [``](/api-reference/react-flow#connection-connectionLineComponent) + * component. The `ConnectionLineComponentProps` are passed to your custom component. + * + * @public + */ export type ConnectionLineComponentProps = { connectionLineStyle?: CSSProperties; connectionLineType: ConnectionLineType; diff --git a/packages/react/src/types/general.ts b/packages/react/src/types/general.ts index 48e5b2fe..913ec634 100644 --- a/packages/react/src/types/general.ts +++ b/packages/react/src/types/general.ts @@ -18,11 +18,44 @@ import { import type { Node, Edge, ReactFlowInstance, EdgeProps, NodeProps } from '.'; +/** + * This type can be used to type the `onNodesChange` function with a custom node type. + * + * @public + * + * @example + * + * ```ts + * const onNodesChange: OnNodesChange = useCallback((changes) => { + * setNodes((nodes) => applyNodeChanges(nodes, changes)); + * },[]); + * ``` + */ export type OnNodesChange = (changes: NodeChange[]) => void; + +/** + * This type can be used to type the `onEdgesChange` function with a custom edge type. + * + * @public + * + * @example + * + * ```ts + * const onEdgesChange: OnEdgesChange = useCallback((changes) => { + * setEdges((edges) => applyEdgeChanges(edges, changes)); + * },[]); + * ``` + */ export type OnEdgesChange = (changes: EdgeChange[]) => void; export type OnNodesDelete = (nodes: NodeType[]) => void; export type OnEdgesDelete = (edges: EdgeType[]) => void; + +/** + * This type can be used to type the `onDelete` function with a custom node and edge type. + * + * @public + */ export type OnDelete = (params: { nodes: NodeType[]; edges: EdgeType[]; @@ -39,6 +72,7 @@ export type NodeTypes = Record< } > >; + export type EdgeTypes = Record< string, ComponentType< @@ -64,12 +98,23 @@ export type OnSelectionChangeParams = { export type OnSelectionChangeFunc = (params: OnSelectionChangeParams) => void; export type FitViewParams = FitViewParamsBase; + +/** + * When calling [`fitView`](/api-reference/types/react-flow-instance#fitview) these options + * can be used to customize the behaviour. For example, the `duration` option can be used to + * transform the viewport smoothly over a given amount of time. + * + * @public + */ export type FitViewOptions = FitViewOptionsBase; export type FitView = (fitViewOptions?: FitViewOptions) => Promise; export type OnInit = ( reactFlowInstance: ReactFlowInstance ) => void; +/** + * @inline + */ export type ViewportHelperFunctions = { /** * Zooms viewport in by 1.2. diff --git a/packages/react/src/types/instance.ts b/packages/react/src/types/instance.ts index 992655a4..85d66651 100644 --- a/packages/react/src/types/instance.ts +++ b/packages/react/src/types/instance.ts @@ -13,6 +13,9 @@ export type DeleteElementsOptions = { edges?: (Edge | { id: Edge['id'] })[]; }; +/** + * @inline + */ export type GeneralHelpers = { /** * Returns nodes. @@ -217,6 +220,14 @@ export type GeneralHelpers NodeConnection[]; }; +/** + * The `ReactFlowInstance` provides a collection of methods to query and manipulate + * the internal state of your flow. You can get an instance by using the + * [`useReactFlow`](/api-reference/hooks/use-react-flow) hook or attaching a listener + * to the [`onInit`](/api-reference/react-flow#event-oninit) event. + * + * @public + */ export type ReactFlowInstance = GeneralHelpers< NodeType, EdgeType diff --git a/packages/react/src/types/nodes.ts b/packages/react/src/types/nodes.ts index 0d3924e4..b07c0337 100644 --- a/packages/react/src/types/nodes.ts +++ b/packages/react/src/types/nodes.ts @@ -4,7 +4,10 @@ import type { CoordinateExtent, NodeBase, OnError, NodeProps as NodePropsBase, I import { NodeTypes } from './general'; /** - * The node data structure that gets used for the nodes prop. + * The `Node` type represents everything React Flow needs to know about a given node. + * Whenever you want to update a certain attribute of a node, you need to create a new + * node object. + * * @public */ export type Node< @@ -18,9 +21,10 @@ export type Node< }; /** - * The node data structure that gets used for internal nodes. - * There are some data structures added under node.internal - * that are needed for tracking some properties + * The `InternalNode` type is identical to the base [`Node`](/api-references/types/node) + * type but is extended with some additional properties used internally. + * Some functions and callbacks that return nodes may return an `InternalNode`. + * * @public */ export type InternalNode = InternalNodeBase; @@ -56,8 +60,46 @@ export type NodeWrapperProps = { nodeClickDistance?: number; }; +/** + * The `BuiltInNode` type represents the built-in node types that are available in React Flow. + * You can use this type to extend your custom node type if you still want ot use the built-in ones. + * + * @public + * @example + * ```ts + * type CustomNode = Node<{ value: number }, 'custom'>; + * type MyAppNode = CustomNode | BuiltInNode; + * ``` + */ export type BuiltInNode = | Node<{ label: string }, 'input' | 'output' | 'default'> | Node, 'group'>; +/** + * When you implement a [custom node](/learn/customization/custom-nodes) it is + * wrapped in a component that enables basic functionality like selection and + * dragging. Your custom node receives `NodeProps` as props. + * + * @public + * @example + * ```tsx + *import { useState } from 'react'; + *import { NodeProps, Node } from '@xyflow/react'; + * + *export type CounterNode = Node<{ initialCount?: number }, 'counter'>; + * + *export default function CounterNode(props: NodeProps) { + * const [count, setCount] = useState(props.data?.initialCount ?? 0); + * + * return ( + *
+ *

Count: {count}

+ * + *
+ * ); + *} + *``` + */ export type NodeProps = NodePropsBase; diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index 6de98e3a..0bc30e46 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -11,13 +11,17 @@ import { } from '@xyflow/system'; import type { Node, Edge, InternalNode } from '../types'; -// This function applies changes to nodes or edges that are triggered by React Flow internally. -// When you drag a node for example, React Flow will send a position change update. -// This function then applies the changes and returns the updated elements. +/* + * This function applies changes to nodes or edges that are triggered by React Flow internally. + * When you drag a node for example, React Flow will send a position change update. + * This function then applies the changes and returns the updated elements. + */ function applyChanges(changes: any[], elements: any[]): any[] { const updatedElements: any[] = []; - // By storing a map of changes for each element, we can a quick lookup as we - // iterate over the elements array! + /* + * By storing a map of changes for each element, we can a quick lookup as we + * iterate over the elements array! + */ const changesMap = new Map(); const addItemChanges: any[] = []; @@ -26,15 +30,19 @@ function applyChanges(changes: any[], elements: any[]): any[] { addItemChanges.push(change); continue; } else if (change.type === 'remove' || change.type === 'replace') { - // For a 'remove' change we can safely ignore any other changes queued for - // the same element, it's going to be removed anyway! + /* + * For a 'remove' change we can safely ignore any other changes queued for + * the same element, it's going to be removed anyway! + */ changesMap.set(change.id, [change]); } else { const elementChanges = changesMap.get(change.id); if (elementChanges) { - // If we have some changes queued already, we can do a mutable update of - // that array and save ourselves some copying. + /* + * If we have some changes queued already, we can do a mutable update of + * that array and save ourselves some copying. + */ elementChanges.push(change); } else { changesMap.set(change.id, [change]); @@ -45,8 +53,10 @@ function applyChanges(changes: any[], elements: any[]): any[] { for (const element of elements) { const changes = changesMap.get(element.id); - // When there are no changes for an element we can just push it unmodified, - // no need to copy it. + /* + * When there are no changes for an element we can just push it unmodified, + * no need to copy it. + */ if (!changes) { updatedElements.push(element); continue; @@ -62,9 +72,11 @@ function applyChanges(changes: any[], elements: any[]): any[] { continue; } - // For other types of changes, we want to start with a shallow copy of the - // object so React knows this element has changed. Sequential changes will - /// each _mutate_ this object, so there's only ever one copy. + /** + * For other types of changes, we want to start with a shallow copy of the + * object so React knows this element has changed. Sequential changes will + * each _mutate_ this object, so there's only ever one copy. + */ const updatedElement = { ...element }; for (const change of changes) { @@ -74,8 +86,10 @@ function applyChanges(changes: any[], elements: any[]): any[] { updatedElements.push(updatedElement); } - // we need to wait for all changes to be applied before adding new items - // to be able to add them at the correct index + /* + * we need to wait for all changes to be applied before adding new items + * to be able to add them at the correct index + */ if (addItemChanges.length) { addItemChanges.forEach((change) => { if (change.index !== undefined) { @@ -133,22 +147,33 @@ function applyChange(change: any, element: any): any { /** * Drop in function that applies node changes to an array of nodes. * @public - * @remarks Various events on the component can produce an {@link NodeChange} that describes how to update the edges of your flow in some way. - If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges. * @param changes - Array of changes to apply * @param nodes - Array of nodes to apply the changes to * @returns Array of updated nodes * @example - * const onNodesChange = useCallback( - (changes) => { - setNodes((oldNodes) => applyNodeChanges(changes, oldNodes)); - }, - [setNodes], - ); - - return ( - - ); + *```tsx + *import { useState, useCallback } from 'react'; + *import { ReactFlow, applyNodeChanges, type Node, type Edge, type OnNodesChange } from '@xyflow/react'; + * + *export default function Flow() { + * const [nodes, setNodes] = useState([]); + * const [edges, setEdges] = useState([]); + * const onNodesChange: OnNodesChange = useCallback( + * (changes) => { + * setNodes((oldNodes) => applyNodeChanges(changes, oldNodes)); + * }, + * [setNodes], + * ); + * + * return ( + * + * ); + *} + *``` + * @remarks Various events on the component can produce an {@link NodeChange} + * that describes how to update the edges of your flow in some way. + * If you don't need any custom behaviour, this util can be used to take an array + * of these changes and apply them to your edges. */ export function applyNodeChanges( changes: NodeChange[], @@ -160,22 +185,33 @@ export function applyNodeChanges( /** * Drop in function that applies edge changes to an array of edges. * @public - * @remarks Various events on the component can produce an {@link EdgeChange} that describes how to update the edges of your flow in some way. - If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges. * @param changes - Array of changes to apply * @param edges - Array of edge to apply the changes to * @returns Array of updated edges * @example + * ```tsx + *import { useState, useCallback } from 'react'; + *import { ReactFlow, applyEdgeChanges } from '@xyflow/react'; + * + *export default function Flow() { + * const [nodes, setNodes] = useState([]); + * const [edges, setEdges] = useState([]); * const onEdgesChange = useCallback( - (changes) => { - setEdges((oldEdges) => applyEdgeChanges(changes, oldEdges)); - }, - [setEdges], - ); - - return ( - - ); + * (changes) => { + * setEdges((oldEdges) => applyEdgeChanges(changes, oldEdges)); + * }, + * [setEdges], + * ); + * + * return ( + * + * ); + *} + *``` + * @remarks Various events on the component can produce an {@link EdgeChange} + * that describes how to update the edges of your flow in some way. + * If you don't need any custom behaviour, this util can be used to take an array + * of these changes and apply them to your edges. */ export function applyEdgeChanges( changes: EdgeChange[], @@ -205,9 +241,11 @@ export function getSelectionChanges( // we don't want to set all items to selected=false on the first selection if (!(item.selected === undefined && !willBeSelected) && item.selected !== willBeSelected) { if (mutateItem) { - // this hack is needed for nodes. When the user dragged a node, it's selected. - // When another node gets dragged, we need to deselect the previous one, - // in order to have only one selected node at a time - the onNodesChange callback comes too late here :/ + /* + * this hack is needed for nodes. When the user dragged a node, it's selected. + * When another node gets dragged, we need to deselect the previous one, + * in order to have only one selected node at a time - the onNodesChange callback comes too late here :/ + */ item.selected = willBeSelected; } changes.push(createSelectionChange(item.id, willBeSelected)); diff --git a/packages/react/src/utils/general.ts b/packages/react/src/utils/general.ts index 04608f74..e21bdfb6 100644 --- a/packages/react/src/utils/general.ts +++ b/packages/react/src/utils/general.ts @@ -4,21 +4,45 @@ import { isNodeBase, isEdgeBase } from '@xyflow/system'; import type { Edge, Node } from '../types'; /** - * Test whether an object is useable as a Node + * Test whether an object is useable as an [`Node`](/api-reference/types/node). + * In TypeScript this is a type guard that will narrow the type of whatever you pass in to + * [`Node`](/api-reference/types/node) if it returns `true`. + * * @public * @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Node if it returns true * @param element - The element to test * @returns A boolean indicating whether the element is an Node + * + * @example + * ```js + *import { isNode } from '@xyflow/react'; + * + *if (isNode(node)) { + * // .. + *} + *``` */ export const isNode = (element: unknown): element is NodeType => isNodeBase(element); /** - * Test whether an object is useable as an Edge + * Test whether an object is useable as an [`Edge`](/api-reference/types/edge). + * In TypeScript this is a type guard that will narrow the type of whatever you pass in to + * [`Edge`](/api-reference/types/edge) if it returns `true`. + * * @public * @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Edge if it returns true * @param element - The element to test * @returns A boolean indicating whether the element is an Edge + * + * @example + * ```js + *import { isEdge } from '@xyflow/react'; + * + *if (isEdge(edge)) { + * // .. + *} + *``` */ export const isEdge = (element: unknown): element is EdgeType => isEdgeBase(element); diff --git a/packages/svelte/src/lib/types/edges.ts b/packages/svelte/src/lib/types/edges.ts index de1def91..b2f6ba7e 100644 --- a/packages/svelte/src/lib/types/edges.ts +++ b/packages/svelte/src/lib/types/edges.ts @@ -11,7 +11,9 @@ import type { import type { Node } from '$lib/types'; /** - * The Edge type is mainly used for the `edges` that get passed to the SvelteFlow component. + * An `Edge` is the complete description with everything React Flow needs + *to know in order to render it. + * @public */ export type Edge< EdgeData extends Record = Record, diff --git a/packages/system/src/types/changes.ts b/packages/system/src/types/changes.ts index 9b232733..8ab81a37 100644 --- a/packages/system/src/types/changes.ts +++ b/packages/system/src/types/changes.ts @@ -42,7 +42,10 @@ export type NodeReplaceChange = { }; /** - * Union type of all possible node changes. + * The [`onNodesChange`](/api-reference/react-flow#on-nodes-change) callback takes + *an array of `NodeChange` objects that you should use to update your flow's state. + *The `NodeChange` type is a union of six different object types that represent that + *various ways an node can change in a flow. * @public */ export type NodeChange = @@ -67,6 +70,14 @@ export type EdgeReplaceChange = { type: 'replace'; }; +/** + * The [`onEdgesChange`](/api-reference/react-flow#on-edges-change) callback takes + *an array of `EdgeChange` objects that you should use to update your flow's state. + *The `EdgeChange` type is a union of four different object types that represent that + *various ways an edge can change in a flow. + * + * @public + */ export type EdgeChange = | EdgeSelectionChange | EdgeRemoveChange diff --git a/packages/system/src/types/edges.ts b/packages/system/src/types/edges.ts index 39dc97bd..76382e73 100644 --- a/packages/system/src/types/edges.ts +++ b/packages/system/src/types/edges.ts @@ -12,11 +12,13 @@ export type EdgeBase< source: string; /** Id of target node */ target: string; - /** Id of source handle + /** + * Id of source handle * only needed if there are multiple handles per node */ sourceHandle?: string | null; - /** Id of target handle + /** + * Id of target handle * only needed if there are multiple handles per node */ targetHandle?: string | null; @@ -27,11 +29,13 @@ export type EdgeBase< /** Arbitrary data passed to an edge */ data?: EdgeData; selected?: boolean; - /** Set the marker on the beginning of an edge + /** + * Set the marker on the beginning of an edge * @example 'arrow', 'arrowclosed' or custom marker */ markerStart?: EdgeMarkerType; - /** Set the marker on the end of an edge + /** + * Set the marker on the end of an edge * @example 'arrow', 'arrowclosed' or custom marker */ markerEnd?: EdgeMarkerType; @@ -54,11 +58,24 @@ export type BezierPathOptions = { curvature?: number; }; +/** + * @inline + */ export type DefaultEdgeOptionsBase = Omit< EdgeType, 'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'selected' >; +/** + * If you set the `connectionLineType` prop on your [``](/api-reference/react-flow#connection-connectionLineType) + *component, it will dictate the style of connection line rendered when creating + *new edges. + * + * @public + * + * @remarks If you choose to render a custom connection line component, this value will be + *passed to your component as part of its [`ConnectionLineComponentProps`](/api-reference/types/connection-line-component-props). + */ export enum ConnectionLineType { Bezier = 'default', Straight = 'straight', @@ -67,6 +84,13 @@ export enum ConnectionLineType { SimpleBezier = 'simplebezier', } +/** + * Edges can optionally have markers at the start and end of an edge. The `EdgeMarker` + *type is used to configure those markers! Check the docs for [`MarkerType`](/api-reference/types/marker-type) + *for details on what types of edge marker are available. + * + * @public + */ export type EdgeMarker = { type: MarkerType; color?: string; @@ -79,6 +103,12 @@ export type EdgeMarker = { export type EdgeMarkerType = string | EdgeMarker; +/** + * Edges may optionally have a marker on either end. The MarkerType type enumerates + * the options available to you when configuring a given marker. + * + * @public + */ export enum MarkerType { Arrow = 'arrow', ArrowClosed = 'arrowclosed', @@ -88,6 +118,9 @@ export type MarkerProps = EdgeMarker & { id: string; }; +/** + * @inline + */ export type EdgePosition = { sourceX: number; sourceY: number; diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 3c6eed52..8ac3f9e9 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -26,6 +26,13 @@ export type SetViewport = (viewport: Viewport, options?: ViewportHelperFunctionO export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => Promise; export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => Promise; +/** + * The `Connection` type is the basic minimal description of an [`Edge`](/api-reference/types/edge) + * between two nodes. The [`addEdge`](/api-reference/utils/add-edge) util can be used to upgrade + * a `Connection` to an [`Edge`](/api-reference/types/edge). + * + * @public + */ export type Connection = { source: string; target: string; @@ -33,15 +40,28 @@ export type Connection = { targetHandle: string | null; }; -// TODO: remove in next version +/** + * The `HandleConnection` type is an extention of a basic [Connection](/api-reference/types/connection) that includes the `edgeId`. + */ export type HandleConnection = Connection & { edgeId: string; }; +/** + * The `NodeConnection` type is an extention of a basic [Connection](/api-reference/types/connection) that includes the `edgeId`. + * + */ export type NodeConnection = Connection & { edgeId: string; }; +/** + * The `ConnectionMode` is used to set the mode of connection between nodes. + * The `Strict` mode is the default one and only allows source to target edges. + * `Loose` mode allows source to source and target to target edges as well. + * + * @public + */ export enum ConnectionMode { Strict = 'strict', Loose = 'loose', @@ -59,6 +79,9 @@ export type OnConnectEnd = (event: MouseEvent | TouchEvent, connectionState: Fin export type IsValidConnection = (edge: EdgeBase | Connection) => boolean; +/** + * @inline + */ export type FitViewParamsBase = { nodes: Map>; width: number; @@ -68,6 +91,9 @@ export type FitViewParamsBase = { maxZoom: number; }; +/** + * @inline + */ export type FitViewOptionsBase = { padding?: number; includeHiddenNodes?: boolean; @@ -77,6 +103,17 @@ export type FitViewOptionsBase = { nodes?: (NodeType | { id: string })[]; }; +/** + * Internally, React Flow maintains a coordinate system that is independent of the + * rest of the page. The `Viewport` type tells you where in that system your flow + * is currently being display at and how zoomed in or out it is. + * + * @public + * @remarks A `Transform` has the same properties as the viewport, but they represent + * different things. Make sure you don't get them muddled up or things will start + * to look weird! + * + */ export type Viewport = { x: number; y: number; @@ -87,12 +124,23 @@ export type KeyCode = string | Array; export type SnapGrid = [number, number]; +/** + * This enum is used to set the different modes of panning the viewport when the + * user scrolls. The `Free` mode allows the user to pan in any direction by scrolling + * with a device like a trackpad. The `Vertical` and `Horizontal` modes restrict + * scroll panning to only the vertical or horizontal axis, respectively. + * + * @public + */ export enum PanOnScrollMode { Free = 'free', Vertical = 'vertical', Horizontal = 'horizontal', } +/** + * @inline + */ export type ViewportHelperFunctionOptions = { duration?: number; }; @@ -113,6 +161,14 @@ export type D3ZoomHandler = (this: Element, event: any, d: unknown) => void; export type UpdateNodeInternals = (nodeId: string | string[]) => void; +/** + * This type is mostly used to help position things on top of the flow viewport. For + * example both the [``](/api-reference/components/minimap) and + * [``](/api-reference/components/controls) components take a `position` + * prop of this type. + * + * @public + */ export type PanelPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'; export type ProOptions = { @@ -174,6 +230,13 @@ export type ConnectionInProgress = | ConnectionInProgress | NoConnection; diff --git a/packages/system/src/types/handles.ts b/packages/system/src/types/handles.ts index 9cf2a1dd..ef67d373 100644 --- a/packages/system/src/types/handles.ts +++ b/packages/system/src/types/handles.ts @@ -14,11 +14,13 @@ export type Handle = { }; export type HandleProps = { - /** Type of the handle + /** + * Type of the handle * @example HandleType.Source, HandleType.Target */ type: HandleType; - /** Position of the handle + /** + * Position of the handle * @example Position.TopLeft, Position.TopRight, * Position.BottomLeft, Position.BottomRight */ @@ -29,11 +31,13 @@ export type HandleProps = { isConnectableStart?: boolean; /** Should you be able to connect to this handle */ isConnectableEnd?: boolean; - /** Callback if connection is valid + /** + * Callback if connection is valid * @remarks connection becomes an edge if isValidConnection returns true */ isValidConnection?: IsValidConnection; - /** Id of the handle + /** + * Id of the handle * @remarks optional if there is only one handle of this type */ id?: string | null; diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index 92261205..68dd0ce3 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -4,6 +4,7 @@ import { Optional } from '../utils/types'; /** * Framework independent node data structure. * + * @inline * @typeParam NodeData - type of the node data * @typeParam NodeType - type of the node */ @@ -13,7 +14,8 @@ export type NodeBase< > = { /** Unique id of a node */ id: string; - /** Position of a node on the pane + /** + * Position of a node on the pane * @example { x: 0, y: 0 } */ position: XYPosition; @@ -21,11 +23,13 @@ export type NodeBase< data: NodeData; /** Type of node defined in nodeTypes */ type?: NodeType; - /** Only relevant for default, source, target nodeType. controls source position + /** + * Only relevant for default, source, target nodeType. controls source position * @example 'right', 'left', 'top', 'bottom' */ sourcePosition?: Position; - /** Only relevant for default, source, target nodeType. controls target position + /** + * Only relevant for default, source, target nodeType. controls target position * @example 'right', 'left', 'top', 'bottom' */ targetPosition?: Position; @@ -45,13 +49,15 @@ export type NodeBase< /** Parent node id, used for creating sub-flows */ parentId?: string; zIndex?: number; - /** Boundary a node can be moved in + /** + * Boundary a node can be moved in * @example 'parent' or [[0, 0], [100, 100]] */ extent?: 'parent' | CoordinateExtent; expandParent?: boolean; ariaLabel?: string; - /** Origin of the node relative to it's position + /** + * Origin of the node relative to it's position * @example * [0.5, 0.5] // centers the node * [0, 0] // top left @@ -73,8 +79,10 @@ export type InternalNodeBase = NodeType & internals: { positionAbsolute: XYPosition; z: number; - /** Holds a reference to the original node object provided by the user. - * Used as an optimization to avoid certain operations. */ + /** + * Holds a reference to the original node object provided by the user. + * Used as an optimization to avoid certain operations. + */ userNode: NodeType; handleBounds?: NodeHandleBounds; bounds?: NodeBounds; @@ -82,7 +90,7 @@ export type InternalNodeBase = NodeType & }; /** - * The node data structure that gets used for the nodes prop. + * The node data structure that gets used for the custom nodes props. * * @public */ @@ -134,10 +142,22 @@ export type NodeDragItem = { expandParent?: boolean; }; +/** + * The origin of a Node determines how it is placed relative to its own coordinates. + * `[0, 0]` places it at the top left corner, `[0.5, 0.5]` right in the center and + * `[1, 1]` at the bottom right of its position. + * + * @public + */ export type NodeOrigin = [number, number]; export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void; +/** + * Type for the handles of a node + * + * @public + */ export type NodeHandle = Omit, 'nodeId'>; export type Align = 'center' | 'start' | 'end'; diff --git a/packages/system/src/types/utils.ts b/packages/system/src/types/utils.ts index b06786a7..274bfe55 100644 --- a/packages/system/src/types/utils.ts +++ b/packages/system/src/types/utils.ts @@ -1,3 +1,10 @@ +/** + * While [`PanelPosition`](/api-reference/types/panel-position) can be used to place a + * component in the corners of a container, the `Position` enum is less precise and used + * primarily in relation to edges and handles. + * + * @public + */ export enum Position { Left = 'left', Top = 'top', @@ -12,6 +19,11 @@ export const oppositePosition = { [Position.Bottom]: Position.Top, }; +/** + * All positions are stored in an object with x and y coordinates. + * + * @public + */ export type XYPosition = { x: number; y: number; @@ -33,4 +45,14 @@ export type Box = XYPosition & { export type Transform = [number, number, number]; +/** + * A coordinate extent represents two points in a coordinate system: one in the top + * left corner and one in the bottom right corner. It is used to represent the + * bounds of nodes in the flow or the bounds of the viewport. + * + * @public + * + * @remarks Props that expect a `CoordinateExtent` usually default to `[[-∞, -∞], [+∞, +∞]]` + * to represent an unbounded extent. + */ export type CoordinateExtent = [[number, number], [number, number]]; diff --git a/packages/system/src/utils/dom.ts b/packages/system/src/utils/dom.ts index 12188e9b..2311153f 100644 --- a/packages/system/src/utils/dom.ts +++ b/packages/system/src/utils/dom.ts @@ -61,9 +61,11 @@ export const getEventPosition = (event: MouseEvent | TouchEvent, bounds?: DOMRec }; }; -// The handle bounds are calculated relative to the node element. -// We store them in the internals object of the node in order to avoid -// unnecessary recalculations. +/* + * The handle bounds are calculated relative to the node element. + * We store them in the internals object of the node in order to avoid + * unnecessary recalculations. + */ export const getHandleBounds = ( type: 'source' | 'target', nodeElement: HTMLDivElement, diff --git a/packages/system/src/utils/edges/bezier-edge.ts b/packages/system/src/utils/edges/bezier-edge.ts index 640deffa..3b373938 100644 --- a/packages/system/src/utils/edges/bezier-edge.ts +++ b/packages/system/src/utils/edges/bezier-edge.ts @@ -38,8 +38,10 @@ export function getBezierEdgeCenter({ targetControlX: number; targetControlY: number; }): [number, number, number, number] { - // cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate - // https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve + /* + * cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate + * https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve + */ const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125; const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125; const offsetX = Math.abs(centerX - sourceX); @@ -70,7 +72,9 @@ function getControlWithCurvature({ pos, x1, y1, x2, y2, c }: GetControlWithCurva } /** - * Get a bezier path from source to target handle + * The `getBezierPath` util returns everything you need to render a bezier edge + *between two nodes. + * @public * @param params.sourceX - The x position of the source handle * @param params.sourceY - The y position of the source handle * @param params.sourcePosition - The position of the source handle (default: Position.Bottom) @@ -80,17 +84,22 @@ function getControlWithCurvature({ pos, x1, y1, x2, y2, c }: GetControlWithCurva * @param params.curvature - The curvature of the bezier edge * @returns A path string you can use in an SVG, the labelX and labelY position (center of path) and offsetX, offsetY between source handle and label * @example + * ```js * const source = { x: 0, y: 20 }; - const target = { x: 150, y: 100 }; - - const [path, labelX, labelY, offsetX, offsetY] = getBezierPath({ - sourceX: source.x, - sourceY: source.y, - sourcePosition: Position.Right, - targetX: target.x, - targetY: target.y, - targetPosition: Position.Left, -}); + * const target = { x: 150, y: 100 }; + * + * const [path, labelX, labelY, offsetX, offsetY] = getBezierPath({ + * sourceX: source.x, + * sourceY: source.y, + * sourcePosition: Position.Right, + * targetX: target.x, + * targetY: target.y, + * targetPosition: Position.Left, + *}); + *``` + * + * @remarks This function returns a tuple (aka a fixed-size array) to make it easier to + *work with multiple edge paths at once. */ export function getBezierPath({ sourceX, diff --git a/packages/system/src/utils/edges/general.ts b/packages/system/src/utils/edges/general.ts index 7dfa4b44..7f98d00f 100644 --- a/packages/system/src/utils/edges/general.ts +++ b/packages/system/src/utils/edges/general.ts @@ -90,12 +90,16 @@ const connectionExists = (edge: EdgeBase, edges: EdgeBase[]) => { }; /** - * This util is a convenience function to add a new Edge to an array of edges - * @remarks It also performs some validation to make sure you don't add an invalid edge or duplicate an existing one. + * This util is a convenience function to add a new Edge to an array of edges. It also performs some validation to make sure you don't add an invalid edge or duplicate an existing one. * @public * @param edgeParams - Either an Edge or a Connection you want to add * @param edges - The array of all current edges * @returns A new array of edges with the new edge added + * + * @remarks If an edge with the same `target` and `source` already exists (and the same + *`targetHandle` and `sourceHandle` if those are set), then this util won't add + *a new edge even if the `id` property is different. + * */ export const addEdge = ( edgeParams: EdgeType | Connection, @@ -137,12 +141,21 @@ export type ReconnectEdgeOptions = { }; /** - * A handy utility to reconnect an existing edge with new properties + * A handy utility to update an existing [`Edge`](/api-reference/types/edge) with new properties. + *This searches your edge array for an edge with a matching `id` and updates its + *properties with the connection you provide. + * @public * @param oldEdge - The edge you want to update * @param newConnection - The new connection you want to update the edge with * @param edges - The array of all current edges * @param options.shouldReplaceId - should the id of the old edge be replaced with the new connection id * @returns the updated edges array + * + * @example + * ```js + *const onReconnect = useCallback( + * (oldEdge: Edge, newConnection: Connection) => setEdges((els) => reconnectEdge(oldEdge, newConnection, els)),[]); + *``` */ export const reconnectEdge = ( oldEdge: EdgeType, diff --git a/packages/system/src/utils/edges/smoothstep-edge.ts b/packages/system/src/utils/edges/smoothstep-edge.ts index 5e57efcd..0771f357 100644 --- a/packages/system/src/utils/edges/smoothstep-edge.ts +++ b/packages/system/src/utils/edges/smoothstep-edge.ts @@ -38,8 +38,10 @@ const getDirection = ({ const distance = (a: XYPosition, b: XYPosition) => Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2)); -// ith this function we try to mimic a orthogonal edge routing behaviour -// It's not as good as a real orthogonal edge routing but it's faster and good enough as a default for step and smooth step edges +/* + * ith this function we try to mimic a orthogonal edge routing behaviour + * It's not as good as a real orthogonal edge routing but it's faster and good enough as a default for step and smooth step edges + */ function getPoints({ source, sourcePosition = Position.Bottom, @@ -83,16 +85,20 @@ function getPoints({ if (sourceDir[dirAccessor] * targetDir[dirAccessor] === -1) { centerX = center.x ?? defaultCenterX; centerY = center.y ?? defaultCenterY; - // ---> - // | - // >--- + /* + * ---> + * | + * >--- + */ const verticalSplit: XYPosition[] = [ { x: centerX, y: sourceGapped.y }, { x: centerX, y: targetGapped.y }, ]; - // | - // --- - // | + /* + * | + * --- + * | + */ const horizontalSplit: XYPosition[] = [ { x: sourceGapped.x, y: centerY }, { x: targetGapped.x, y: centerY }, @@ -191,7 +197,10 @@ function getBend(a: XYPosition, b: XYPosition, c: XYPosition, size: number): str } /** - * Get a smooth step path from source to target handle + * The `getSmoothStepPath` util returns everything you need to render a stepped path + *between two nodes. The `borderRadius` property can be used to choose how rounded + *the corners of those steps are. + * @public * @param params.sourceX - The x position of the source handle * @param params.sourceY - The y position of the source handle * @param params.sourcePosition - The position of the source handle (default: Position.Bottom) @@ -200,17 +209,20 @@ function getBend(a: XYPosition, b: XYPosition, c: XYPosition, size: number): str * @param params.targetPosition - The position of the target handle (default: Position.Top) * @returns A path string you can use in an SVG, the labelX and labelY position (center of path) and offsetX, offsetY between source handle and label * @example + * ```js * const source = { x: 0, y: 20 }; - const target = { x: 150, y: 100 }; - - const [path, labelX, labelY, offsetX, offsetY] = getSmoothStepPath({ - sourceX: source.x, - sourceY: source.y, - sourcePosition: Position.Right, - targetX: target.x, - targetY: target.y, - targetPosition: Position.Left, - }); + * const target = { x: 150, y: 100 }; + * + * const [path, labelX, labelY, offsetX, offsetY] = getSmoothStepPath({ + * sourceX: source.x, + * sourceY: source.y, + * sourcePosition: Position.Right, + * targetX: target.x, + * targetY: target.y, + * targetPosition: Position.Left, + * }); + * ``` + * @remarks This function returns a tuple (aka a fixed-size array) to make it easier to work with multiple edge paths at once. */ export function getSmoothStepPath({ sourceX, diff --git a/packages/system/src/utils/edges/straight-edge.ts b/packages/system/src/utils/edges/straight-edge.ts index 9940bfea..baa7093b 100644 --- a/packages/system/src/utils/edges/straight-edge.ts +++ b/packages/system/src/utils/edges/straight-edge.ts @@ -8,24 +8,28 @@ export type GetStraightPathParams = { }; /** - * Get a straight path from source to target handle + * Calculates the straight line path between two points. + * @public * @param params.sourceX - The x position of the source handle * @param params.sourceY - The y position of the source handle * @param params.targetX - The x position of the target handle * @param params.targetY - The y position of the target handle * @returns A path string you can use in an SVG, the labelX and labelY position (center of path) and offsetX, offsetY between source handle and label * @example + * ```js * const source = { x: 0, y: 20 }; - const target = { x: 150, y: 100 }; - - const [path, labelX, labelY, offsetX, offsetY] = getStraightPath({ - sourceX: source.x, - sourceY: source.y, - sourcePosition: Position.Right, - targetX: target.x, - targetY: target.y, - targetPosition: Position.Left, - }); + * const target = { x: 150, y: 100 }; + * + * const [path, labelX, labelY, offsetX, offsetY] = getStraightPath({ + * sourceX: source.x, + * sourceY: source.y, + * sourcePosition: Position.Right, + * targetX: target.x, + * targetY: target.y, + * targetPosition: Position.Left, + * }); + * ``` + * @remarks This function returns a tuple (aka a fixed-size array) to make it easier to work with multiple edge paths at once. */ export function getStraightPath({ sourceX, diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index 7f631a59..32c339e5 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -186,8 +186,8 @@ export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Tra * @returns A transforned {@link Viewport} that encloses the given bounds which you can pass to e.g. {@link setViewport} * @example * const { x, y, zoom } = getViewportForBounds( - { x: 0, y: 0, width: 100, height: 100}, - 1200, 800, 0.5, 2); + *{ x: 0, y: 0, width: 100, height: 100}, + *1200, 800, 0.5, 2); */ export const getViewportForBounds = ( bounds: Rect, diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index ddec917c..b2b1e6e5 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -54,12 +54,27 @@ export const isInternalNodeBase = 'id' in element && 'internals' in element && !('source' in element) && !('target' in element); /** - * Pass in a node, and get connected nodes where edge.source === node.id + * This util is used to tell you what nodes, if any, are connected to the given node + * as the _target_ of an edge. * @public * @param node - The node to get the connected nodes from * @param nodes - The array of all nodes * @param edges - The array of all edges * @returns An array of nodes that are connected over eges where the source is the given node + * + * @example + * ```ts + *import { getOutgoers } from '@xyflow/react'; + * + *const nodes = []; + *const edges = []; + * + *const outgoers = getOutgoers( + * { id: '1', position: { x: 0, y: 0 }, data: { label: 'node' } }, + * nodes, + * edges, + *); + *``` */ export const getOutgoers = ( node: NodeType | { id: string }, @@ -81,12 +96,27 @@ export const getOutgoers = ( node: NodeType | { id: string }, @@ -124,12 +154,40 @@ export type GetNodesBoundsParams = { }; /** - * Internal function for determining a bounding box that contains all given nodes in an array. + * Returns the bounding box that contains all the given nodes in an array. This can + * be useful when combined with [`getViewportForBounds`](/api-reference/utils/get-viewport-for-bounds) + * to calculate the correct transform to fit the given nodes in a viewport. * @public * @remarks Useful when combined with {@link getViewportForBounds} to calculate the correct transform to fit the given nodes in a viewport. * @param nodes - Nodes to calculate the bounds for * @param params.nodeOrigin - Origin of the nodes: [0, 0] - top left, [0.5, 0.5] - center * @returns Bounding box enclosing all nodes + * + * @remarks This function was previously called `getRectOfNodes` + * + * @example + * ```js + *import { getNodesBounds } from '@xyflow/react'; + * + *const nodes = [ + * { + * id: 'a', + * position: { x: 0, y: 0 }, + * data: { label: 'a' }, + * width: 50, + * height: 25, + * }, + * { + * id: 'b', + * position: { x: 100, y: 100 }, + * data: { label: 'b' }, + * width: 50, + * height: 25, + * }, + *]; + * + *const bounds = getNodesBounds(nodes); + *``` */ export const getNodesBounds = ( nodes: (NodeType | InternalNodeBase | string)[], @@ -238,10 +296,30 @@ export const getNodesInside = ( }; /** - * Get all connecting edges for a given set of nodes + * This utility filters an array of edges, keeping only those where either the source or target + * node is present in the given array of nodes. + * @public * @param nodes - Nodes you want to get the connected edges for * @param edges - All edges * @returns Array of edges that connect any of the given nodes with each other + * + * @example + * ```js + *import { getConnectedEdges } from '@xyflow/react'; + * + *const nodes = [ + * { id: 'a', position: { x: 0, y: 0 } }, + * { id: 'b', position: { x: 100, y: 0 } }, + *]; + * + *const edges = [ + * { id: 'a->c', source: 'a', target: 'c' }, + * { id: 'c->d', source: 'c', target: 'd' }, + *]; + * + *const connectedEdges = getConnectedEdges(nodes, edges); + * // => [{ id: 'a->c', source: 'a', target: 'c' }] + *``` */ export const getConnectedEdges = ( nodes: NodeType[], diff --git a/packages/system/src/utils/node-toolbar.ts b/packages/system/src/utils/node-toolbar.ts index 1813513c..f5246bcc 100644 --- a/packages/system/src/utils/node-toolbar.ts +++ b/packages/system/src/utils/node-toolbar.ts @@ -15,8 +15,10 @@ export function getNodeToolbarTransform( alignmentOffset = 1; } - // position === Position.Top - // we set the x any y position of the toolbar based on the nodes position + /* + * position === Position.Top + * we set the x any y position of the toolbar based on the nodes position + */ let pos = [ (nodeRect.x + nodeRect.width * alignmentOffset) * viewport.zoom + viewport.x, nodeRect.y * viewport.zoom + viewport.y - offset, diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 77c5b369..a9d13316 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -276,8 +276,10 @@ export function handleExpandParent( }, }); - // We move all child nodes in the oppsite direction - // so the x,y changes of the parent do not move the children + /* + * We move all child nodes in the oppsite direction + * so the x,y changes of the parent do not move the children + */ parentLookup.get(parentId)?.forEach((childNode) => { if (!children.some((child) => child.id === childNode.id)) { changes.push({ @@ -472,9 +474,11 @@ function addConnectionToLookup( nodeId: string, handleId: string | null ) { - // We add the connection to the connectionLookup at the following keys - // 1. nodeId, 2. nodeId-type, 3. nodeId-type-handleId - // If the key already exists, we add the connection to the existing map + /* + * We add the connection to the connectionLookup at the following keys + * 1. nodeId, 2. nodeId-type, 3. nodeId-type-handleId + * If the key already exists, we add the connection to the existing map + */ let key = nodeId; const nodeMap = connectionLookup.get(key) || new Map(); connectionLookup.set(key, nodeMap.set(connectionKey, connection)); diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts index 0921e3ec..33658aad 100644 --- a/packages/system/src/xydrag/XYDrag.ts +++ b/packages/system/src/xydrag/XYDrag.ts @@ -140,8 +140,10 @@ export function XYDrag voi for (const [id, dragItem] of dragItems) { if (!nodeLookup.has(id)) { - // if the node is not in the nodeLookup anymore, it was probably deleted while dragging - // and we don't need to update it anymore + /* + * if the node is not in the nodeLookup anymore, it was probably deleted while dragging + * and we don't need to update it anymore + */ continue; } @@ -150,8 +152,10 @@ export function XYDrag voi nextPosition = snapPosition(nextPosition, snapGrid); } - // if there is selection with multiple nodes and a node extent is set, we need to adjust the node extent for each node - // based on its position so that the node stays at it's position relative to the selection. + /* + * if there is selection with multiple nodes and a node extent is set, we need to adjust the node extent for each node + * based on its position so that the node stays at it's position relative to the selection. + */ let adjustedNodeExtent: CoordinateExtent = [ [nodeExtent[0][0], nodeExtent[0][1]], [nodeExtent[1][0], nodeExtent[1][1]], diff --git a/packages/system/src/xydrag/utils.ts b/packages/system/src/xydrag/utils.ts index 5c0eda0a..dc980281 100644 --- a/packages/system/src/xydrag/utils.ts +++ b/packages/system/src/xydrag/utils.ts @@ -74,9 +74,11 @@ export function getDragItems( return dragItems; } -// returns two params: -// 1. the dragged node (or the first of the list, if we are dragging a node selection) -// 2. array of selected nodes (for multi selections) +/* + * returns two params: + * 1. the dragged node (or the first of the list, if we are dragging a node selection) + * 2. array of selected nodes (for multi selections) + */ export function getEventHandlerParams({ nodeId, dragItems, @@ -112,10 +114,10 @@ export function getEventHandlerParams({ !node ? nodesFromDragItems[0] : { - ...node, - position: dragItems.get(nodeId)?.position || node.position, - dragging, - }, + ...node, + position: dragItems.get(nodeId)?.position || node.position, + dragging, + }, nodesFromDragItems, ]; } diff --git a/packages/system/src/xyhandle/XYHandle.ts b/packages/system/src/xyhandle/XYHandle.ts index 5f4fc610..9c2af456 100644 --- a/packages/system/src/xyhandle/XYHandle.ts +++ b/packages/system/src/xyhandle/XYHandle.ts @@ -165,8 +165,10 @@ function onPointerDown( toNode: result.toHandle ? nodeLookup.get(result.toHandle.nodeId)! : null, }; - // we don't want to trigger an update when the connection - // is snapped to the same handle as before + /* + * we don't want to trigger an update when the connection + * is snapped to the same handle as before + */ if ( isValid && closestHandle && @@ -190,8 +192,10 @@ function onPointerDown( onConnect?.(connection); } - // it's important to get a fresh reference from the store here - // in order to get the latest state of onConnectEnd + /* + * it's important to get a fresh reference from the store here + * in order to get the latest state of onConnectEnd + */ // eslint-disable-next-line @typescript-eslint/no-unused-vars const { inProgress, ...connectionState } = previousConnection; const finalConnectionState = { @@ -248,8 +252,10 @@ function isValidHandle( const { x, y } = getEventPosition(event); const handleBelow = doc.elementFromPoint(x, y); - // we always want to prioritize the handle below the mouse cursor over the closest distance handle, - // because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor + /* + * we always want to prioritize the handle below the mouse cursor over the closest distance handle, + * because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor + */ const handleToCheck = handleBelow?.classList.contains(`${lib}-flow__handle`) ? handleBelow : handleDomNode; const result: Result = { diff --git a/packages/system/src/xyhandle/utils.ts b/packages/system/src/xyhandle/utils.ts index ecc9b1cc..4650f192 100644 --- a/packages/system/src/xyhandle/utils.ts +++ b/packages/system/src/xyhandle/utils.ts @@ -19,8 +19,10 @@ function getNodesWithinDistance(position: XYPosition, nodeLookup: NodeLookup, di return nodes; } -// this distance is used for the area around the user pointer -// while doing a connection for finding the closest nodes +/* + * this distance is used for the area around the user pointer + * while doing a connection for finding the closest nodes + */ const ADDITIONAL_DISTANCE = 250; export function getClosestHandle( diff --git a/packages/system/src/xypanzoom/XYPanZoom.ts b/packages/system/src/xypanzoom/XYPanZoom.ts index 54b97e30..93bc79f8 100644 --- a/packages/system/src/xypanzoom/XYPanZoom.ts +++ b/packages/system/src/xypanzoom/XYPanZoom.ts @@ -114,22 +114,22 @@ export function XYPanZoom({ const wheelHandler = isPanOnScroll ? createPanOnScrollHandler({ - zoomPanValues, - noWheelClassName, - d3Selection, - d3Zoom: d3ZoomInstance, - panOnScrollMode, - panOnScrollSpeed, - zoomOnPinch, - onPanZoomStart, - onPanZoom, - onPanZoomEnd, - }) + zoomPanValues, + noWheelClassName, + d3Selection, + d3Zoom: d3ZoomInstance, + panOnScrollMode, + panOnScrollSpeed, + zoomOnPinch, + onPanZoomStart, + onPanZoom, + onPanZoomEnd, + }) : createZoomOnScrollHandler({ - noWheelClassName, - preventScrolling, - d3ZoomHandler, - }); + noWheelClassName, + preventScrolling, + d3ZoomHandler, + }); d3Selection.on('wheel.zoom', wheelHandler, { passive: false }); @@ -178,9 +178,11 @@ export function XYPanZoom({ }); d3ZoomInstance.filter(filter); - // We cannot add zoomOnDoubleClick to the filter above because - // double tapping on touch screens circumvents the filter and - // dblclick.zoom is fired on the selection directly + /* + * We cannot add zoomOnDoubleClick to the filter above because + * double tapping on touch screens circumvents the filter and + * dblclick.zoom is fired on the selection directly + */ if (zoomOnDoubleClick) { d3Selection.on('dblclick.zoom', d3DblClickZoomHandler); } else { diff --git a/packages/system/src/xypanzoom/eventhandler.ts b/packages/system/src/xypanzoom/eventhandler.ts index 2e78b90a..751a31c5 100644 --- a/packages/system/src/xypanzoom/eventhandler.ts +++ b/packages/system/src/xypanzoom/eventhandler.ts @@ -90,8 +90,10 @@ export function createPanOnScrollHandler({ return; } - // increase scroll speed in firefox - // firefox: deltaMode === 1; chrome: deltaMode === 0 + /* + * increase scroll speed in firefox + * firefox: deltaMode === 1; chrome: deltaMode === 0 + */ const deltaNormalize = event.deltaMode === 1 ? 20 : 1; let deltaX = panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize; let deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize; @@ -114,9 +116,11 @@ export function createPanOnScrollHandler({ clearTimeout(zoomPanValues.panScrollTimeout); - // for pan on scroll we need to handle the event calls on our own - // we can't use the start, zoom and end events from d3-zoom - // because start and move gets called on every scroll event and not once at the beginning + /* + * for pan on scroll we need to handle the event calls on our own + * we can't use the start, zoom and end events from d3-zoom + * because start and move gets called on every scroll event and not once at the beginning + */ if (!zoomPanValues.isPanScrolling) { zoomPanValues.isPanScrolling = true; diff --git a/packages/system/src/xyresizer/XYResizer.ts b/packages/system/src/xyresizer/XYResizer.ts index 1f4ba321..44dd1ac6 100644 --- a/packages/system/src/xyresizer/XYResizer.ts +++ b/packages/system/src/xyresizer/XYResizer.ts @@ -154,8 +154,10 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X parentExtent = parentNode && node.extent === 'parent' ? nodeToParentExtent(parentNode) : undefined; } - // Collect all child nodes to correct their relative positions when top/left changes - // Determine largest minimal extent the parent node is allowed to resize to + /* + * Collect all child nodes to correct their relative positions when top/left changes + * Determine largest minimal extent the parent node is allowed to resize to + */ childNodes = []; childExtent = undefined; @@ -230,8 +232,10 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X prevValues.x = change.x; prevValues.y = change.y; - // when top/left changes, correct the relative positions of child nodes - // so that they stay in the same position + /* + * when top/left changes, correct the relative positions of child nodes + * so that they stay in the same position + */ if (childNodes.length > 0) { const xChange = x - prevX; const yChange = y - prevY; diff --git a/packages/system/src/xyresizer/types.ts b/packages/system/src/xyresizer/types.ts index 99480512..34cad9e8 100644 --- a/packages/system/src/xyresizer/types.ts +++ b/packages/system/src/xyresizer/types.ts @@ -11,10 +11,25 @@ export type ResizeParamsWithDirection = ResizeParams & { direction: number[]; }; +/** + * Used to determine the control line position of the NodeResizer + * + * @public + */ export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right'; +/** + * Used to determine the control position of the NodeResizer + * + * @public + */ export type ControlPosition = ControlLinePosition | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; +/** + * Used to determine the variant of the resize control + * + * @public + */ export enum ResizeControlVariant { Line = 'line', Handle = 'handle', diff --git a/tooling/eslint-config/package.json b/tooling/eslint-config/package.json index 00da90da..d4aa78ee 100644 --- a/tooling/eslint-config/package.json +++ b/tooling/eslint-config/package.json @@ -5,11 +5,11 @@ "license": "MIT", "main": "src/index.js", "devDependencies": { - "eslint-config-prettier": "^10.0.1", - "eslint-config-turbo": "^2.4.0", - "eslint-plugin-react": "^7.37.4", "@typescript-eslint/eslint-plugin": "^8.23.0", "@typescript-eslint/parser": "^8.23.0", - "eslint-plugin-prettier": "^4.2.1" + "eslint-config-prettier": "^10.0.1", + "eslint-config-turbo": "^2.4.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-react": "^7.37.4" } }