diff --git a/.changeset/sharp-dogs-chew.md b/.changeset/sharp-dogs-chew.md new file mode 100644 index 00000000..086b5961 --- /dev/null +++ b/.changeset/sharp-dogs-chew.md @@ -0,0 +1,7 @@ +--- +'@xyflow/react': minor +'@xyflow/svelte': minor +'@xyflow/system': minor +--- + +Add `domAttributes` option for nodes and edges diff --git a/examples/react/src/examples/A11y/index.tsx b/examples/react/src/examples/A11y/index.tsx index f7f0b5b2..e7fefc2d 100644 --- a/examples/react/src/examples/A11y/index.tsx +++ b/examples/react/src/examples/A11y/index.tsx @@ -24,6 +24,10 @@ const initialNodes: Node[] = [ data: { label: 'A11y Node 1' }, position: { x: 250, y: 5 }, className: 'light', + domAttributes: { + tabIndex: 10, + 'aria-roledescription': 'A11y Node', + }, }, { id: '2', diff --git a/packages/react/src/components/EdgeWrapper/index.tsx b/packages/react/src/components/EdgeWrapper/index.tsx index a5db4441..e1b5ba99 100644 --- a/packages/react/src/components/EdgeWrapper/index.tsx +++ b/packages/react/src/components/EdgeWrapper/index.tsx @@ -199,7 +199,7 @@ export function EdgeWrapper({ onKeyDown={isFocusable ? onKeyDown : undefined} tabIndex={isFocusable ? 0 : undefined} role={edge.ariaRole ?? (isFocusable ? 'group' : 'img')} - aria-roledescription={edge.ariaRoleDescription || 'edge'} + aria-roledescription={edge.domAttributes?.['aria-roledescription'] || 'edge'} data-id={id} data-testid={`rf__edge-${id}`} aria-label={ @@ -207,6 +207,7 @@ export function EdgeWrapper({ } aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined} ref={edgeRef} + {...edge.domAttributes} > {!reconnecting && ( ({ onKeyDown={isFocusable ? onKeyDown : undefined} tabIndex={isFocusable ? 0 : undefined} role={node.ariaRole ?? (isFocusable ? 'group' : undefined)} - aria-roledescription={node.ariaRoleDescription || 'node'} + aria-roledescription={node.domAttributes?.['aria-roledescription'] || 'node'} aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`} aria-label={node.ariaLabel} + {...node.domAttributes} > , 'id' | 'style' | 'className' | 'role' | 'aria-label'>; }; type SmoothStepEdge = Record> = Edge< diff --git a/packages/react/src/types/nodes.ts b/packages/react/src/types/nodes.ts index d9e2671b..1e6eb80f 100644 --- a/packages/react/src/types/nodes.ts +++ b/packages/react/src/types/nodes.ts @@ -1,4 +1,4 @@ -import type { CSSProperties, MouseEvent as ReactMouseEvent, AriaRole } from 'react'; +import type { CSSProperties, MouseEvent as ReactMouseEvent, AriaRole, HTMLAttributes, DOMAttributes } from 'react'; import type { CoordinateExtent, NodeBase, OnError, NodeProps as NodePropsBase, InternalNodeBase } from '@xyflow/system'; import { NodeTypes } from './general'; @@ -22,8 +22,15 @@ export type Node< * The ARIA role attribute for the node element, used for accessibility. * @default "group" */ - ariaRole?: AriaRole; + + /** + * General escape hatch for adding custom attributes to the node's DOM element. + */ + domAttributes?: Omit< + HTMLAttributes, + 'id' | 'style' | 'className' | 'draggable' | 'role' | 'aria-label' | keyof DOMAttributes + >; }; /** diff --git a/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte b/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte index c63d85e5..33aa1841 100644 --- a/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte +++ b/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte @@ -138,9 +138,10 @@ : `Edge from ${source} to ${target}`} aria-describedby={focusable ? `${ARIA_EDGE_DESC_KEY}-${store.flowId}` : undefined} role={edge.ariaRole ?? (focusable ? 'group' : 'img')} - aria-roledescription={edge.ariaRoleDescription || 'edge'} + aria-roledescription={edge.domAttributes?.['aria-roledescription'] || 'edge'} onkeydown={focusable ? onkeydown : undefined} tabindex={focusable ? 0 : undefined} + {...edge.domAttributes} > ['role']; + /** + * General escape hatch for adding custom attributes to the edge's DOM element. + */ + domAttributes?: Omit< + SVGAttributes, + 'id' | 'style' | 'class' | 'role' | 'aria-label' + >; }; export type BaseEdgeProps = Pick< diff --git a/packages/svelte/src/lib/types/nodes.ts b/packages/svelte/src/lib/types/nodes.ts index f7f65469..69f2063f 100644 --- a/packages/svelte/src/lib/types/nodes.ts +++ b/packages/svelte/src/lib/types/nodes.ts @@ -1,5 +1,5 @@ import type { Component } from 'svelte'; -import type { ClassValue, HTMLAttributes } from 'svelte/elements'; +import type { ClassValue, HTMLAttributes, DOMAttributes } from 'svelte/elements'; import type { InternalNodeBase, NodeBase, NodeProps as NodePropsBase } from '@xyflow/system'; /** @@ -25,7 +25,21 @@ export type Node< * The ARIA role attribute for the node element, used for accessibility. * @default "group" */ - ariaRole?: HTMLAttributes['role']; + ariaRole?: HTMLAttributes['role']; + + /** + * General escape hatch for adding custom attributes to the node's DOM element. + */ + domAttributes?: Omit< + HTMLAttributes, + | 'id' + | 'style' + | 'class' + | 'draggable' + | 'role' + | 'aria-label' + | keyof DOMAttributes + >; }; // @todo: currently generics for nodes are not really supported diff --git a/packages/system/src/types/edges.ts b/packages/system/src/types/edges.ts index 3e69bf4c..c0568200 100644 --- a/packages/system/src/types/edges.ts +++ b/packages/system/src/types/edges.ts @@ -40,11 +40,6 @@ export type EdgeBase< * This property sets the width of that invisible path. */ interactionWidth?: number; - /** - * A description of the edge's, used for accessibility. - * @default "edge" - */ - ariaRoleDescription?: string; }; export type SmoothStepPathOptions = { diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index 49012024..687f81ba 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -73,11 +73,6 @@ export type NodeBase< */ origin?: NodeOrigin; handles?: NodeHandle[]; - /** - * A description of the node's role, used for accessibility. - * @default "node" - */ - ariaRoleDescription?: string; measured?: { width?: number; height?: number;