diff --git a/.changeset/dry-pianos-fail.md b/.changeset/dry-pianos-fail.md new file mode 100644 index 00000000..d8c7acf4 --- /dev/null +++ b/.changeset/dry-pianos-fail.md @@ -0,0 +1,7 @@ +--- +"@xyflow/react": minor +"@xyflow/svelte": minor +"@xyflow/system": patch +--- + +Add `ariaRole` prop to nodes and edges diff --git a/packages/react/src/components/EdgeWrapper/index.tsx b/packages/react/src/components/EdgeWrapper/index.tsx index 72c5c2f8..a5db4441 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) => { @@ -198,7 +198,8 @@ export function EdgeWrapper({ onMouseLeave={onEdgeMouseLeave} onKeyDown={isFocusable ? onKeyDown : undefined} tabIndex={isFocusable ? 0 : undefined} - role={isFocusable ? 'button' : 'img'} + role={edge.ariaRole ?? (isFocusable ? 'group' : 'img')} + aria-roledescription={edge.ariaRoleDescription || 'edge'} data-id={id} data-testid={`rf__edge-${id}`} aria-label={ diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 5c0ce08b..11f65d42 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -154,7 +154,6 @@ export function NodeWrapper({ }); } }; - return (
({ onDoubleClick={onDoubleClickHandler} onKeyDown={isFocusable ? onKeyDown : undefined} tabIndex={isFocusable ? 0 : undefined} - role={isFocusable ? 'button' : undefined} + role={node.ariaRole ?? (isFocusable ? 'group' : undefined)} + aria-roledescription={node.ariaRoleDescription || 'node'} aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`} aria-label={node.ariaLabel} > diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index 2247a004..bd2f1334 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -170,6 +170,7 @@ function ReactFlow( ref={ref} className={cc(['react-flow', className, colorModeClassName])} id={id} + role="application" > = Record> = Edge< diff --git a/packages/react/src/types/nodes.ts b/packages/react/src/types/nodes.ts index 8dc213d7..d9e2671b 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 } from 'react'; +import type { CSSProperties, MouseEvent as ReactMouseEvent, AriaRole } from 'react'; import type { CoordinateExtent, NodeBase, OnError, NodeProps as NodePropsBase, InternalNodeBase } from '@xyflow/system'; import { NodeTypes } from './general'; @@ -18,6 +18,12 @@ export type Node< className?: string; resizing?: boolean; focusable?: boolean; + /** + * The ARIA role attribute for the node element, used for accessibility. + * @default "group" + */ + + ariaRole?: AriaRole; }; /** diff --git a/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte b/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte index 187567d7..83ce1411 100644 --- a/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte +++ b/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte @@ -36,7 +36,6 @@ style:width={toPxString(width)} style:height={toPxString(height)} style:z-index={z} - role="button" tabindex="-1" onclick={() => { if (selectEdgeOnClick && id) store.handleEdgeSelection(id); diff --git a/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte b/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte index c1c988fd..c63d85e5 100644 --- a/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte +++ b/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte @@ -137,7 +137,8 @@ ? ariaLabel : `Edge from ${source} to ${target}`} aria-describedby={focusable ? `${ARIA_EDGE_DESC_KEY}-${store.flowId}` : undefined} - role={focusable ? 'button' : 'img'} + role={edge.ariaRole ?? (focusable ? 'group' : 'img')} + aria-roledescription={edge.ariaRoleDescription || 'edge'} onkeydown={focusable ? onkeydown : undefined} tabindex={focusable ? 0 : undefined} > diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index f053cd82..e8a11de9 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -252,7 +252,8 @@ : undefined} onkeydown={focusable ? onKeyDown : undefined} tabIndex={focusable ? 0 : undefined} - role={focusable ? 'button' : undefined} + role={node.ariaRole ?? (focusable ? 'group' : undefined)} + aria-roledescription={node.ariaRoleDescription || 'node'} aria-describedby={store.disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${store.flowId}`} diff --git a/packages/svelte/src/lib/types/edges.ts b/packages/svelte/src/lib/types/edges.ts index 7ae205de..77988643 100644 --- a/packages/svelte/src/lib/types/edges.ts +++ b/packages/svelte/src/lib/types/edges.ts @@ -25,6 +25,11 @@ export type Edge< style?: string; class?: ClassValue; focusable?: boolean; + /** + * The ARIA role attribute for the edge, used for accessibility. + * @default "group" + */ + ariaRole?: HTMLAttributes['role']; }; export type BaseEdgeProps = Pick< diff --git a/packages/svelte/src/lib/types/nodes.ts b/packages/svelte/src/lib/types/nodes.ts index f861c13b..f7f65469 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 } from 'svelte/elements'; +import type { ClassValue, HTMLAttributes } from 'svelte/elements'; import type { InternalNodeBase, NodeBase, NodeProps as NodePropsBase } from '@xyflow/system'; /** @@ -21,6 +21,11 @@ export type Node< class?: ClassValue; style?: string; focusable?: boolean; + /** + * The ARIA role attribute for the node element, used for accessibility. + * @default "group" + */ + ariaRole?: HTMLAttributes['role']; }; // @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 c0568200..3e69bf4c 100644 --- a/packages/system/src/types/edges.ts +++ b/packages/system/src/types/edges.ts @@ -40,6 +40,11 @@ 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 687f81ba..49012024 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -73,6 +73,11 @@ 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;