Merge pull request #5299 from xyflow/4264-accessibility-support-aria-pressed-attributes

feat: add `ariaRole` prop to nodes
This commit is contained in:
Moritz Klack
2025-06-03 20:58:36 +02:00
committed by GitHub
13 changed files with 67 additions and 19 deletions

View File

@@ -0,0 +1,7 @@
---
"@xyflow/react": minor
"@xyflow/svelte": minor
"@xyflow/system": patch
---
Add `ariaRole` prop to nodes and edges

View File

@@ -136,28 +136,28 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
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<EdgeType extends Edge = Edge>({
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={

View File

@@ -154,7 +154,6 @@ export function NodeWrapper<NodeType extends Node>({
});
}
};
return (
<div
className={cc([
@@ -192,7 +191,8 @@ export function NodeWrapper<NodeType extends Node>({
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}
>

View File

@@ -170,6 +170,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
ref={ref}
className={cc(['react-flow', className, colorModeClassName])}
id={id}
role="application"
>
<Wrapper
nodes={nodes}

View File

@@ -1,6 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { CSSProperties, SVGAttributes, ReactNode, MouseEvent as ReactMouseEvent, ComponentType } from 'react';
import type {
CSSProperties,
SVGAttributes,
ReactNode,
MouseEvent as ReactMouseEvent,
ComponentType,
AriaRole,
} from 'react';
import type {
EdgeBase,
BezierPathOptions,
@@ -57,6 +64,11 @@ export type Edge<
*/
reconnectable?: boolean | HandleType;
focusable?: boolean;
/**
* The ARIA role attribute for the edge, used for accessibility.
* @default "group"
*/
ariaRole?: AriaRole;
};
type SmoothStepEdge<EdgeData extends Record<string, unknown> = Record<string, unknown>> = Edge<

View File

@@ -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;
};
/**

View File

@@ -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);

View File

@@ -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}
>

View File

@@ -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}`}

View File

@@ -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<HTMLElement>['role'];
};
export type BaseEdgeProps = Pick<

View File

@@ -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<HTMLElement>['role'];
};
// @todo: currently generics for nodes are not really supported

View File

@@ -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 = {

View File

@@ -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;