From 9e4a9ae65dacce710f109f1eb62c6cb37aedd8c6 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Mon, 19 May 2025 13:15:26 +0200 Subject: [PATCH 01/35] #5201 chore(A11yDescriptions): add static support for localization --- .../react/src/examples/Overview/index.tsx | 5 +++ .../src/components/A11yDescriptions/index.tsx | 32 ++++++++++++++++--- .../react/src/container/ReactFlow/index.tsx | 3 +- packages/react/src/types/component-props.ts | 6 ++++ packages/system/src/types/general.ts | 6 ++++ 5 files changed, 46 insertions(+), 6 deletions(-) diff --git a/examples/react/src/examples/Overview/index.tsx b/examples/react/src/examples/Overview/index.tsx index 8dddbb0d..b1f7db61 100644 --- a/examples/react/src/examples/Overview/index.tsx +++ b/examples/react/src/examples/Overview/index.tsx @@ -235,6 +235,11 @@ const OverviewFlow = () => { onBeforeDelete={onBeforeDelete} onDelete={onDelete} onPaneMouseMove={onPaneMouseMove} + a11yMessages={{ + 'a11yDescription.node.default': 'Custom Node Desc.', + 'a11yDescription.node.keyboardDisabled': 'Custom Keyboard Desc.', + 'a11yDescription.edge.default': 'Custom Edge Desc.', + }} > diff --git a/packages/react/src/components/A11yDescriptions/index.tsx b/packages/react/src/components/A11yDescriptions/index.tsx index 51b4a939..9e9ddd2d 100644 --- a/packages/react/src/components/A11yDescriptions/index.tsx +++ b/packages/react/src/components/A11yDescriptions/index.tsx @@ -2,6 +2,7 @@ import { CSSProperties } from 'react'; import { useStore } from '../../hooks/useStore'; import type { ReactFlowState } from '../../types'; +import type { a11yMessages } from '@xyflow/system'; const style: CSSProperties = { display: 'none' }; const ariaLiveStyle: CSSProperties = { @@ -20,6 +21,15 @@ export const ARIA_NODE_DESC_KEY = 'react-flow__node-desc'; export const ARIA_EDGE_DESC_KEY = 'react-flow__edge-desc'; export const ARIA_LIVE_MESSAGE = 'react-flow__aria-live'; +const defaultA11yMessages: Required = { + 'a11yDescription.node.default': + 'Press enter or space to select a node. Press delete to remove it and escape to cancel.', + 'a11yDescription.node.keyboardDisabled': + 'Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.', + 'a11yDescription.edge.default': + 'Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.', +}; + const selector = (s: ReactFlowState) => s.ariaLiveMessage; function AriaLiveMessage({ rfId }: { rfId: string }) { @@ -32,16 +42,28 @@ function AriaLiveMessage({ rfId }: { rfId: string }) { ); } -export function A11yDescriptions({ rfId, disableKeyboardA11y }: { rfId: string; disableKeyboardA11y: boolean }) { +export function A11yDescriptions({ + rfId, + disableKeyboardA11y, + a11yMessages = {}, +}: { + rfId: string; + disableKeyboardA11y: boolean; + a11yMessages?: a11yMessages; +}) { + const nodeDesc = disableKeyboardA11y + ? a11yMessages['a11yDescription.node.default'] || defaultA11yMessages['a11yDescription.node.default'] + : a11yMessages['a11yDescription.node.keyboardDisabled'] || + defaultA11yMessages['a11yDescription.node.keyboardDisabled']; + const edgeDesc = a11yMessages['a11yDescription.edge.default'] || defaultA11yMessages['a11yDescription.edge.default']; + return ( <>
- Press enter or space to select a node. - {!disableKeyboardA11y && 'You can then use the arrow keys to move the node around.'} Press delete to remove it - and escape to cancel.{' '} + {nodeDesc}
- Press enter or space to select an edge. You can then press delete to remove it or escape to cancel. + {edgeDesc}
{!disableKeyboardA11y && } diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index 2247a004..6d8b3bb5 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -145,6 +145,7 @@ function ReactFlow( colorMode = 'light', debug, onScroll, + a11yMessages, ...rest }: ReactFlowProps, ref: ForwardedRef @@ -309,7 +310,7 @@ function ReactFlow( onSelectionChange={onSelectionChange} /> {children} - + ); diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index 9bea9004..af1dafa9 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -21,6 +21,7 @@ import type { ColorMode, SnapGrid, OnReconnect, + a11yMessages, } from '@xyflow/system'; import type { @@ -666,4 +667,9 @@ export interface ReactFlowProps Promise; + +export type a11yMessages = { + 'a11yDescription.node.default'?: string; + 'a11yDescription.node.keyboardDisabled'?: string; + 'a11yDescription.edge.default'?: string; +}; From 36f1d94ca75895c4321190bb208e3bc891f70d1e Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Mon, 19 May 2025 15:16:19 +0200 Subject: [PATCH 02/35] chore(a11y): pass a11y descriptions in svelte --- .../src/routes/examples/overview/Flow.svelte | 5 ++++ .../A11yDescriptions/A11yDescriptions.svelte | 27 +++++++++++++------ .../container/SvelteFlow/SvelteFlow.svelte | 3 ++- .../src/lib/container/SvelteFlow/types.ts | 8 +++++- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/examples/svelte/src/routes/examples/overview/Flow.svelte b/examples/svelte/src/routes/examples/overview/Flow.svelte index 3075a0e8..b499fc47 100644 --- a/examples/svelte/src/routes/examples/overview/Flow.svelte +++ b/examples/svelte/src/routes/examples/overview/Flow.svelte @@ -220,6 +220,11 @@ console.log('on selection changed via prop', { nodes, edges }); }} selectNodesOnDrag + a11yMessages={{ + 'a11yDescription.node.default': 'Svelte Custom Node Description.', + 'a11yDescription.node.keyboardDisabled': 'Svelte Custom Keyboard Description', + 'a11yDescription.edge.default': 'Svelte Custom Edge Desc.', + }} > {#snippet before()} diff --git a/packages/svelte/src/lib/components/A11yDescriptions/A11yDescriptions.svelte b/packages/svelte/src/lib/components/A11yDescriptions/A11yDescriptions.svelte index a26778c1..f0ff3809 100644 --- a/packages/svelte/src/lib/components/A11yDescriptions/A11yDescriptions.svelte +++ b/packages/svelte/src/lib/components/A11yDescriptions/A11yDescriptions.svelte @@ -1,21 +1,32 @@ {#if !store.disableKeyboardA11y} diff --git a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte index 9b039558..919a345e 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte @@ -70,6 +70,7 @@ nodes = $bindable([]), edges = $bindable([]), viewport = $bindable(undefined), + a11yMessages, ...props }: SvelteFlowProps & Omit, 'onselectionchange'> = $props(); @@ -217,6 +218,6 @@ - + {@render children?.()} diff --git a/packages/svelte/src/lib/container/SvelteFlow/types.ts b/packages/svelte/src/lib/container/SvelteFlow/types.ts index 3df0a991..85bb63df 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/types.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/types.ts @@ -20,7 +20,8 @@ import type { OnConnectEnd, OnReconnect, OnReconnectStart, - OnReconnectEnd + OnReconnectEnd, + a11yMessages } from '@xyflow/system'; import type { @@ -471,4 +472,9 @@ export type SvelteFlowProps< onselectionstart?: (event: PointerEvent) => void; /** This event handler gets called when the user finishes dragging a selection box */ onselectionend?: (event: PointerEvent) => void; + /** + * Custom accessibility messages for screen readers and a11y features. + * Allows localization and customization of ARIA descriptions. + */ + a11yMessages?: a11yMessages; }; From 07265ed2772a8602fe27450fed7f9efce45680d5 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Tue, 20 May 2025 14:40:28 +0200 Subject: [PATCH 03/35] chore(a11y): rename props, fix linting error --- examples/react/src/examples/Overview/index.tsx | 2 +- .../src/routes/examples/overview/Flow.svelte | 2 +- .../src/components/A11yDescriptions/index.tsx | 16 ++++++++-------- packages/react/src/container/ReactFlow/index.tsx | 4 ++-- packages/react/src/types/component-props.ts | 4 ++-- .../A11yDescriptions/A11yDescriptions.svelte | 14 +++++++------- .../lib/container/SvelteFlow/SvelteFlow.svelte | 4 ++-- .../src/lib/container/SvelteFlow/Wrapper.svelte | 1 + .../svelte/src/lib/container/SvelteFlow/types.ts | 4 ++-- packages/system/src/types/general.ts | 2 +- 10 files changed, 27 insertions(+), 26 deletions(-) diff --git a/examples/react/src/examples/Overview/index.tsx b/examples/react/src/examples/Overview/index.tsx index b1f7db61..a5e056b2 100644 --- a/examples/react/src/examples/Overview/index.tsx +++ b/examples/react/src/examples/Overview/index.tsx @@ -235,7 +235,7 @@ const OverviewFlow = () => { onBeforeDelete={onBeforeDelete} onDelete={onDelete} onPaneMouseMove={onPaneMouseMove} - a11yMessages={{ + descriptions={{ 'a11yDescription.node.default': 'Custom Node Desc.', 'a11yDescription.node.keyboardDisabled': 'Custom Keyboard Desc.', 'a11yDescription.edge.default': 'Custom Edge Desc.', diff --git a/examples/svelte/src/routes/examples/overview/Flow.svelte b/examples/svelte/src/routes/examples/overview/Flow.svelte index b499fc47..5364b37d 100644 --- a/examples/svelte/src/routes/examples/overview/Flow.svelte +++ b/examples/svelte/src/routes/examples/overview/Flow.svelte @@ -220,7 +220,7 @@ console.log('on selection changed via prop', { nodes, edges }); }} selectNodesOnDrag - a11yMessages={{ + descriptions={{ 'a11yDescription.node.default': 'Svelte Custom Node Description.', 'a11yDescription.node.keyboardDisabled': 'Svelte Custom Keyboard Description', 'a11yDescription.edge.default': 'Svelte Custom Edge Desc.', diff --git a/packages/react/src/components/A11yDescriptions/index.tsx b/packages/react/src/components/A11yDescriptions/index.tsx index 9e9ddd2d..eaa80945 100644 --- a/packages/react/src/components/A11yDescriptions/index.tsx +++ b/packages/react/src/components/A11yDescriptions/index.tsx @@ -2,7 +2,7 @@ import { CSSProperties } from 'react'; import { useStore } from '../../hooks/useStore'; import type { ReactFlowState } from '../../types'; -import type { a11yMessages } from '@xyflow/system'; +import type { descriptions } from '@xyflow/system'; const style: CSSProperties = { display: 'none' }; const ariaLiveStyle: CSSProperties = { @@ -21,7 +21,7 @@ export const ARIA_NODE_DESC_KEY = 'react-flow__node-desc'; export const ARIA_EDGE_DESC_KEY = 'react-flow__edge-desc'; export const ARIA_LIVE_MESSAGE = 'react-flow__aria-live'; -const defaultA11yMessages: Required = { +const defaultDescriptions: Required = { 'a11yDescription.node.default': 'Press enter or space to select a node. Press delete to remove it and escape to cancel.', 'a11yDescription.node.keyboardDisabled': @@ -45,17 +45,17 @@ function AriaLiveMessage({ rfId }: { rfId: string }) { export function A11yDescriptions({ rfId, disableKeyboardA11y, - a11yMessages = {}, + descriptions = {}, }: { rfId: string; disableKeyboardA11y: boolean; - a11yMessages?: a11yMessages; + descriptions?: descriptions; }) { const nodeDesc = disableKeyboardA11y - ? a11yMessages['a11yDescription.node.default'] || defaultA11yMessages['a11yDescription.node.default'] - : a11yMessages['a11yDescription.node.keyboardDisabled'] || - defaultA11yMessages['a11yDescription.node.keyboardDisabled']; - const edgeDesc = a11yMessages['a11yDescription.edge.default'] || defaultA11yMessages['a11yDescription.edge.default']; + ? descriptions['a11yDescription.node.default'] || defaultDescriptions['a11yDescription.node.default'] + : descriptions['a11yDescription.node.keyboardDisabled'] || + defaultDescriptions['a11yDescription.node.keyboardDisabled']; + const edgeDesc = descriptions['a11yDescription.edge.default'] || defaultDescriptions['a11yDescription.edge.default']; return ( <> diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index 6d8b3bb5..c351f302 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -145,7 +145,7 @@ function ReactFlow( colorMode = 'light', debug, onScroll, - a11yMessages, + descriptions, ...rest }: ReactFlowProps, ref: ForwardedRef @@ -310,7 +310,7 @@ function ReactFlow( onSelectionChange={onSelectionChange} /> {children} - + ); diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index af1dafa9..21f69dea 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -21,7 +21,7 @@ import type { ColorMode, SnapGrid, OnReconnect, - a11yMessages, + descriptions, } from '@xyflow/system'; import type { @@ -671,5 +671,5 @@ export interface ReactFlowProps import type { SvelteFlowStore } from '$lib/store/types'; - import type { a11yMessages } from '@xyflow/system'; + import type { descriptions as DescriptionsType } from '@xyflow/system'; // Rename the imported type import type { Node, Edge } from '$lib/types'; import { ARIA_EDGE_DESC_KEY, ARIA_LIVE_MESSAGE, ARIA_NODE_DESC_KEY } from '.'; - const { store, a11yMessages = {} }: { + const { store, descriptions = {} }: { store: SvelteFlowStore; - a11yMessages?: Partial; + descriptions?: Partial; } = $props(); -const defaultA11yMessages = { +const defaultDescriptions = { 'a11yDescription.node.default': 'Press enter or space to select a node. Press delete to remove it and escape to cancel.', 'a11yDescription.node.keyboardDisabled': @@ -22,11 +22,11 @@ const defaultA11yMessages = { {#if !store.disableKeyboardA11y} diff --git a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte index 919a345e..78b7112e 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte @@ -70,7 +70,7 @@ nodes = $bindable([]), edges = $bindable([]), viewport = $bindable(undefined), - a11yMessages, + descriptions, ...props }: SvelteFlowProps & Omit, 'onselectionchange'> = $props(); @@ -218,6 +218,6 @@ - + {@render children?.()} diff --git a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte index b1353a42..b492f0d3 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte @@ -92,6 +92,7 @@ noDragClass, noPanClass, noWheelClass, + descriptions, ...divAttributes } = $derived(rest); /* eslint-enable @typescript-eslint/no-unused-vars */ diff --git a/packages/svelte/src/lib/container/SvelteFlow/types.ts b/packages/svelte/src/lib/container/SvelteFlow/types.ts index 85bb63df..25ae6e10 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/types.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/types.ts @@ -21,7 +21,7 @@ import type { OnReconnect, OnReconnectStart, OnReconnectEnd, - a11yMessages + descriptions } from '@xyflow/system'; import type { @@ -476,5 +476,5 @@ export type SvelteFlowProps< * Custom accessibility messages for screen readers and a11y features. * Allows localization and customization of ARIA descriptions. */ - a11yMessages?: a11yMessages; + descriptions?: descriptions; }; diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 5aff115d..fec535ef 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -314,7 +314,7 @@ export type OnBeforeDeleteBase Promise; -export type a11yMessages = { +export type descriptions = { 'a11yDescription.node.default'?: string; 'a11yDescription.node.keyboardDisabled'?: string; 'a11yDescription.edge.default'?: string; From ee8709b5fb0028793dd03d936416c6d540f37d00 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Tue, 20 May 2025 17:20:46 +0200 Subject: [PATCH 04/35] chore: update LabelConfig name, add to store, create a11y example --- examples/react/src/App/routes.ts | 6 ++ examples/react/src/examples/A11y/index.tsx | 101 ++++++++++++++++++ .../react/src/examples/Overview/index.tsx | 5 - .../src/routes/examples/overview/Flow.svelte | 2 +- .../src/components/A11yDescriptions/index.tsx | 34 ++---- .../react/src/container/ReactFlow/index.tsx | 4 +- packages/react/src/store/index.ts | 6 ++ packages/react/src/store/initialState.ts | 2 + packages/react/src/types/component-props.ts | 4 +- packages/react/src/types/store.ts | 4 +- packages/system/src/constants.ts | 9 ++ packages/system/src/types/general.ts | 2 +- 12 files changed, 142 insertions(+), 37 deletions(-) create mode 100644 examples/react/src/examples/A11y/index.tsx diff --git a/examples/react/src/App/routes.ts b/examples/react/src/App/routes.ts index 2256ce52..7bbe7890 100644 --- a/examples/react/src/App/routes.ts +++ b/examples/react/src/App/routes.ts @@ -1,3 +1,4 @@ +import A11y from '../examples/A11y'; import Basic from '../examples/Basic'; import Backgrounds from '../examples/Backgrounds'; import BrokenNodes from '../examples/BrokenNodes'; @@ -68,6 +69,11 @@ const routes: IRoute[] = [ path: 'add-node-edge-drop', component: AddNodeOnEdgeDrop, }, + { + name: 'A11y', + path: 'a11y', + component: A11y, + }, { name: 'Basic', path: 'basic', diff --git a/examples/react/src/examples/A11y/index.tsx b/examples/react/src/examples/A11y/index.tsx new file mode 100644 index 00000000..7e68910d --- /dev/null +++ b/examples/react/src/examples/A11y/index.tsx @@ -0,0 +1,101 @@ +import { MouseEvent, useMemo, useEffect } from 'react'; +import { + ReactFlow, + MiniMap, + Background, + BackgroundVariant, + Controls, + ReactFlowProvider, + Node, + Edge, + OnNodeDrag, + FitViewOptions, + useStore, +} from '@xyflow/react'; + +const onNodeDrag: OnNodeDrag = (_, node: Node, nodes: Node[]) => console.log('drag', node, nodes); +const onNodeDragStart = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes); +const onNodeDragStop = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag stop', node, nodes); +const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); + +const initialNodes: Node[] = [ + { + id: '1', + type: 'input', + data: { label: 'A11y Node 1' }, + position: { x: 250, y: 5 }, + className: 'light', + }, + { + id: '2', + data: { label: 'Node 2' }, + position: { x: 100, y: 100 }, + className: 'light', + }, + { + id: '3', + data: { label: 'Node 3' }, + position: { x: 400, y: 100 }, + className: 'light', + }, +]; + +const initialEdges: Edge[] = [ + { id: 'e1-2', source: '1', target: '2', animated: true }, + { id: 'e1-3', source: '1', target: '3' }, +]; + +const A11y = () => { + const labelConfig = useStore((state) => state.labelConfig); + const updateLabelConfig = useStore((state) => state.updateLabelConfig); + + useEffect(() => { + console.log('Label Config:', labelConfig); + }, [labelConfig]); + + useEffect(() => { + console.log('Updating labelConfig...'); + updateLabelConfig({ + 'a11yDescription.node.default': 'Updated Node Desc.', + 'a11yDescription.node.keyboardDisabled': 'Updated Keyboard Desc.', + 'a11yDescription.edge.default': 'Updated Edge Desc.', + }); + }, [updateLabelConfig]); + + return ( + + + + + + ); +}; + +export default function App() { + return ( + + + + ); +} diff --git a/examples/react/src/examples/Overview/index.tsx b/examples/react/src/examples/Overview/index.tsx index a5e056b2..8dddbb0d 100644 --- a/examples/react/src/examples/Overview/index.tsx +++ b/examples/react/src/examples/Overview/index.tsx @@ -235,11 +235,6 @@ const OverviewFlow = () => { onBeforeDelete={onBeforeDelete} onDelete={onDelete} onPaneMouseMove={onPaneMouseMove} - descriptions={{ - 'a11yDescription.node.default': 'Custom Node Desc.', - 'a11yDescription.node.keyboardDisabled': 'Custom Keyboard Desc.', - 'a11yDescription.edge.default': 'Custom Edge Desc.', - }} > diff --git a/examples/svelte/src/routes/examples/overview/Flow.svelte b/examples/svelte/src/routes/examples/overview/Flow.svelte index 5364b37d..7c752462 100644 --- a/examples/svelte/src/routes/examples/overview/Flow.svelte +++ b/examples/svelte/src/routes/examples/overview/Flow.svelte @@ -220,7 +220,7 @@ console.log('on selection changed via prop', { nodes, edges }); }} selectNodesOnDrag - descriptions={{ + labelConfig={{ 'a11yDescription.node.default': 'Svelte Custom Node Description.', 'a11yDescription.node.keyboardDisabled': 'Svelte Custom Keyboard Description', 'a11yDescription.edge.default': 'Svelte Custom Edge Desc.', diff --git a/packages/react/src/components/A11yDescriptions/index.tsx b/packages/react/src/components/A11yDescriptions/index.tsx index eaa80945..4931ada0 100644 --- a/packages/react/src/components/A11yDescriptions/index.tsx +++ b/packages/react/src/components/A11yDescriptions/index.tsx @@ -2,7 +2,6 @@ import { CSSProperties } from 'react'; import { useStore } from '../../hooks/useStore'; import type { ReactFlowState } from '../../types'; -import type { descriptions } from '@xyflow/system'; const style: CSSProperties = { display: 'none' }; const ariaLiveStyle: CSSProperties = { @@ -21,19 +20,11 @@ export const ARIA_NODE_DESC_KEY = 'react-flow__node-desc'; export const ARIA_EDGE_DESC_KEY = 'react-flow__edge-desc'; export const ARIA_LIVE_MESSAGE = 'react-flow__aria-live'; -const defaultDescriptions: Required = { - 'a11yDescription.node.default': - 'Press enter or space to select a node. Press delete to remove it and escape to cancel.', - 'a11yDescription.node.keyboardDisabled': - 'Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.', - 'a11yDescription.edge.default': - 'Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.', -}; - -const selector = (s: ReactFlowState) => s.ariaLiveMessage; +const ariaLiveSelector = (s: ReactFlowState) => s.ariaLiveMessage; +const labelConfigSelector = (s: ReactFlowState) => s.labelConfig; function AriaLiveMessage({ rfId }: { rfId: string }) { - const ariaLiveMessage = useStore(selector); + const ariaLiveMessage = useStore(ariaLiveSelector); return (
@@ -42,20 +33,13 @@ function AriaLiveMessage({ rfId }: { rfId: string }) { ); } -export function A11yDescriptions({ - rfId, - disableKeyboardA11y, - descriptions = {}, -}: { - rfId: string; - disableKeyboardA11y: boolean; - descriptions?: descriptions; -}) { +export function A11yDescriptions({ rfId, disableKeyboardA11y }: { rfId: string; disableKeyboardA11y: boolean }) { + const labelConfig = useStore(labelConfigSelector); + const nodeDesc = disableKeyboardA11y - ? descriptions['a11yDescription.node.default'] || defaultDescriptions['a11yDescription.node.default'] - : descriptions['a11yDescription.node.keyboardDisabled'] || - defaultDescriptions['a11yDescription.node.keyboardDisabled']; - const edgeDesc = descriptions['a11yDescription.edge.default'] || defaultDescriptions['a11yDescription.edge.default']; + ? labelConfig['a11yDescription.node.default'] + : labelConfig['a11yDescription.node.keyboardDisabled']; + const edgeDesc = labelConfig['a11yDescription.edge.default']; return ( <> diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index c351f302..3eba7e34 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -145,7 +145,7 @@ function ReactFlow( colorMode = 'light', debug, onScroll, - descriptions, + labelConfig, ...rest }: ReactFlowProps, ref: ForwardedRef @@ -310,7 +310,7 @@ function ReactFlow( onSelectionChange={onSelectionChange} /> {children} - +
); diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 39702bd0..8365400d 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -14,6 +14,7 @@ import { NodeOrigin, CoordinateExtent, fitViewport, + LabelConfig, } from '@xyflow/system'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; @@ -370,6 +371,11 @@ const createStore = ({ }, reset: () => set({ ...getInitialState() }), + updateLabelConfig: (newLabelConfig: Partial) => { + set((state) => ({ + labelConfig: { ...state.labelConfig, ...newLabelConfig }, + })); + }, }; }, Object.is); diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 34507efd..186f1eca 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -10,6 +10,7 @@ import { NodeOrigin, initialConnection, CoordinateExtent, + defaultLabelConfig, } from '@xyflow/system'; import type { Edge, FitViewOptions, InternalNode, Node, ReactFlowStore } from '../types'; @@ -141,6 +142,7 @@ const getInitialState = ({ lib: 'react', debug: false, + labelConfig: defaultLabelConfig, }; }; diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index 21f69dea..0206e102 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -21,7 +21,7 @@ import type { ColorMode, SnapGrid, OnReconnect, - descriptions, + LabelConfig, } from '@xyflow/system'; import type { @@ -671,5 +671,5 @@ export interface ReactFlowProps; }; export type ReactFlowActions = { @@ -171,6 +172,7 @@ export type ReactFlowActions = { triggerEdgeChanges: (changes: EdgeChange[]) => void; panBy: PanBy; setPaneClickDistance: (distance: number) => void; + updateLabelConfig: (newLabelConfig: Partial) => void; }; export type ReactFlowState = ReactFlowStore< diff --git a/packages/system/src/constants.ts b/packages/system/src/constants.ts index 9bb65656..10029975 100644 --- a/packages/system/src/constants.ts +++ b/packages/system/src/constants.ts @@ -36,3 +36,12 @@ export const infiniteExtent: CoordinateExtent = [ ]; export const elementSelectionKeys = ['Enter', ' ', 'Escape']; + +export const defaultLabelConfig = { + 'a11yDescription.node.default': + 'Press enter or space to select a node. Press delete to remove it and escape to cancel.', + 'a11yDescription.node.keyboardDisabled': + 'Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.', + 'a11yDescription.edge.default': + 'Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.', +}; diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index fec535ef..75d51c78 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -314,7 +314,7 @@ export type OnBeforeDeleteBase Promise; -export type descriptions = { +export type LabelConfig = { 'a11yDescription.node.default'?: string; 'a11yDescription.node.keyboardDisabled'?: string; 'a11yDescription.edge.default'?: string; From 54fc3c071726b153f9a1282389b32b9bf52a1f61 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Wed, 21 May 2025 12:21:59 +0200 Subject: [PATCH 05/35] chore(a11y): pass labelConfig state through store updater --- examples/react/src/examples/A11y/index.tsx | 20 +------------------ .../src/components/StoreUpdater/index.tsx | 1 + .../react/src/container/ReactFlow/index.tsx | 1 + packages/react/src/store/index.ts | 5 ----- packages/react/src/types/store.ts | 3 +-- packages/system/src/constants.ts | 2 ++ packages/system/src/types/general.ts | 6 ------ 7 files changed, 6 insertions(+), 32 deletions(-) diff --git a/examples/react/src/examples/A11y/index.tsx b/examples/react/src/examples/A11y/index.tsx index 7e68910d..61627a8f 100644 --- a/examples/react/src/examples/A11y/index.tsx +++ b/examples/react/src/examples/A11y/index.tsx @@ -1,4 +1,4 @@ -import { MouseEvent, useMemo, useEffect } from 'react'; +import { MouseEvent } from 'react'; import { ReactFlow, MiniMap, @@ -9,8 +9,6 @@ import { Node, Edge, OnNodeDrag, - FitViewOptions, - useStore, } from '@xyflow/react'; const onNodeDrag: OnNodeDrag = (_, node: Node, nodes: Node[]) => console.log('drag', node, nodes); @@ -46,22 +44,6 @@ const initialEdges: Edge[] = [ ]; const A11y = () => { - const labelConfig = useStore((state) => state.labelConfig); - const updateLabelConfig = useStore((state) => state.updateLabelConfig); - - useEffect(() => { - console.log('Label Config:', labelConfig); - }, [labelConfig]); - - useEffect(() => { - console.log('Updating labelConfig...'); - updateLabelConfig({ - 'a11yDescription.node.default': 'Updated Node Desc.', - 'a11yDescription.node.keyboardDisabled': 'Updated Keyboard Desc.', - 'a11yDescription.edge.default': 'Updated Edge Desc.', - }); - }, [updateLabelConfig]); - return ( ( onBeforeDelete={onBeforeDelete} paneClickDistance={paneClickDistance} debug={debug} + labelConfig={labelConfig} /> onSelectionChange={onSelectionChange} /> {children} diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 8365400d..29111bb3 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -371,11 +371,6 @@ const createStore = ({ }, reset: () => set({ ...getInitialState() }), - updateLabelConfig: (newLabelConfig: Partial) => { - set((state) => ({ - labelConfig: { ...state.labelConfig, ...newLabelConfig }, - })); - }, }; }, Object.is); diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 11ab53b5..5b3dec5b 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -148,7 +148,7 @@ export type ReactFlowStore; + labelConfig: LabelConfig; }; export type ReactFlowActions = { @@ -172,7 +172,6 @@ export type ReactFlowActions = { triggerEdgeChanges: (changes: EdgeChange[]) => void; panBy: PanBy; setPaneClickDistance: (distance: number) => void; - updateLabelConfig: (newLabelConfig: Partial) => void; }; export type ReactFlowState = ReactFlowStore< diff --git a/packages/system/src/constants.ts b/packages/system/src/constants.ts index 10029975..b3ff1d41 100644 --- a/packages/system/src/constants.ts +++ b/packages/system/src/constants.ts @@ -45,3 +45,5 @@ export const defaultLabelConfig = { 'a11yDescription.edge.default': 'Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.', }; + +export type LabelConfig = Partial; diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 75d51c78..d15a4b54 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -313,9 +313,3 @@ export type OnBeforeDeleteBase Promise; - -export type LabelConfig = { - 'a11yDescription.node.default'?: string; - 'a11yDescription.node.keyboardDisabled'?: string; - 'a11yDescription.edge.default'?: string; -}; From 91ecfe73ccd18dbee13cf9e1b34b6e4ab60d3970 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Wed, 21 May 2025 14:03:50 +0200 Subject: [PATCH 06/35] chore(a11y): pass LabelConfig values through store SvelteFlow --- .../src/components/Header/Header.svelte | 1 + .../src/routes/examples/a11y/+page.svelte | 40 +++++++++++++++++++ .../src/routes/examples/overview/Flow.svelte | 5 --- packages/react/src/store/index.ts | 1 - .../A11yDescriptions/A11yDescriptions.svelte | 28 +++++-------- .../container/SvelteFlow/SvelteFlow.svelte | 3 +- .../lib/container/SvelteFlow/Wrapper.svelte | 2 +- .../src/lib/container/SvelteFlow/types.ts | 4 +- .../src/lib/store/initial-store.svelte.ts | 5 ++- 9 files changed, 58 insertions(+), 31 deletions(-) create mode 100644 examples/svelte/src/routes/examples/a11y/+page.svelte diff --git a/examples/svelte/src/components/Header/Header.svelte b/examples/svelte/src/components/Header/Header.svelte index 1c251c67..f6260917 100644 --- a/examples/svelte/src/components/Header/Header.svelte +++ b/examples/svelte/src/components/Header/Header.svelte @@ -3,6 +3,7 @@ import { page } from '$app/stores'; const routes = [ + 'a11y', 'add-node-on-drop', 'color-mode', 'custom-connection-line', diff --git a/examples/svelte/src/routes/examples/a11y/+page.svelte b/examples/svelte/src/routes/examples/a11y/+page.svelte new file mode 100644 index 00000000..395a09c6 --- /dev/null +++ b/examples/svelte/src/routes/examples/a11y/+page.svelte @@ -0,0 +1,40 @@ + + + + + + + diff --git a/examples/svelte/src/routes/examples/overview/Flow.svelte b/examples/svelte/src/routes/examples/overview/Flow.svelte index 7c752462..3075a0e8 100644 --- a/examples/svelte/src/routes/examples/overview/Flow.svelte +++ b/examples/svelte/src/routes/examples/overview/Flow.svelte @@ -220,11 +220,6 @@ console.log('on selection changed via prop', { nodes, edges }); }} selectNodesOnDrag - labelConfig={{ - 'a11yDescription.node.default': 'Svelte Custom Node Description.', - 'a11yDescription.node.keyboardDisabled': 'Svelte Custom Keyboard Description', - 'a11yDescription.edge.default': 'Svelte Custom Edge Desc.', - }} > {#snippet before()} diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 29111bb3..39702bd0 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -14,7 +14,6 @@ import { NodeOrigin, CoordinateExtent, fitViewport, - LabelConfig, } from '@xyflow/system'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; diff --git a/packages/svelte/src/lib/components/A11yDescriptions/A11yDescriptions.svelte b/packages/svelte/src/lib/components/A11yDescriptions/A11yDescriptions.svelte index 09cd7b74..0383629e 100644 --- a/packages/svelte/src/lib/components/A11yDescriptions/A11yDescriptions.svelte +++ b/packages/svelte/src/lib/components/A11yDescriptions/A11yDescriptions.svelte @@ -1,32 +1,22 @@ {#if !store.disableKeyboardA11y} @@ -38,4 +28,4 @@ const defaultDescriptions = { > {store.ariaLiveMessage} -{/if} +{/if} \ No newline at end of file diff --git a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte index 78b7112e..9b039558 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte @@ -70,7 +70,6 @@ nodes = $bindable([]), edges = $bindable([]), viewport = $bindable(undefined), - descriptions, ...props }: SvelteFlowProps & Omit, 'onselectionchange'> = $props(); @@ -218,6 +217,6 @@ - + {@render children?.()} diff --git a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte index b492f0d3..3734cb25 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte @@ -92,7 +92,7 @@ noDragClass, noPanClass, noWheelClass, - descriptions, + labelConfig, ...divAttributes } = $derived(rest); /* eslint-enable @typescript-eslint/no-unused-vars */ diff --git a/packages/svelte/src/lib/container/SvelteFlow/types.ts b/packages/svelte/src/lib/container/SvelteFlow/types.ts index 25ae6e10..749f2fb8 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/types.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/types.ts @@ -21,7 +21,7 @@ import type { OnReconnect, OnReconnectStart, OnReconnectEnd, - descriptions + LabelConfig } from '@xyflow/system'; import type { @@ -476,5 +476,5 @@ export type SvelteFlowProps< * Custom accessibility messages for screen readers and a11y features. * Allows localization and customization of ARIA descriptions. */ - descriptions?: descriptions; + labelConfig?: LabelConfig; }; diff --git a/packages/svelte/src/lib/store/initial-store.svelte.ts b/packages/svelte/src/lib/store/initial-store.svelte.ts index 8ba4efa4..99f4c95b 100644 --- a/packages/svelte/src/lib/store/initial-store.svelte.ts +++ b/packages/svelte/src/lib/store/initial-store.svelte.ts @@ -7,6 +7,7 @@ import { getViewportForBounds, updateConnectionLookup, initialConnection, + defaultLabelConfig, type SelectionRect, type SnapGrid, type MarkerProps, @@ -32,7 +33,8 @@ import { type Handle, type OnReconnect, type OnReconnectStart, - type OnReconnectEnd + type OnReconnectEnd, + type LabelConfig } from '@xyflow/system'; import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; @@ -289,6 +291,7 @@ export function getInitialStore Date: Wed, 21 May 2025 16:50:48 +0200 Subject: [PATCH 07/35] chore(a11y): add controls to LabelConfig --- examples/react/src/examples/A11y/index.tsx | 4 ++++ .../Controls/Controls.tsx | 20 +++++++++---------- packages/system/src/constants.ts | 6 ++++++ 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/examples/react/src/examples/A11y/index.tsx b/examples/react/src/examples/A11y/index.tsx index 61627a8f..c837c14c 100644 --- a/examples/react/src/examples/A11y/index.tsx +++ b/examples/react/src/examples/A11y/index.tsx @@ -65,6 +65,10 @@ const A11y = () => { 'a11yDescription.node.default': 'Custom Node Desc.', 'a11yDescription.node.keyboardDisabled': 'Custom Keyboard Desc.', 'a11yDescription.edge.default': 'Custom Edge Desc.', + 'controls.zoomin.title': 'Custom Zoom in', + 'controls.zoomout.title': 'Custom Zoom Out', + 'controls.fitview.title': 'Custom Fit View', + 'controls.interactive.title': 'Custom Toggle Interactivity', }} > diff --git a/packages/react/src/additional-components/Controls/Controls.tsx b/packages/react/src/additional-components/Controls/Controls.tsx index 84337a28..1dca3548 100644 --- a/packages/react/src/additional-components/Controls/Controls.tsx +++ b/packages/react/src/additional-components/Controls/Controls.tsx @@ -19,6 +19,7 @@ const selector = (s: ReactFlowState) => ({ isInteractive: s.nodesDraggable || s.nodesConnectable || s.elementsSelectable, minZoomReached: s.transform[2] <= s.minZoom, maxZoomReached: s.transform[2] >= s.maxZoom, + labelConfig: s.labelConfig, }); function ControlsComponent({ @@ -38,7 +39,7 @@ function ControlsComponent({ 'aria-label': ariaLabel = 'React Flow controls', }: ControlProps) { const store = useStoreApi(); - const { isInteractive, minZoomReached, maxZoomReached } = useStore(selector, shallow); + const { isInteractive, minZoomReached, maxZoomReached, labelConfig } = useStore(selector, shallow); const { zoomIn, zoomOut, fitView } = useReactFlow(); const onZoomInHandler = () => { @@ -67,7 +68,6 @@ function ControlsComponent({ }; const orientationClass = orientation === 'horizontal' ? 'horizontal' : 'vertical'; - return ( @@ -90,8 +90,8 @@ function ControlsComponent({ @@ -102,8 +102,8 @@ function ControlsComponent({ @@ -112,8 +112,8 @@ function ControlsComponent({ {isInteractive ? : } diff --git a/packages/system/src/constants.ts b/packages/system/src/constants.ts index b3ff1d41..9a3259a2 100644 --- a/packages/system/src/constants.ts +++ b/packages/system/src/constants.ts @@ -44,6 +44,12 @@ export const defaultLabelConfig = { 'Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.', 'a11yDescription.edge.default': 'Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.', + + // Control elements + 'controls.zoomin.title': 'Zoom In', + 'controls.zoomout.title': 'Zoom Out', + 'controls.fitview.title': 'Fit View', + 'controls.interactive.title': 'Toggle Interactivity', }; export type LabelConfig = Partial; From 6081f687bf281675dd9282cecbf06f06cf43e203 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Wed, 21 May 2025 16:56:35 +0200 Subject: [PATCH 08/35] chore(a11y): add custom control labels Svelte --- .../src/routes/examples/a11y/+page.svelte | 19 ++++++++++++++----- .../src/lib/plugins/Controls/Controls.svelte | 17 +++++++++-------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/examples/svelte/src/routes/examples/a11y/+page.svelte b/examples/svelte/src/routes/examples/a11y/+page.svelte index 395a09c6..18a50dcc 100644 --- a/examples/svelte/src/routes/examples/a11y/+page.svelte +++ b/examples/svelte/src/routes/examples/a11y/+page.svelte @@ -29,11 +29,20 @@ - + diff --git a/packages/svelte/src/lib/plugins/Controls/Controls.svelte b/packages/svelte/src/lib/plugins/Controls/Controls.svelte index d91588f9..85a7623b 100644 --- a/packages/svelte/src/lib/plugins/Controls/Controls.svelte +++ b/packages/svelte/src/lib/plugins/Controls/Controls.svelte @@ -46,6 +46,7 @@ ); let minZoomReached = $derived(store.viewport.zoom <= store.minZoom); let maxZoomReached = $derived(store.viewport.zoom >= store.maxZoom); + let labelConfig = $derived(store.labelConfig); let orientationClass = $derived(orientation === 'horizontal' ? 'horizontal' : 'vertical'); const onZoomInHandler = () => { @@ -83,8 +84,8 @@ @@ -93,8 +94,8 @@ @@ -105,8 +106,8 @@ @@ -116,8 +117,8 @@ {#if isInteractive}{:else}{/if} From 48c54678512d2e1f96b24b6ec92763a650162048 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Thu, 22 May 2025 11:38:02 +0200 Subject: [PATCH 09/35] chore(a11y): merge labelConfig with defaults --- examples/react/src/examples/A11y/index.tsx | 2 +- examples/svelte/src/routes/examples/a11y/+page.svelte | 2 +- packages/react/src/components/StoreUpdater/index.tsx | 6 +++++- packages/svelte/src/lib/store/initial-store.svelte.ts | 4 ++-- packages/system/src/utils/general.ts | 6 ++++++ 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/examples/react/src/examples/A11y/index.tsx b/examples/react/src/examples/A11y/index.tsx index c837c14c..39c486d6 100644 --- a/examples/react/src/examples/A11y/index.tsx +++ b/examples/react/src/examples/A11y/index.tsx @@ -67,7 +67,7 @@ const A11y = () => { 'a11yDescription.edge.default': 'Custom Edge Desc.', 'controls.zoomin.title': 'Custom Zoom in', 'controls.zoomout.title': 'Custom Zoom Out', - 'controls.fitview.title': 'Custom Fit View', + // 'controls.fitview.title': 'Custom Fit View', 'controls.interactive.title': 'Custom Toggle Interactivity', }} > diff --git a/examples/svelte/src/routes/examples/a11y/+page.svelte b/examples/svelte/src/routes/examples/a11y/+page.svelte index 18a50dcc..9ca28ae6 100644 --- a/examples/svelte/src/routes/examples/a11y/+page.svelte +++ b/examples/svelte/src/routes/examples/a11y/+page.svelte @@ -39,7 +39,7 @@ 'a11yDescription.edge.default': 'Svelte Custom Edge Desc.', 'controls.zoomin.title': 'Svelte Custom Zoom in', 'controls.zoomout.title': 'Svelte Custom Zoom Out', - 'controls.fitview.title': 'Svelte Custom Fit View', + // 'controls.fitview.title': 'Svelte Custom Fit View', 'controls.interactive.title': 'Svelte Custom Toggle Interactivity', }} > diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index dc1163dc..d2fb3cae 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -5,7 +5,7 @@ */ import { useEffect, useRef } from 'react'; import { shallow } from 'zustand/shallow'; -import { infiniteExtent, type CoordinateExtent } from '@xyflow/system'; +import { infiniteExtent, type CoordinateExtent, mergeLabelConfig, LabelConfig } from '@xyflow/system'; import { useStore, useStoreApi } from '../../hooks/useStore'; import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } from '../../types'; @@ -157,6 +157,10 @@ export function StoreUpdater Math.min(Math.max(val, min), max); export const clampPosition = ( @@ -418,3 +420,7 @@ export function withResolvers(): { }); return { promise, resolve, reject }; } + +export function mergeLabelConfig(partial?: Partial): LabelConfig { + return { ...defaultLabelConfig, ...(partial || {}) }; +} From 3c421d5f020872dc3eb3c20967bc4898b59764de Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Thu, 22 May 2025 12:16:37 +0200 Subject: [PATCH 10/35] chore(Minimap): add custom aria label for SF and RF --- examples/react/src/examples/A11y/index.tsx | 1 + .../svelte/src/routes/examples/a11y/+page.svelte | 1 + .../src/additional-components/MiniMap/MiniMap.tsx | 14 ++++++++++---- .../svelte/src/lib/plugins/Minimap/Minimap.svelte | 7 +++++-- packages/system/src/constants.ts | 3 +++ 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/examples/react/src/examples/A11y/index.tsx b/examples/react/src/examples/A11y/index.tsx index 39c486d6..9e5d6579 100644 --- a/examples/react/src/examples/A11y/index.tsx +++ b/examples/react/src/examples/A11y/index.tsx @@ -69,6 +69,7 @@ const A11y = () => { 'controls.zoomout.title': 'Custom Zoom Out', // 'controls.fitview.title': 'Custom Fit View', 'controls.interactive.title': 'Custom Toggle Interactivity', + 'minimap.ariaLabel': 'Custom Aria Label', }} > diff --git a/examples/svelte/src/routes/examples/a11y/+page.svelte b/examples/svelte/src/routes/examples/a11y/+page.svelte index 9ca28ae6..b80ca187 100644 --- a/examples/svelte/src/routes/examples/a11y/+page.svelte +++ b/examples/svelte/src/routes/examples/a11y/+page.svelte @@ -41,6 +41,7 @@ 'controls.zoomout.title': 'Svelte Custom Zoom Out', // 'controls.fitview.title': 'Svelte Custom Fit View', 'controls.interactive.title': 'Svelte Custom Toggle Interactivity', + 'minimap.ariaLabel': 'Svelte Custom Minimap', }} > diff --git a/packages/react/src/additional-components/MiniMap/MiniMap.tsx b/packages/react/src/additional-components/MiniMap/MiniMap.tsx index 38e63f72..c48207ca 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMap.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMap.tsx @@ -36,11 +36,11 @@ const selector = (s: ReactFlowState) => { translateExtent: s.translateExtent, flowWidth: s.width, flowHeight: s.height, + labelConfig: s.labelConfig, }; }; const ARIA_LABEL_KEY = 'react-flow__minimap-desc'; - function MiniMapComponent({ style, className, @@ -63,14 +63,17 @@ function MiniMapComponent({ onNodeClick, pannable = false, zoomable = false, - ariaLabel = 'React Flow mini map', + ariaLabel, inversePan, zoomStep = 10, offsetScale = 5, }: MiniMapProps) { const store = useStoreApi(); const svg = useRef(null); - const { boundingRect, viewBB, rfId, panZoom, translateExtent, flowWidth, flowHeight } = useStore(selector, shallow); + const { boundingRect, viewBB, rfId, panZoom, translateExtent, flowWidth, flowHeight, labelConfig } = useStore( + selector, + shallow + ); const elementWidth = (style?.width as number) ?? defaultWidth; const elementHeight = (style?.height as number) ?? defaultHeight; const scaledWidth = boundingRect.width / elementWidth; @@ -130,6 +133,8 @@ function MiniMapComponent({ }, []) : undefined; + const effectiveAriaLabel = ariaLabel ?? labelConfig['minimap.ariaLabel']; + return ( ({ ref={svg} onClick={onSvgClick} > - {ariaLabel && {ariaLabel}} + {effectiveAriaLabel && {effectiveAriaLabel}} + onClick={onSvgNodeClick} nodeColor={nodeColor} diff --git a/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte b/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte index 5dcb5b3f..d31f5771 100644 --- a/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte +++ b/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte @@ -21,7 +21,7 @@ let { position = 'bottom-right', - ariaLabel = 'Mini map', + ariaLabel, nodeStrokeColor = 'transparent', nodeColor, nodeClass = '', @@ -42,6 +42,7 @@ }: MiniMapProps = $props(); let store = $derived(useStore()); + let labelConfig = $derived(store.labelConfig); const nodeColorFunc = nodeColor === undefined ? undefined : getAttrFunction(nodeColor); const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor); @@ -113,7 +114,9 @@ zoomable }} > - {#if ariaLabel}{ariaLabel}{/if} + {#if ariaLabel ?? labelConfig['minimap.ariaLabel']} + {ariaLabel ?? labelConfig['minimap.ariaLabel']} + {/if} {#each store.nodes as userNode (userNode.id)} {@const node = store.nodeLookup.get(userNode.id)} diff --git a/packages/system/src/constants.ts b/packages/system/src/constants.ts index 9a3259a2..a52c1fcc 100644 --- a/packages/system/src/constants.ts +++ b/packages/system/src/constants.ts @@ -50,6 +50,9 @@ export const defaultLabelConfig = { 'controls.zoomout.title': 'Zoom Out', 'controls.fitview.title': 'Fit View', 'controls.interactive.title': 'Toggle Interactivity', + + // Mini map + 'minimap.ariaLabel': 'React Flow mini map', }; export type LabelConfig = Partial; From 74929f09bc60a9471423bf1876c70c398f973cb3 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Thu, 22 May 2025 12:36:36 +0200 Subject: [PATCH 11/35] chore(controls): add custom aria labels --- examples/react/src/examples/A11y/index.tsx | 1 + examples/svelte/src/routes/examples/a11y/+page.svelte | 1 + .../react/src/additional-components/Controls/Controls.tsx | 5 +++-- packages/react/src/additional-components/MiniMap/types.ts | 2 +- packages/svelte/src/lib/plugins/Controls/Controls.svelte | 2 +- packages/system/src/constants.ts | 3 ++- 6 files changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/react/src/examples/A11y/index.tsx b/examples/react/src/examples/A11y/index.tsx index 9e5d6579..3f872bfb 100644 --- a/examples/react/src/examples/A11y/index.tsx +++ b/examples/react/src/examples/A11y/index.tsx @@ -65,6 +65,7 @@ const A11y = () => { 'a11yDescription.node.default': 'Custom Node Desc.', 'a11yDescription.node.keyboardDisabled': 'Custom Keyboard Desc.', 'a11yDescription.edge.default': 'Custom Edge Desc.', + 'controls.ariaLabel': 'Custom Controls Aria Label', 'controls.zoomin.title': 'Custom Zoom in', 'controls.zoomout.title': 'Custom Zoom Out', // 'controls.fitview.title': 'Custom Fit View', diff --git a/examples/svelte/src/routes/examples/a11y/+page.svelte b/examples/svelte/src/routes/examples/a11y/+page.svelte index b80ca187..939f331f 100644 --- a/examples/svelte/src/routes/examples/a11y/+page.svelte +++ b/examples/svelte/src/routes/examples/a11y/+page.svelte @@ -37,6 +37,7 @@ 'a11yDescription.node.default': 'Svelte Custom Node Desc.', 'a11yDescription.node.keyboardDisabled': 'Svelte Custom Keyboard Desc.', 'a11yDescription.edge.default': 'Svelte Custom Edge Desc.', + 'controls.ariaLabel': 'Svelte Custom Control Aria Label', 'controls.zoomin.title': 'Svelte Custom Zoom in', 'controls.zoomout.title': 'Svelte Custom Zoom Out', // 'controls.fitview.title': 'Svelte Custom Fit View', diff --git a/packages/react/src/additional-components/Controls/Controls.tsx b/packages/react/src/additional-components/Controls/Controls.tsx index 1dca3548..e6388e66 100644 --- a/packages/react/src/additional-components/Controls/Controls.tsx +++ b/packages/react/src/additional-components/Controls/Controls.tsx @@ -36,11 +36,12 @@ function ControlsComponent({ children, position = 'bottom-left', orientation = 'vertical', - 'aria-label': ariaLabel = 'React Flow controls', + 'aria-label': ariaLabel, }: ControlProps) { const store = useStoreApi(); const { isInteractive, minZoomReached, maxZoomReached, labelConfig } = useStore(selector, shallow); const { zoomIn, zoomOut, fitView } = useReactFlow(); + const effectiveAriaLabel = ariaLabel ?? labelConfig['controls.ariaLabel']; const onZoomInHandler = () => { zoomIn(); @@ -74,7 +75,7 @@ function ControlsComponent({ position={position} style={style} data-testid="rf__controls" - aria-label={ariaLabel} + aria-label={effectiveAriaLabel} > {showZoom && ( <> diff --git a/packages/react/src/additional-components/MiniMap/types.ts b/packages/react/src/additional-components/MiniMap/types.ts index 02513b10..53b01114 100644 --- a/packages/react/src/additional-components/MiniMap/types.ts +++ b/packages/react/src/additional-components/MiniMap/types.ts @@ -83,7 +83,7 @@ export type MiniMapProps = Omit diff --git a/packages/system/src/constants.ts b/packages/system/src/constants.ts index a52c1fcc..b1697334 100644 --- a/packages/system/src/constants.ts +++ b/packages/system/src/constants.ts @@ -46,13 +46,14 @@ export const defaultLabelConfig = { 'Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.', // Control elements + 'controls.ariaLabel': 'Control Panel', 'controls.zoomin.title': 'Zoom In', 'controls.zoomout.title': 'Zoom Out', 'controls.fitview.title': 'Fit View', 'controls.interactive.title': 'Toggle Interactivity', // Mini map - 'minimap.ariaLabel': 'React Flow mini map', + 'minimap.ariaLabel': 'Mini Map', }; export type LabelConfig = Partial; From 21e25d698c12062a9f6a63f2cb8d72a46e07519f Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Thu, 22 May 2025 15:04:20 +0200 Subject: [PATCH 12/35] feat(node): add `as` property to allow custom HTML elements --- examples/react/src/examples/A11y/index.tsx | 6 ++-- .../src/routes/examples/a11y/+page.svelte | 5 ++- .../src/components/NodeWrapper/index.tsx | 34 +++++++++++++------ .../components/NodeWrapper/NodeWrapper.svelte | 27 +++++++++------ packages/system/src/types/nodes.ts | 4 +++ 5 files changed, 47 insertions(+), 29 deletions(-) diff --git a/examples/react/src/examples/A11y/index.tsx b/examples/react/src/examples/A11y/index.tsx index 3f872bfb..4cc2cdff 100644 --- a/examples/react/src/examples/A11y/index.tsx +++ b/examples/react/src/examples/A11y/index.tsx @@ -22,19 +22,17 @@ const initialNodes: Node[] = [ type: 'input', data: { label: 'A11y Node 1' }, position: { x: 250, y: 5 }, - className: 'light', }, { id: '2', - data: { label: 'Node 2' }, + data: { label: '
    Node' }, position: { x: 100, y: 100 }, - className: 'light', + as: 'ul', }, { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, - className: 'light', }, ]; diff --git a/examples/svelte/src/routes/examples/a11y/+page.svelte b/examples/svelte/src/routes/examples/a11y/+page.svelte index 939f331f..d4e1d320 100644 --- a/examples/svelte/src/routes/examples/a11y/+page.svelte +++ b/examples/svelte/src/routes/examples/a11y/+page.svelte @@ -13,10 +13,9 @@ { id: 'A', position: { x: 0, y: 0 }, - data: { label: 'A' }, - + data: { label: 'A' }, }, - { id: 'B', position: { x: -100, y: 150 }, data: { label: 'B' } }, + { id: 'B', position: { x: -100, y: 150 }, data: { label: '
      Node' }, as: 'ul' }, { id: 'C', position: { x: 100, y: 150 }, data: { label: 'C' } }, { id: 'D', position: { x: 0, y: 260 }, data: { label: 'D' } } ]); diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 5c0ce08b..da30038d 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -67,7 +67,15 @@ export function NodeWrapper({ const store = useStoreApi(); const hasDimensions = nodeHasDimensions(node); - const nodeRef = useNodeObserver({ node, nodeType, hasDimensions, resizeObserver }); + const Tag: keyof JSX.IntrinsicElements = node.as || 'div'; + + const nodeRef = useNodeObserver({ + node, + nodeType, + hasDimensions, + resizeObserver, + }); + const dragging = useDrag({ nodeRef, disabled: node.hidden || !isDraggable, @@ -156,7 +164,7 @@ export function NodeWrapper({ }; return ( -
      ({ dragging, }, ])} - ref={nodeRef} + ref={(instance: HTMLElement | null) => { + if (nodeRef && 'current' in nodeRef) { + (nodeRef as React.MutableRefObject).current = instance; + } + }} style={{ zIndex: internals.z, transform: `translate(${internals.positionAbsolute.x}px,${internals.positionAbsolute.y}px)`, @@ -184,13 +196,13 @@ export function NodeWrapper({ }} data-id={id} data-testid={`rf__node-${id}`} - onMouseEnter={onMouseEnterHandler} - onMouseMove={onMouseMoveHandler} - onMouseLeave={onMouseLeaveHandler} - onContextMenu={onContextMenuHandler} - onClick={onSelectNodeHandler} - onDoubleClick={onDoubleClickHandler} - onKeyDown={isFocusable ? onKeyDown : undefined} + onMouseEnter={onMouseEnterHandler as React.MouseEventHandler} + onMouseMove={onMouseMoveHandler as React.MouseEventHandler} + onMouseLeave={onMouseLeaveHandler as React.MouseEventHandler} + onContextMenu={onContextMenuHandler as React.MouseEventHandler} + onClick={onSelectNodeHandler as React.MouseEventHandler} + onDoubleClick={onDoubleClickHandler as React.MouseEventHandler} + onKeyDown={isFocusable ? (onKeyDown as React.KeyboardEventHandler) : undefined} tabIndex={isFocusable ? 0 : undefined} role={isFocusable ? 'button' : undefined} aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`} @@ -217,6 +229,6 @@ export function NodeWrapper({ {...nodeDimensions} /> -
      + ); } diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index f053cd82..24f40a8c 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -53,15 +53,19 @@ width, height, dragHandle, + as = 'div', internals: { z: zIndex = 0, positionAbsolute: { x: positionX, y: positionY }, - userNode + userNode, + } } = $derived(node); let { id } = node; + let Tag = $derived(typeof as === 'string' ? as : 'div'); + let draggable = $derived(_draggable ?? store.nodesDraggable); let selectable = $derived(_selectable ?? store.elementsSelectable); let connectable = $derived(_connectable ?? store.nodesConnectable); @@ -74,7 +78,7 @@ let isParent = $derived(isInParentLookup(id)); - let nodeRef: HTMLDivElement | null = $state(null); + let nodeRef: HTMLElement | null = $state(null); let prevNodeRef: HTMLDivElement | null = null; // svelte-ignore state_referenced_locally @@ -131,7 +135,7 @@ id, { id, - nodeElement: nodeRef, + nodeElement: nodeRef as HTMLDivElement, force: true } ] @@ -151,7 +155,7 @@ if (resizeObserver && (!initialized || nodeRef !== prevNodeRef)) { prevNodeRef && resizeObserver.unobserve(prevNodeRef); nodeRef && resizeObserver.observe(nodeRef); - prevNodeRef = nodeRef; + prevNodeRef = nodeRef as HTMLDivElement | null; } /* eslint-enable @typescript-eslint/no-unused-expressions */ }); @@ -180,7 +184,7 @@ if (elementSelectionKeys.includes(event.key) && selectable) { const unselect = event.key === 'Escape'; - store.handleNodeSelection(id, unselect, nodeRef); + store.handleNodeSelection(id, unselect, nodeRef as HTMLDivElement | null); } else if ( draggable && node.selected && @@ -199,7 +203,8 @@ {#if !hidden} -
      onnodepointerenter({ node: userNode, event }) + ? (event: PointerEvent) => onnodepointerenter({ node: userNode, event }) : undefined} onpointerleave={onnodepointerleave - ? (event) => onnodepointerleave({ node: userNode, event }) + ? (event: PointerEvent) => onnodepointerleave({ node: userNode, event }) : undefined} onpointermove={onnodepointermove - ? (event) => onnodepointermove({ node: userNode, event }) + ? (event: PointerEvent) => onnodepointermove({ node: userNode, event }) : undefined} oncontextmenu={onnodecontextmenu - ? (event) => onnodecontextmenu({ node: userNode, event }) + ? (event: MouseEvent) => onnodecontextmenu({ node: userNode, event }) : undefined} onkeydown={focusable ? onKeyDown : undefined} tabIndex={focusable ? 0 : undefined} @@ -277,5 +282,5 @@ {width} {height} /> -
      + {/if} diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index b32d58c5..7239b61d 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -23,6 +23,10 @@ export type NodeBase< data: NodeData; /** Type of node defined in `nodeTypes`. */ type?: NodeType; + /** Custom element type for rendering the node (e.g., 'div', 'li', etc.). + * @default 'div' + */ + as?: Extract; /** * Only relevant for default, source, target nodeType. Controls source position. * @example 'right', 'left', 'top', 'bottom' From 1fb7a5c98a6dbf0aacb4afbc4c204acebcc89cf5 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Thu, 22 May 2025 15:58:31 +0200 Subject: [PATCH 13/35] chore(a11y): add ariaLiveMessage to label config --- examples/react/src/examples/A11y/index.tsx | 2 ++ .../svelte/src/routes/examples/a11y/+page.svelte | 5 ++++- packages/react/src/components/NodeWrapper/index.tsx | 12 ++++++++---- .../lib/components/NodeWrapper/NodeWrapper.svelte | 10 ++++++---- packages/system/src/constants.ts | 2 ++ 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/examples/react/src/examples/A11y/index.tsx b/examples/react/src/examples/A11y/index.tsx index 4cc2cdff..85d959ff 100644 --- a/examples/react/src/examples/A11y/index.tsx +++ b/examples/react/src/examples/A11y/index.tsx @@ -63,6 +63,8 @@ const A11y = () => { 'a11yDescription.node.default': 'Custom Node Desc.', 'a11yDescription.node.keyboardDisabled': 'Custom Keyboard Desc.', 'a11yDescription.edge.default': 'Custom Edge Desc.', + 'a11yDescription.ariaLiveMessage': (node: string, x: number, y: number) => + `Custom Moved selected node ${node}. New position, x: ${x}, y: ${y}`, 'controls.ariaLabel': 'Custom Controls Aria Label', 'controls.zoomin.title': 'Custom Zoom in', 'controls.zoomout.title': 'Custom Zoom Out', diff --git a/examples/svelte/src/routes/examples/a11y/+page.svelte b/examples/svelte/src/routes/examples/a11y/+page.svelte index d4e1d320..4f0ab7b0 100644 --- a/examples/svelte/src/routes/examples/a11y/+page.svelte +++ b/examples/svelte/src/routes/examples/a11y/+page.svelte @@ -15,7 +15,8 @@ position: { x: 0, y: 0 }, data: { label: 'A' }, }, - { id: 'B', position: { x: -100, y: 150 }, data: { label: '
        Node' }, as: 'ul' }, + // You need to explicitly pass the as type + { id: 'B', position: { x: -100, y: 150 }, data: { label: '
          Node' }, as: "ul" as "ul" }, { id: 'C', position: { x: 100, y: 150 }, data: { label: 'C' } }, { id: 'D', position: { x: 0, y: 260 }, data: { label: 'D' } } ]); @@ -36,6 +37,8 @@ 'a11yDescription.node.default': 'Svelte Custom Node Desc.', 'a11yDescription.node.keyboardDisabled': 'Svelte Custom Keyboard Desc.', 'a11yDescription.edge.default': 'Svelte Custom Edge Desc.', + 'a11yDescription.ariaLiveMessage': (node: string, x: number, y: number) => + `Custom Moved selected node ${node}. New position, x: ${x}, y: ${y}`, 'controls.ariaLabel': 'Svelte Custom Control Aria Label', 'controls.zoomin.title': 'Svelte Custom Zoom in', 'controls.zoomout.title': 'Svelte Custom Zoom Out', diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index da30038d..a088164b 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -40,7 +40,7 @@ export function NodeWrapper({ nodeClickDistance, onError, }: NodeWrapperProps) { - const { node, internals, isParent } = useStore((s) => { + const { node, internals, isParent, labelConfig } = useStore((s) => { const node = s.nodeLookup.get(id)! as InternalNode; const isParent = s.parentLookup.has(id); @@ -48,6 +48,7 @@ export function NodeWrapper({ node, internals: node.internals, isParent, + labelConfig: s.labelConfig, }; }, shallow); @@ -151,9 +152,12 @@ export function NodeWrapper({ event.preventDefault(); store.setState({ - ariaLiveMessage: `Moved selected node ${event.key - .replace('Arrow', '') - .toLowerCase()}. New position, x: ${~~internals.positionAbsolute.x}, y: ${~~internals.positionAbsolute.y}`, + ariaLiveMessage: + labelConfig['a11yDescription.ariaLiveMessage']?.( + event.key.replace('Arrow', '').toLowerCase(), + ~~internals.positionAbsolute.x, + ~~internals.positionAbsolute.y + ) || '', }); moveSelectedNodes({ diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index 24f40a8c..8e96fc70 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -89,6 +89,7 @@ let prevTargetPosition: Position | undefined = targetPosition; let NodeComponent = $derived(store.nodeTypes[type] ?? DefaultNode); + let labelConfig = $derived(store.labelConfig); let connectableContext: ConnectableContext = { get value() { @@ -192,10 +193,11 @@ ) { // prevent default scrolling behavior on arrow key press when node is moved event.preventDefault(); - - store.ariaLiveMessage = `Moved selected node ${event.key - .replace('Arrow', '') - .toLowerCase()}. New position, x: ${node.internals.positionAbsolute.x}, y: ${node.internals.positionAbsolute.y}`; + store.ariaLiveMessage = labelConfig['a11yDescription.ariaLiveMessage']( + event.key.replace('Arrow', '').toLowerCase(), + ~~node.internals.positionAbsolute.x, + ~~node.internals.positionAbsolute.y + ) || ''; store.moveSelectedNodes(arrowKeyDiffs[event.key], event.shiftKey ? 4 : 1); } diff --git a/packages/system/src/constants.ts b/packages/system/src/constants.ts index b1697334..24941555 100644 --- a/packages/system/src/constants.ts +++ b/packages/system/src/constants.ts @@ -44,6 +44,8 @@ export const defaultLabelConfig = { 'Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.', 'a11yDescription.edge.default': 'Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.', + 'a11yDescription.ariaLiveMessage': (node: string, x: number, y: number) => + `Moved selected node ${node}. New position, x: ${x}, y: ${y}`, // Control elements 'controls.ariaLabel': 'Control Panel', From b310586dfbabf32a271b80a19d1d8fb50211ff43 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Thu, 22 May 2025 16:31:50 +0200 Subject: [PATCH 14/35] fix: revert addition of `as` prop --- examples/react/src/examples/A11y/index.tsx | 6 ++-- .../src/routes/examples/a11y/+page.svelte | 6 ++-- .../src/components/NodeWrapper/index.tsx | 34 ++++++------------- .../components/NodeWrapper/NodeWrapper.svelte | 27 ++++++--------- packages/system/src/types/nodes.ts | 4 --- 5 files changed, 29 insertions(+), 48 deletions(-) diff --git a/examples/react/src/examples/A11y/index.tsx b/examples/react/src/examples/A11y/index.tsx index 85d959ff..0c921798 100644 --- a/examples/react/src/examples/A11y/index.tsx +++ b/examples/react/src/examples/A11y/index.tsx @@ -22,17 +22,19 @@ const initialNodes: Node[] = [ type: 'input', data: { label: 'A11y Node 1' }, position: { x: 250, y: 5 }, + className: 'light', }, { id: '2', - data: { label: '
            Node' }, + data: { label: 'Node 2' }, position: { x: 100, y: 100 }, - as: 'ul', + className: 'light', }, { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, + className: 'light', }, ]; diff --git a/examples/svelte/src/routes/examples/a11y/+page.svelte b/examples/svelte/src/routes/examples/a11y/+page.svelte index 4f0ab7b0..171576db 100644 --- a/examples/svelte/src/routes/examples/a11y/+page.svelte +++ b/examples/svelte/src/routes/examples/a11y/+page.svelte @@ -13,10 +13,10 @@ { id: 'A', position: { x: 0, y: 0 }, - data: { label: 'A' }, + data: { label: 'A' }, + }, - // You need to explicitly pass the as type - { id: 'B', position: { x: -100, y: 150 }, data: { label: '
              Node' }, as: "ul" as "ul" }, + { id: 'B', position: { x: -100, y: 150 }, data: { label: 'B' } }, { id: 'C', position: { x: 100, y: 150 }, data: { label: 'C' } }, { id: 'D', position: { x: 0, y: 260 }, data: { label: 'D' } } ]); diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index a088164b..b64176fa 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -68,15 +68,7 @@ export function NodeWrapper({ const store = useStoreApi(); const hasDimensions = nodeHasDimensions(node); - const Tag: keyof JSX.IntrinsicElements = node.as || 'div'; - - const nodeRef = useNodeObserver({ - node, - nodeType, - hasDimensions, - resizeObserver, - }); - + const nodeRef = useNodeObserver({ node, nodeType, hasDimensions, resizeObserver }); const dragging = useDrag({ nodeRef, disabled: node.hidden || !isDraggable, @@ -168,7 +160,7 @@ export function NodeWrapper({ }; return ( - ({ dragging, }, ])} - ref={(instance: HTMLElement | null) => { - if (nodeRef && 'current' in nodeRef) { - (nodeRef as React.MutableRefObject).current = instance; - } - }} + ref={nodeRef} style={{ zIndex: internals.z, transform: `translate(${internals.positionAbsolute.x}px,${internals.positionAbsolute.y}px)`, @@ -200,13 +188,13 @@ export function NodeWrapper({ }} data-id={id} data-testid={`rf__node-${id}`} - onMouseEnter={onMouseEnterHandler as React.MouseEventHandler} - onMouseMove={onMouseMoveHandler as React.MouseEventHandler} - onMouseLeave={onMouseLeaveHandler as React.MouseEventHandler} - onContextMenu={onContextMenuHandler as React.MouseEventHandler} - onClick={onSelectNodeHandler as React.MouseEventHandler} - onDoubleClick={onDoubleClickHandler as React.MouseEventHandler} - onKeyDown={isFocusable ? (onKeyDown as React.KeyboardEventHandler) : undefined} + onMouseEnter={onMouseEnterHandler} + onMouseMove={onMouseMoveHandler} + onMouseLeave={onMouseLeaveHandler} + onContextMenu={onContextMenuHandler} + onClick={onSelectNodeHandler} + onDoubleClick={onDoubleClickHandler} + onKeyDown={isFocusable ? onKeyDown : undefined} tabIndex={isFocusable ? 0 : undefined} role={isFocusable ? 'button' : undefined} aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`} @@ -233,6 +221,6 @@ export function NodeWrapper({ {...nodeDimensions} /> - + ); } diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index 8e96fc70..ea8c6494 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -53,19 +53,15 @@ width, height, dragHandle, - as = 'div', internals: { z: zIndex = 0, positionAbsolute: { x: positionX, y: positionY }, - userNode, - + userNode } } = $derived(node); let { id } = node; - let Tag = $derived(typeof as === 'string' ? as : 'div'); - let draggable = $derived(_draggable ?? store.nodesDraggable); let selectable = $derived(_selectable ?? store.elementsSelectable); let connectable = $derived(_connectable ?? store.nodesConnectable); @@ -78,7 +74,7 @@ let isParent = $derived(isInParentLookup(id)); - let nodeRef: HTMLElement | null = $state(null); + let nodeRef: HTMLDivElement | null = $state(null); let prevNodeRef: HTMLDivElement | null = null; // svelte-ignore state_referenced_locally @@ -136,7 +132,7 @@ id, { id, - nodeElement: nodeRef as HTMLDivElement, + nodeElement: nodeRef, force: true } ] @@ -156,7 +152,7 @@ if (resizeObserver && (!initialized || nodeRef !== prevNodeRef)) { prevNodeRef && resizeObserver.unobserve(prevNodeRef); nodeRef && resizeObserver.observe(nodeRef); - prevNodeRef = nodeRef as HTMLDivElement | null; + prevNodeRef = nodeRef; } /* eslint-enable @typescript-eslint/no-unused-expressions */ }); @@ -185,7 +181,7 @@ if (elementSelectionKeys.includes(event.key) && selectable) { const unselect = event.key === 'Escape'; - store.handleNodeSelection(id, unselect, nodeRef as HTMLDivElement | null); + store.handleNodeSelection(id, unselect, nodeRef); } else if ( draggable && node.selected && @@ -205,8 +201,7 @@ {#if !hidden} - onnodepointerenter({ node: userNode, event }) + ? (event) => onnodepointerenter({ node: userNode, event }) : undefined} onpointerleave={onnodepointerleave - ? (event: PointerEvent) => onnodepointerleave({ node: userNode, event }) + ? (event) => onnodepointerleave({ node: userNode, event }) : undefined} onpointermove={onnodepointermove - ? (event: PointerEvent) => onnodepointermove({ node: userNode, event }) + ? (event) => onnodepointermove({ node: userNode, event }) : undefined} oncontextmenu={onnodecontextmenu - ? (event: MouseEvent) => onnodecontextmenu({ node: userNode, event }) + ? (event) => onnodecontextmenu({ node: userNode, event }) : undefined} onkeydown={focusable ? onKeyDown : undefined} tabIndex={focusable ? 0 : undefined} @@ -284,5 +279,5 @@ {width} {height} /> - + {/if} diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index 7239b61d..b32d58c5 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -23,10 +23,6 @@ export type NodeBase< data: NodeData; /** Type of node defined in `nodeTypes`. */ type?: NodeType; - /** Custom element type for rendering the node (e.g., 'div', 'li', etc.). - * @default 'div' - */ - as?: Extract; /** * Only relevant for default, source, target nodeType. Controls source position. * @example 'right', 'left', 'top', 'bottom' From d444d2e9489042b9f8470aadcc859d451661b526 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Thu, 22 May 2025 16:37:44 +0200 Subject: [PATCH 15/35] fix: fix undefined linting error --- .../svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index ea8c6494..9a1a5271 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -189,7 +189,7 @@ ) { // prevent default scrolling behavior on arrow key press when node is moved event.preventDefault(); - store.ariaLiveMessage = labelConfig['a11yDescription.ariaLiveMessage']( + store.ariaLiveMessage = labelConfig['a11yDescription.ariaLiveMessage']?.( event.key.replace('Arrow', '').toLowerCase(), ~~node.internals.positionAbsolute.x, ~~node.internals.positionAbsolute.y From 9641f2a0dffc421aee7ea5a6fcb282a78c503e03 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Mon, 26 May 2025 12:04:22 +0200 Subject: [PATCH 16/35] chore: make labelConfig items required, update description text --- packages/react/src/components/NodeWrapper/index.tsx | 11 +++++------ packages/react/src/types/component-props.ts | 6 +++--- .../src/lib/components/NodeWrapper/NodeWrapper.svelte | 4 ++-- packages/svelte/src/lib/container/SvelteFlow/types.ts | 4 ++-- packages/system/src/constants.ts | 2 +- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index b64176fa..af067adc 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -144,12 +144,11 @@ export function NodeWrapper({ event.preventDefault(); store.setState({ - ariaLiveMessage: - labelConfig['a11yDescription.ariaLiveMessage']?.( - event.key.replace('Arrow', '').toLowerCase(), - ~~internals.positionAbsolute.x, - ~~internals.positionAbsolute.y - ) || '', + ariaLiveMessage: labelConfig['a11yDescription.ariaLiveMessage']( + event.key.replace('Arrow', '').toLowerCase(), + ~~internals.positionAbsolute.x, + ~~internals.positionAbsolute.y + ), }); moveSelectedNodes({ diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index 0206e102..315168a9 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -668,8 +668,8 @@ export interface ReactFlowProps; } diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index 9a1a5271..5fd55e72 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -189,11 +189,11 @@ ) { // prevent default scrolling behavior on arrow key press when node is moved event.preventDefault(); - store.ariaLiveMessage = labelConfig['a11yDescription.ariaLiveMessage']?.( + store.ariaLiveMessage = labelConfig['a11yDescription.ariaLiveMessage']( event.key.replace('Arrow', '').toLowerCase(), ~~node.internals.positionAbsolute.x, ~~node.internals.positionAbsolute.y - ) || ''; + ), store.moveSelectedNodes(arrowKeyDiffs[event.key], event.shiftKey ? 4 : 1); } diff --git a/packages/svelte/src/lib/container/SvelteFlow/types.ts b/packages/svelte/src/lib/container/SvelteFlow/types.ts index 749f2fb8..03259006 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/types.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/types.ts @@ -473,8 +473,8 @@ export type SvelteFlowProps< /** This event handler gets called when the user finishes dragging a selection box */ onselectionend?: (event: PointerEvent) => void; /** - * Custom accessibility messages for screen readers and a11y features. - * Allows localization and customization of ARIA descriptions. + * Configuration for customizable labels, descriptions, and UI text. Provided keys will override the corresponding defaults. + * Allows localization, customization of ARIA descriptions, control labels, minimap labels, and other UI strings. */ labelConfig?: LabelConfig; }; diff --git a/packages/system/src/constants.ts b/packages/system/src/constants.ts index 24941555..8bbdd005 100644 --- a/packages/system/src/constants.ts +++ b/packages/system/src/constants.ts @@ -58,4 +58,4 @@ export const defaultLabelConfig = { 'minimap.ariaLabel': 'Mini Map', }; -export type LabelConfig = Partial; +export type LabelConfig = Required; From f59730ce3530a91f579f6bbd2ea9335680f552ef Mon Sep 17 00:00:00 2001 From: printer_scanner Date: Mon, 26 May 2025 12:12:46 +0200 Subject: [PATCH 17/35] chore: add changeset --- .changeset/quiet-forks-visit.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/quiet-forks-visit.md diff --git a/.changeset/quiet-forks-visit.md b/.changeset/quiet-forks-visit.md new file mode 100644 index 00000000..f2b27615 --- /dev/null +++ b/.changeset/quiet-forks-visit.md @@ -0,0 +1,8 @@ +--- +"@xyflow/react": minor +"@xyflow/svelte": minor +"@xyflow/system": minor +--- + +feat(a11y): add generalized labelConfig for customizable UI text and localization & ARIA descriptions + From ba9df8eaf848ce9eea937ec94572b7d9c35f2140 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Mon, 26 May 2025 17:08:29 +0200 Subject: [PATCH 18/35] feat: add role prop to nodes --- examples/react/src/examples/Basic/index.tsx | 3 ++- packages/react/src/components/NodeWrapper/index.tsx | 4 ++-- packages/react/src/types/nodes.ts | 10 +++++++++- packages/system/src/types/nodes.ts | 8 ++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/examples/react/src/examples/Basic/index.tsx b/examples/react/src/examples/Basic/index.tsx index 168bcff6..58123d57 100644 --- a/examples/react/src/examples/Basic/index.tsx +++ b/examples/react/src/examples/Basic/index.tsx @@ -25,9 +25,10 @@ const initialNodes: Node[] = [ { id: '1', type: 'input', - data: { label: 'Node 1' }, + data: { label: 'Focusable Node' }, position: { x: 250, y: 5 }, className: 'light', + role: null, }, { id: '2', diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 5c0ce08b..272f9de9 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -39,6 +39,7 @@ export function NodeWrapper({ nodeTypes, nodeClickDistance, onError, + role, }: NodeWrapperProps) { const { node, internals, isParent } = useStore((s) => { const node = s.nodeLookup.get(id)! as InternalNode; @@ -154,7 +155,6 @@ export function NodeWrapper({ }); } }; - return (
              ({ onDoubleClick={onDoubleClickHandler} onKeyDown={isFocusable ? onKeyDown : undefined} tabIndex={isFocusable ? 0 : undefined} - role={isFocusable ? 'button' : undefined} + role={node.role === null ? undefined : node.role || (isFocusable ? 'button' : undefined)} aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`} aria-label={node.ariaLabel} > diff --git a/packages/react/src/types/nodes.ts b/packages/react/src/types/nodes.ts index 8dc213d7..9be41de7 100644 --- a/packages/react/src/types/nodes.ts +++ b/packages/react/src/types/nodes.ts @@ -1,5 +1,12 @@ import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; -import type { CoordinateExtent, NodeBase, OnError, NodeProps as NodePropsBase, InternalNodeBase } from '@xyflow/system'; +import type { + CoordinateExtent, + NodeBase, + OnError, + NodeProps as NodePropsBase, + InternalNodeBase, + NodeRole, +} from '@xyflow/system'; import { NodeTypes } from './general'; @@ -58,6 +65,7 @@ export type NodeWrapperProps = { nodeExtent?: CoordinateExtent; onError?: OnError; nodeClickDistance?: number; + role?: NodeRole; }; /** diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index 687f81ba..f90b367d 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -73,6 +73,13 @@ export type NodeBase< */ origin?: NodeOrigin; handles?: NodeHandle[]; + /** + * The ARIA role attribute for the node element, used for accessibility. + * Common values for nodes might be 'button', 'group', 'listitem', etc. + * When not specified, focusable nodes default to 'button' role. + * @default "button" (for focusable nodes) + */ + role?: NodeRole; measured?: { width?: number; height?: number; @@ -180,3 +187,4 @@ export type Align = 'center' | 'start' | 'end'; export type NodeLookup = Map; export type ParentLookup = Map>; +export type NodeRole = 'button' | 'group' | 'listitem' | 'application' | 'region' | 'none' | null; From 18d940f2063d636bb5e7c85f661b10c1c4c1a206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Tue, 27 May 2025 14:35:18 +0200 Subject: [PATCH 19/35] chore: change readme maintainers section --- README.md | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 585dac0e..3ed80c4b 100644 --- a/README.md +++ b/README.md @@ -159,16 +159,9 @@ For releasing packages we are using [changesets](https://github.com/changesets/c 3. changset creates a PR that bumps all packages based on the changesets 4. merge changeset PR if you want to release to Github and npm -## The xyflow team - -React Flow and Svelte Flow are maintained by the team behind [xyflow](https://xyflow.com). If you need help or want to talk to us about a collaboration, reach out through our [contact form](https://xyflow.com/contact) or by joining our [Discord Server](https://discord.gg/Bqt6xrs). - -- Christopher • [Twitter](https://twitter.com/chrtze) • [Github](https://github.com/chrtze) -- Hayleigh • [Twitter](https://twitter.com/hayleighdotdev) • [Github](https://github.com/hayleigh-dot-dev) -- Abbey • [Github](https://github.com/printerscanner) -- Moritz • [Twitter](https://twitter.com/moklick) • [Github](https://github.com/moklick) -- Peter • [Github](https://github.com/peterkogo) +## Built by [xyflow](https://xyflow.com) +React Flow and Svelte Flow are maintained by the [xyflow team](https://xyflow.com/about). If you need help or want to talk to us about a collaboration, reach out through our [contact form](https://xyflow.com/contact) or by joining our [Discord Server](https://discord.gg/Bqt6xrs). ## License From cdc08f1b5477daba51b55b30b68f9ed5285b76b0 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Mon, 2 Jun 2025 12:10:54 +0200 Subject: [PATCH 20/35] chore: swap role for ariaRole, add svelte --- examples/react/src/examples/Basic/index.tsx | 3 +-- packages/react/src/components/NodeWrapper/index.tsx | 3 +-- packages/react/src/container/ReactFlow/index.tsx | 1 + packages/react/src/types/nodes.ts | 10 +--------- .../src/lib/components/NodeWrapper/NodeWrapper.svelte | 2 +- packages/system/src/types/nodes.ts | 5 +++-- 6 files changed, 8 insertions(+), 16 deletions(-) diff --git a/examples/react/src/examples/Basic/index.tsx b/examples/react/src/examples/Basic/index.tsx index 58123d57..168bcff6 100644 --- a/examples/react/src/examples/Basic/index.tsx +++ b/examples/react/src/examples/Basic/index.tsx @@ -25,10 +25,9 @@ const initialNodes: Node[] = [ { id: '1', type: 'input', - data: { label: 'Focusable Node' }, + data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light', - role: null, }, { id: '2', diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 272f9de9..10909bcd 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -39,7 +39,6 @@ export function NodeWrapper({ nodeTypes, nodeClickDistance, onError, - role, }: NodeWrapperProps) { const { node, internals, isParent } = useStore((s) => { const node = s.nodeLookup.get(id)! as InternalNode; @@ -192,7 +191,7 @@ export function NodeWrapper({ onDoubleClick={onDoubleClickHandler} onKeyDown={isFocusable ? onKeyDown : undefined} tabIndex={isFocusable ? 0 : undefined} - role={node.role === null ? undefined : node.role || (isFocusable ? 'button' : undefined)} + role={node.ariaRole === null ? undefined : node.ariaRole || (isFocusable ? 'button' : undefined)} 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" > = { nodeExtent?: CoordinateExtent; onError?: OnError; nodeClickDistance?: number; - role?: NodeRole; }; /** diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index f053cd82..dce41767 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -252,7 +252,7 @@ : undefined} onkeydown={focusable ? onKeyDown : undefined} tabIndex={focusable ? 0 : undefined} - role={focusable ? 'button' : undefined} + role={node.ariaRole === null ? undefined : node.ariaRole || (focusable ? 'button' : undefined)} aria-describedby={store.disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${store.flowId}`} diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index f90b367d..97c7fa8d 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -79,7 +79,8 @@ export type NodeBase< * When not specified, focusable nodes default to 'button' role. * @default "button" (for focusable nodes) */ - role?: NodeRole; + + ariaRole?: AriaRole; measured?: { width?: number; height?: number; @@ -187,4 +188,4 @@ export type Align = 'center' | 'start' | 'end'; export type NodeLookup = Map; export type ParentLookup = Map>; -export type NodeRole = 'button' | 'group' | 'listitem' | 'application' | 'region' | 'none' | null; +export type AriaRole = 'button' | 'group' | 'listitem' | 'application' | 'region' | 'none' | null; From 28b043060626fa5af8b630ef0f1b04c37e2166b3 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Mon, 2 Jun 2025 13:19:27 +0200 Subject: [PATCH 21/35] chore: set default role to group, add aria-roledescription --- packages/react/src/components/NodeWrapper/index.tsx | 3 ++- .../src/lib/components/NodeWrapper/NodeWrapper.svelte | 3 ++- packages/system/src/types/nodes.ts | 9 ++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 10909bcd..218ce700 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -191,7 +191,8 @@ export function NodeWrapper({ onDoubleClick={onDoubleClickHandler} onKeyDown={isFocusable ? onKeyDown : undefined} tabIndex={isFocusable ? 0 : undefined} - role={node.ariaRole === null ? undefined : node.ariaRole || (isFocusable ? 'button' : undefined)} + role={node.ariaRole === null ? undefined : 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/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index dce41767..3c7e7db9 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={node.ariaRole === null ? undefined : node.ariaRole || (focusable ? 'button' : undefined)} + role={node.ariaRole === null ? undefined : 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/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index 97c7fa8d..82446cd7 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -75,12 +75,15 @@ export type NodeBase< handles?: NodeHandle[]; /** * The ARIA role attribute for the node element, used for accessibility. - * Common values for nodes might be 'button', 'group', 'listitem', etc. - * When not specified, focusable nodes default to 'button' role. - * @default "button" (for focusable nodes) + * @default "group" */ ariaRole?: AriaRole; + /** + * A description of the node's role, used for accessibility. + * @default "node" + */ + ariaRoleDescription?: string; measured?: { width?: number; height?: number; From cb3e80dc54a62d1f30e61676dd1968f68c87896c Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Mon, 2 Jun 2025 14:35:44 +0200 Subject: [PATCH 22/35] fix(EdgeLabel): fix a11y error in svelte --- packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte b/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte index 187567d7..5ba2ba38 100644 --- a/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte +++ b/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte @@ -36,7 +36,8 @@ style:width={toPxString(width)} style:height={toPxString(height)} style:z-index={z} - role="button" + role="group" + aria-roledescription="edge label" tabindex="-1" onclick={() => { if (selectEdgeOnClick && id) store.handleEdgeSelection(id); From 679f8a94634acdbc0767b633b5fb29ee4da09506 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Mon, 2 Jun 2025 14:39:14 +0200 Subject: [PATCH 23/35] chore: add handle aria-label --- packages/svelte/src/lib/components/Handle/Handle.svelte | 3 +++ packages/system/src/constants.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte index fbc0ea6e..055012bb 100644 --- a/packages/svelte/src/lib/components/Handle/Handle.svelte +++ b/packages/svelte/src/lib/components/Handle/Handle.svelte @@ -44,6 +44,8 @@ ); let store = useStore(); + let labelConfig = $derived(store.labelConfig); + let prevConnections: Map | null = null; $effect.pre(() => { @@ -227,6 +229,7 @@ The Handle component is the part of a node that can be used to connect nodes. onkeypress={() => {}} {style} role="button" + aria-label={labelConfig[`handle.ariaLabel`]} tabindex="-1" {...rest} > diff --git a/packages/system/src/constants.ts b/packages/system/src/constants.ts index 8bbdd005..cce76df7 100644 --- a/packages/system/src/constants.ts +++ b/packages/system/src/constants.ts @@ -56,6 +56,9 @@ export const defaultLabelConfig = { // Mini map 'minimap.ariaLabel': 'Mini Map', + + // Handle + 'handle.ariaLabel': 'Handle', }; export type LabelConfig = Required; From 848b486b2201b650ecb3317f367a723edb2458e1 Mon Sep 17 00:00:00 2001 From: printer_scanner Date: Mon, 2 Jun 2025 14:43:06 +0200 Subject: [PATCH 24/35] Create dry-pianos-fail.md --- .changeset/dry-pianos-fail.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/dry-pianos-fail.md diff --git a/.changeset/dry-pianos-fail.md b/.changeset/dry-pianos-fail.md new file mode 100644 index 00000000..246cf930 --- /dev/null +++ b/.changeset/dry-pianos-fail.md @@ -0,0 +1,7 @@ +--- +"@xyflow/react": minor +"@xyflow/svelte": minor +"@xyflow/system": minor +--- + +feat: add `ariaRole` prop to nodes From 9f82b15d582196fa7e4f3ef576f4ce04cf40d6dd Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Tue, 3 Jun 2025 11:48:06 +0200 Subject: [PATCH 25/35] chore: add roles to edges --- .../src/components/EdgeWrapper/index.tsx | 23 ++++++++++--------- .../lib/components/EdgeLabel/EdgeLabel.svelte | 2 -- .../components/EdgeWrapper/EdgeWrapper.svelte | 3 ++- packages/system/src/types/edges.ts | 13 ++++++++++- packages/system/src/types/nodes.ts | 2 +- packages/system/src/types/utils.ts | 5 ++++ 6 files changed, 32 insertions(+), 16 deletions(-) diff --git a/packages/react/src/components/EdgeWrapper/index.tsx b/packages/react/src/components/EdgeWrapper/index.tsx index 72c5c2f8..18a8f3b8 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 === null ? undefined : edge.ariaRole || (isFocusable ? 'group' : 'img')} + aria-roledescription={edge.ariaRoleDescription || 'edge'} data-id={id} data-testid={`rf__edge-${id}`} aria-label={ diff --git a/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte b/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte index 5ba2ba38..83ce1411 100644 --- a/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte +++ b/packages/svelte/src/lib/components/EdgeLabel/EdgeLabel.svelte @@ -36,8 +36,6 @@ style:width={toPxString(width)} style:height={toPxString(height)} style:z-index={z} - role="group" - aria-roledescription="edge label" 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..c9f77d85 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 === null ? undefined : edge.ariaRole || (focusable ? 'group' : 'img')} + aria-roledescription={edge.ariaRoleDescription || 'edge'} onkeydown={focusable ? onkeydown : undefined} tabindex={focusable ? 0 : undefined} > diff --git a/packages/system/src/types/edges.ts b/packages/system/src/types/edges.ts index c0568200..ec83b256 100644 --- a/packages/system/src/types/edges.ts +++ b/packages/system/src/types/edges.ts @@ -1,4 +1,4 @@ -import { Position } from './utils'; +import { Position, AriaRole } from './utils'; export type EdgeBase< EdgeData extends Record = Record, @@ -40,6 +40,17 @@ export type EdgeBase< * This property sets the width of that invisible path. */ interactionWidth?: number; + /** + * The ARIA role attribute for the edge, used for accessibility. + * @default "group" + */ + + ariaRole?: AriaRole; + /** + * A description of the edge's, used for accessibility. + * @default "node" + */ + ariaRoleDescription?: string; }; export type SmoothStepPathOptions = { diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index 82446cd7..3f2978ad 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -1,5 +1,6 @@ import type { XYPosition, Position, CoordinateExtent, Handle } from '.'; import { Optional } from '../utils/types'; +import { AriaRole } from './utils'; /** * Framework independent node data structure. @@ -191,4 +192,3 @@ export type Align = 'center' | 'start' | 'end'; export type NodeLookup = Map; export type ParentLookup = Map>; -export type AriaRole = 'button' | 'group' | 'listitem' | 'application' | 'region' | 'none' | null; diff --git a/packages/system/src/types/utils.ts b/packages/system/src/types/utils.ts index 274bfe55..d46bc76c 100644 --- a/packages/system/src/types/utils.ts +++ b/packages/system/src/types/utils.ts @@ -56,3 +56,8 @@ export type Transform = [number, number, number]; * to represent an unbounded extent. */ export type CoordinateExtent = [[number, number], [number, number]]; + +/** + * The `AriaRole` type is used to define the role of an element in the accessibility tree. + */ +export type AriaRole = 'button' | 'group' | 'listitem' | 'application' | 'region' | 'none' | null; From 416f533622dad3113300036d34692a16bdf3f4dc Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Tue, 3 Jun 2025 12:59:43 +0200 Subject: [PATCH 26/35] fix: update default value --- packages/system/src/types/edges.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/system/src/types/edges.ts b/packages/system/src/types/edges.ts index ec83b256..a8446e00 100644 --- a/packages/system/src/types/edges.ts +++ b/packages/system/src/types/edges.ts @@ -48,7 +48,7 @@ export type EdgeBase< ariaRole?: AriaRole; /** * A description of the edge's, used for accessibility. - * @default "node" + * @default "edge" */ ariaRoleDescription?: string; }; From 6543ddc31c07c6e9d5a7e3affd42cf8618800182 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 3 Jun 2025 13:36:16 +0200 Subject: [PATCH 27/35] chore(labelconfig): cleanup --- examples/react/src/examples/A11y/index.tsx | 29 ++++++------ .../src/routes/examples/a11y/+page.svelte | 46 ++++++++----------- .../Controls/Controls.tsx | 3 +- .../additional-components/MiniMap/MiniMap.tsx | 4 +- .../src/components/NodeWrapper/index.tsx | 10 ++-- packages/react/src/index.ts | 1 + .../A11yDescriptions/A11yDescriptions.svelte | 9 ++-- .../components/NodeWrapper/NodeWrapper.svelte | 11 ++--- .../src/lib/container/SvelteFlow/types.ts | 2 +- packages/svelte/src/lib/index.ts | 3 +- packages/system/src/constants.ts | 6 +-- 11 files changed, 59 insertions(+), 65 deletions(-) diff --git a/examples/react/src/examples/A11y/index.tsx b/examples/react/src/examples/A11y/index.tsx index 0c921798..cafa3d7f 100644 --- a/examples/react/src/examples/A11y/index.tsx +++ b/examples/react/src/examples/A11y/index.tsx @@ -9,6 +9,7 @@ import { Node, Edge, OnNodeDrag, + LabelConfig, } from '@xyflow/react'; const onNodeDrag: OnNodeDrag = (_, node: Node, nodes: Node[]) => console.log('drag', node, nodes); @@ -43,6 +44,20 @@ const initialEdges: Edge[] = [ { id: 'e1-3', source: '1', target: '3' }, ]; +const labelConfig: Partial = { + 'a11yDescription.node.default': 'Custom Node Desc.', + 'a11yDescription.node.keyboardDisabled': 'Custom Keyboard Desc.', + 'a11yDescription.edge.default': 'Custom Edge Desc.', + 'a11yDescription.ariaLiveMessage': ({ direction, x, y }) => + `Custom Moved selected node ${direction}. New position, x: ${x}, y: ${y}`, + 'controls.ariaLabel': 'Custom Controls Aria Label', + 'controls.zoomin.title': 'Custom Zoom in', + 'controls.zoomout.title': 'Custom Zoom Out', + // 'controls.fitview.title': 'Custom Fit View', + 'controls.interactive.title': 'Custom Toggle Interactivity', + 'minimap.ariaLabel': 'Custom Aria Label', +}; + const A11y = () => { return ( { elevateEdgesOnSelect elevateNodesOnSelect={false} nodeDragThreshold={0} - labelConfig={{ - 'a11yDescription.node.default': 'Custom Node Desc.', - 'a11yDescription.node.keyboardDisabled': 'Custom Keyboard Desc.', - 'a11yDescription.edge.default': 'Custom Edge Desc.', - 'a11yDescription.ariaLiveMessage': (node: string, x: number, y: number) => - `Custom Moved selected node ${node}. New position, x: ${x}, y: ${y}`, - 'controls.ariaLabel': 'Custom Controls Aria Label', - 'controls.zoomin.title': 'Custom Zoom in', - 'controls.zoomout.title': 'Custom Zoom Out', - // 'controls.fitview.title': 'Custom Fit View', - 'controls.interactive.title': 'Custom Toggle Interactivity', - 'minimap.ariaLabel': 'Custom Aria Label', - }} + labelConfig={labelConfig} > diff --git a/examples/svelte/src/routes/examples/a11y/+page.svelte b/examples/svelte/src/routes/examples/a11y/+page.svelte index 171576db..98f0bc23 100644 --- a/examples/svelte/src/routes/examples/a11y/+page.svelte +++ b/examples/svelte/src/routes/examples/a11y/+page.svelte @@ -1,24 +1,17 @@ - - `Custom Moved selected node ${node}. New position, x: ${x}, y: ${y}`, - 'controls.ariaLabel': 'Svelte Custom Control Aria Label', - 'controls.zoomin.title': 'Svelte Custom Zoom in', - 'controls.zoomout.title': 'Svelte Custom Zoom Out', - // 'controls.fitview.title': 'Svelte Custom Fit View', - 'controls.interactive.title': 'Svelte Custom Toggle Interactivity', - 'minimap.ariaLabel': 'Svelte Custom Minimap', + 'a11yDescription.node.default': 'Svelte Custom Node Desc.', + 'a11yDescription.node.keyboardDisabled': 'Svelte Custom Keyboard Desc.', + 'a11yDescription.edge.default': 'Svelte Custom Edge Desc.', + 'a11yDescription.ariaLiveMessage': ({ direction, x, y }) => + `Custom Moved selected node ${direction}. New position, x: ${x}, y: ${y}`, + 'controls.ariaLabel': 'Svelte Custom Control Aria Label', + 'controls.zoomin.title': 'Svelte Custom Zoom in', + 'controls.zoomout.title': 'Svelte Custom Zoom Out', + // 'controls.fitview.title': 'Svelte Custom Fit View', + 'controls.interactive.title': 'Svelte Custom Toggle Interactivity', + 'minimap.ariaLabel': 'Svelte Custom Minimap' }} > diff --git a/packages/react/src/additional-components/Controls/Controls.tsx b/packages/react/src/additional-components/Controls/Controls.tsx index e6388e66..d92cff99 100644 --- a/packages/react/src/additional-components/Controls/Controls.tsx +++ b/packages/react/src/additional-components/Controls/Controls.tsx @@ -41,7 +41,6 @@ function ControlsComponent({ const store = useStoreApi(); const { isInteractive, minZoomReached, maxZoomReached, labelConfig } = useStore(selector, shallow); const { zoomIn, zoomOut, fitView } = useReactFlow(); - const effectiveAriaLabel = ariaLabel ?? labelConfig['controls.ariaLabel']; const onZoomInHandler = () => { zoomIn(); @@ -75,7 +74,7 @@ function ControlsComponent({ position={position} style={style} data-testid="rf__controls" - aria-label={effectiveAriaLabel} + aria-label={ariaLabel ?? labelConfig['controls.ariaLabel']} > {showZoom && ( <> diff --git a/packages/react/src/additional-components/MiniMap/MiniMap.tsx b/packages/react/src/additional-components/MiniMap/MiniMap.tsx index c48207ca..f23ff60f 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMap.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMap.tsx @@ -133,7 +133,7 @@ function MiniMapComponent({ }, []) : undefined; - const effectiveAriaLabel = ariaLabel ?? labelConfig['minimap.ariaLabel']; + const _ariaLabel = ariaLabel ?? labelConfig['minimap.ariaLabel']; return ( ({ ref={svg} onClick={onSvgClick} > - {effectiveAriaLabel && {effectiveAriaLabel}} + {_ariaLabel && {_ariaLabel}} onClick={onSvgNodeClick} diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index af067adc..d53ef414 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -144,11 +144,11 @@ export function NodeWrapper({ event.preventDefault(); store.setState({ - ariaLiveMessage: labelConfig['a11yDescription.ariaLiveMessage']( - event.key.replace('Arrow', '').toLowerCase(), - ~~internals.positionAbsolute.x, - ~~internals.positionAbsolute.y - ), + ariaLiveMessage: labelConfig['a11yDescription.ariaLiveMessage']({ + direction: event.key.replace('Arrow', '').toLowerCase(), + x: ~~internals.positionAbsolute.x, + y: ~~internals.positionAbsolute.y, + }), }); moveSelectedNodes({ diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 3397a677..f27dcd4a 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -108,6 +108,7 @@ export { type NoConnection, type NodeConnection, type OnReconnect, + type LabelConfig, } from '@xyflow/system'; // we need this workaround to prevent a duplicate identifier error diff --git a/packages/svelte/src/lib/components/A11yDescriptions/A11yDescriptions.svelte b/packages/svelte/src/lib/components/A11yDescriptions/A11yDescriptions.svelte index 875d7f83..dcfbf93f 100644 --- a/packages/svelte/src/lib/components/A11yDescriptions/A11yDescriptions.svelte +++ b/packages/svelte/src/lib/components/A11yDescriptions/A11yDescriptions.svelte @@ -1,5 +1,4 @@
              - {store.disableKeyboardA11y - ? store.labelConfig['a11yDescription.node.default'] - : store.labelConfig['a11yDescription.node.keyboardDisabled']} + {store.disableKeyboardA11y + ? store.labelConfig['a11yDescription.node.default'] + : store.labelConfig['a11yDescription.node.keyboardDisabled']}
              - {store.labelConfig['a11yDescription.edge.default']} + {store.labelConfig['a11yDescription.edge.default']}
              {#if !store.disableKeyboardA11y} diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index 5fd55e72..b7c33ae0 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -189,12 +189,11 @@ ) { // prevent default scrolling behavior on arrow key press when node is moved event.preventDefault(); - store.ariaLiveMessage = labelConfig['a11yDescription.ariaLiveMessage']( - event.key.replace('Arrow', '').toLowerCase(), - ~~node.internals.positionAbsolute.x, - ~~node.internals.positionAbsolute.y - ), - + store.ariaLiveMessage = labelConfig['a11yDescription.ariaLiveMessage']({ + direction: event.key.replace('Arrow', '').toLowerCase(), + x: ~~node.internals.positionAbsolute.x, + y: ~~node.internals.positionAbsolute.y + }); store.moveSelectedNodes(arrowKeyDiffs[event.key], event.shiftKey ? 4 : 1); } } diff --git a/packages/svelte/src/lib/container/SvelteFlow/types.ts b/packages/svelte/src/lib/container/SvelteFlow/types.ts index 03259006..331bee8d 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/types.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/types.ts @@ -476,5 +476,5 @@ export type SvelteFlowProps< * Configuration for customizable labels, descriptions, and UI text. Provided keys will override the corresponding defaults. * Allows localization, customization of ARIA descriptions, control labels, minimap labels, and other UI strings. */ - labelConfig?: LabelConfig; + labelConfig?: Partial; }; diff --git a/packages/svelte/src/lib/index.ts b/packages/svelte/src/lib/index.ts index 095c8b87..57f0c86e 100644 --- a/packages/svelte/src/lib/index.ts +++ b/packages/svelte/src/lib/index.ts @@ -112,7 +112,8 @@ export { type ResizeParamsWithDirection, type ResizeDragEvent, type IsValidConnection, - type NodeConnection + type NodeConnection, + type LabelConfig } from '@xyflow/system'; // system utils diff --git a/packages/system/src/constants.ts b/packages/system/src/constants.ts index cce76df7..a82d952e 100644 --- a/packages/system/src/constants.ts +++ b/packages/system/src/constants.ts @@ -44,8 +44,8 @@ export const defaultLabelConfig = { 'Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.', 'a11yDescription.edge.default': 'Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.', - 'a11yDescription.ariaLiveMessage': (node: string, x: number, y: number) => - `Moved selected node ${node}. New position, x: ${x}, y: ${y}`, + 'a11yDescription.ariaLiveMessage': ({ direction, x, y }: { direction: string; x: number; y: number }) => + `Moved selected node ${direction}. New position, x: ${x}, y: ${y}`, // Control elements 'controls.ariaLabel': 'Control Panel', @@ -61,4 +61,4 @@ export const defaultLabelConfig = { 'handle.ariaLabel': 'Handle', }; -export type LabelConfig = Required; +export type LabelConfig = typeof defaultLabelConfig; From 1a3e293bf99c26dcc955ee91008d7e4c4c227b0c Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 3 Jun 2025 13:40:42 +0200 Subject: [PATCH 28/35] chore(changeset): cleanup --- .changeset/friendly-monkeys-fry.md | 2 +- .changeset/grumpy-spiders-play.md | 2 +- .changeset/late-taxis-press.md | 2 +- .changeset/quiet-forks-visit.md | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.changeset/friendly-monkeys-fry.md b/.changeset/friendly-monkeys-fry.md index 5da27a70..d62a60eb 100644 --- a/.changeset/friendly-monkeys-fry.md +++ b/.changeset/friendly-monkeys-fry.md @@ -2,4 +2,4 @@ '@xyflow/svelte': patch --- -Fix data in EdgeProps not typed correctly +Fix data in `EdgeProps` that was not typed correctly diff --git a/.changeset/grumpy-spiders-play.md b/.changeset/grumpy-spiders-play.md index cf7d295f..2a3cedd6 100644 --- a/.changeset/grumpy-spiders-play.md +++ b/.changeset/grumpy-spiders-play.md @@ -1,7 +1,7 @@ --- '@xyflow/react': minor '@xyflow/svelte': minor -'@xyflow/system': minor +'@xyflow/system': patch --- Improve typing for Nodes diff --git a/.changeset/late-taxis-press.md b/.changeset/late-taxis-press.md index e48f797b..05def642 100644 --- a/.changeset/late-taxis-press.md +++ b/.changeset/late-taxis-press.md @@ -1,7 +1,7 @@ --- '@xyflow/react': minor '@xyflow/svelte': minor -'@xyflow/system': minor +'@xyflow/system': patch --- Add an `ease` and `interpolate` option to all function that alter the viewport diff --git a/.changeset/quiet-forks-visit.md b/.changeset/quiet-forks-visit.md index f2b27615..5706b767 100644 --- a/.changeset/quiet-forks-visit.md +++ b/.changeset/quiet-forks-visit.md @@ -1,8 +1,8 @@ --- "@xyflow/react": minor "@xyflow/svelte": minor -"@xyflow/system": minor +"@xyflow/system": patch --- -feat(a11y): add generalized labelConfig for customizable UI text and localization & ARIA descriptions +Add `labelConfig` prop for customizing UI text like aria labels and descriptions. From 65e912a75545c08ef82608a337b9b92eda830fbe Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 3 Jun 2025 14:04:48 +0200 Subject: [PATCH 29/35] chore(a11y): cleanup --- .../react/src/components/A11yDescriptions/index.tsx | 11 ++++------- packages/react/src/components/NodeWrapper/index.tsx | 5 +++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/react/src/components/A11yDescriptions/index.tsx b/packages/react/src/components/A11yDescriptions/index.tsx index 4931ada0..6f08447e 100644 --- a/packages/react/src/components/A11yDescriptions/index.tsx +++ b/packages/react/src/components/A11yDescriptions/index.tsx @@ -36,18 +36,15 @@ function AriaLiveMessage({ rfId }: { rfId: string }) { export function A11yDescriptions({ rfId, disableKeyboardA11y }: { rfId: string; disableKeyboardA11y: boolean }) { const labelConfig = useStore(labelConfigSelector); - const nodeDesc = disableKeyboardA11y - ? labelConfig['a11yDescription.node.default'] - : labelConfig['a11yDescription.node.keyboardDisabled']; - const edgeDesc = labelConfig['a11yDescription.edge.default']; - return ( <>
              - {nodeDesc} + {disableKeyboardA11y + ? labelConfig['a11yDescription.node.default'] + : labelConfig['a11yDescription.node.keyboardDisabled']}
              - {edgeDesc} + {labelConfig['a11yDescription.edge.default']}
              {!disableKeyboardA11y && } diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index d53ef414..8ac50a09 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -40,7 +40,7 @@ export function NodeWrapper({ nodeClickDistance, onError, }: NodeWrapperProps) { - const { node, internals, isParent, labelConfig } = useStore((s) => { + const { node, internals, isParent } = useStore((s) => { const node = s.nodeLookup.get(id)! as InternalNode; const isParent = s.parentLookup.has(id); @@ -48,7 +48,6 @@ export function NodeWrapper({ node, internals: node.internals, isParent, - labelConfig: s.labelConfig, }; }, shallow); @@ -143,6 +142,8 @@ export function NodeWrapper({ // prevent default scrolling behavior on arrow key press when node is moved event.preventDefault(); + const { labelConfig } = store.getState(); + store.setState({ ariaLiveMessage: labelConfig['a11yDescription.ariaLiveMessage']({ direction: event.key.replace('Arrow', '').toLowerCase(), From 96eba257aa4276ee8354bb213be13ea07acad041 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Tue, 3 Jun 2025 14:41:25 +0200 Subject: [PATCH 30/35] chore: use svelte and react types for AriaRole --- packages/react/src/types/edges.ts | 14 +++++++++++++- packages/react/src/types/nodes.ts | 8 +++++++- packages/svelte/src/lib/types/edges.ts | 1 + packages/svelte/src/lib/types/nodes.ts | 7 ++++++- packages/system/src/types/edges.ts | 8 +------- packages/system/src/types/nodes.ts | 7 ------- packages/system/src/types/utils.ts | 5 ----- 7 files changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/react/src/types/edges.ts b/packages/react/src/types/edges.ts index a5c22c75..a4325ebc 100644 --- a/packages/react/src/types/edges.ts +++ b/packages/react/src/types/edges.ts @@ -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, @@ -176,6 +183,11 @@ export type BaseEdgeProps = Omit, 'd' | 'path' | ' * element in a separate SVG document or element. */ markerEnd?: string; + /** + * The ARIA role attribute for the edge, used for accessibility. + * @default "group" + */ + ariaRole?: AriaRole; }; /** 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/types/edges.ts b/packages/svelte/src/lib/types/edges.ts index c903512a..2b56f48b 100644 --- a/packages/svelte/src/lib/types/edges.ts +++ b/packages/svelte/src/lib/types/edges.ts @@ -46,6 +46,7 @@ export type BaseEdgeProps = Pick< * @example 'url(#arrow)' */ markerEnd?: string; + ariaRole?: HTMLAttributes['role']; class?: ClassValue; } & HTMLAttributes; 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 a8446e00..3e69bf4c 100644 --- a/packages/system/src/types/edges.ts +++ b/packages/system/src/types/edges.ts @@ -1,4 +1,4 @@ -import { Position, AriaRole } from './utils'; +import { Position } from './utils'; export type EdgeBase< EdgeData extends Record = Record, @@ -40,12 +40,6 @@ export type EdgeBase< * This property sets the width of that invisible path. */ interactionWidth?: number; - /** - * The ARIA role attribute for the edge, used for accessibility. - * @default "group" - */ - - ariaRole?: AriaRole; /** * A description of the edge's, used for accessibility. * @default "edge" diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index 3f2978ad..49012024 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -1,6 +1,5 @@ import type { XYPosition, Position, CoordinateExtent, Handle } from '.'; import { Optional } from '../utils/types'; -import { AriaRole } from './utils'; /** * Framework independent node data structure. @@ -74,12 +73,6 @@ export type NodeBase< */ origin?: NodeOrigin; handles?: NodeHandle[]; - /** - * The ARIA role attribute for the node element, used for accessibility. - * @default "group" - */ - - ariaRole?: AriaRole; /** * A description of the node's role, used for accessibility. * @default "node" diff --git a/packages/system/src/types/utils.ts b/packages/system/src/types/utils.ts index d46bc76c..274bfe55 100644 --- a/packages/system/src/types/utils.ts +++ b/packages/system/src/types/utils.ts @@ -56,8 +56,3 @@ export type Transform = [number, number, number]; * to represent an unbounded extent. */ export type CoordinateExtent = [[number, number], [number, number]]; - -/** - * The `AriaRole` type is used to define the role of an element in the accessibility tree. - */ -export type AriaRole = 'button' | 'group' | 'listitem' | 'application' | 'region' | 'none' | null; From 79db292c4d4ce5937ccd900f7295fb242c5f40f9 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Tue, 3 Jun 2025 15:13:32 +0200 Subject: [PATCH 31/35] fix: move ariaRole --- packages/react/src/types/edges.ts | 10 +++++----- packages/svelte/src/lib/types/edges.ts | 6 +++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/react/src/types/edges.ts b/packages/react/src/types/edges.ts index a4325ebc..83d74c2d 100644 --- a/packages/react/src/types/edges.ts +++ b/packages/react/src/types/edges.ts @@ -64,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 = Record> = Edge< @@ -183,11 +188,6 @@ export type BaseEdgeProps = Omit, 'd' | 'path' | ' * element in a separate SVG document or element. */ markerEnd?: string; - /** - * The ARIA role attribute for the edge, used for accessibility. - * @default "group" - */ - ariaRole?: AriaRole; }; /** diff --git a/packages/svelte/src/lib/types/edges.ts b/packages/svelte/src/lib/types/edges.ts index 2b56f48b..182f1e48 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< @@ -46,7 +51,6 @@ export type BaseEdgeProps = Pick< * @example 'url(#arrow)' */ markerEnd?: string; - ariaRole?: HTMLAttributes['role']; class?: ClassValue; } & HTMLAttributes; From c9838359a197c8e812128888e6ff49cecfcd2599 Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Tue, 3 Jun 2025 15:21:02 +0200 Subject: [PATCH 32/35] chore: remove null check --- packages/react/src/components/EdgeWrapper/index.tsx | 2 +- packages/react/src/components/NodeWrapper/index.tsx | 2 +- .../svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte | 2 +- .../svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react/src/components/EdgeWrapper/index.tsx b/packages/react/src/components/EdgeWrapper/index.tsx index 18a8f3b8..a5db4441 100644 --- a/packages/react/src/components/EdgeWrapper/index.tsx +++ b/packages/react/src/components/EdgeWrapper/index.tsx @@ -198,7 +198,7 @@ export function EdgeWrapper({ onMouseLeave={onEdgeMouseLeave} onKeyDown={isFocusable ? onKeyDown : undefined} tabIndex={isFocusable ? 0 : undefined} - role={edge.ariaRole === null ? undefined : edge.ariaRole || (isFocusable ? 'group' : 'img')} + role={edge.ariaRole ?? (isFocusable ? 'group' : 'img')} aria-roledescription={edge.ariaRoleDescription || 'edge'} data-id={id} data-testid={`rf__edge-${id}`} diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 218ce700..11f65d42 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -191,7 +191,7 @@ export function NodeWrapper({ onDoubleClick={onDoubleClickHandler} onKeyDown={isFocusable ? onKeyDown : undefined} tabIndex={isFocusable ? 0 : undefined} - role={node.ariaRole === null ? undefined : node.ariaRole || (isFocusable ? 'group' : 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/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte b/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte index c9f77d85..c63d85e5 100644 --- a/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte +++ b/packages/svelte/src/lib/components/EdgeWrapper/EdgeWrapper.svelte @@ -137,7 +137,7 @@ ? ariaLabel : `Edge from ${source} to ${target}`} aria-describedby={focusable ? `${ARIA_EDGE_DESC_KEY}-${store.flowId}` : undefined} - role={edge.ariaRole === null ? undefined : edge.ariaRole || (focusable ? 'group' : '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 3c7e7db9..e8a11de9 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -252,7 +252,7 @@ : undefined} onkeydown={focusable ? onKeyDown : undefined} tabIndex={focusable ? 0 : undefined} - role={node.ariaRole === null ? undefined : node.ariaRole || (focusable ? 'group' : undefined)} + role={node.ariaRole ?? (focusable ? 'group' : undefined)} aria-roledescription={node.ariaRoleDescription || 'node'} aria-describedby={store.disableKeyboardA11y ? undefined From 13b64afbaf331bb00e7b8fcc7e2a4f30e71412af Mon Sep 17 00:00:00 2001 From: Abbey Yacoe Date: Tue, 3 Jun 2025 15:42:57 +0200 Subject: [PATCH 33/35] chore: rename LabelConfig -> AriaLabelConfig, update dict names --- .changeset/quiet-forks-visit.md | 2 +- examples/react/src/examples/A11y/index.tsx | 20 ++++++++--------- .../src/routes/examples/a11y/+page.svelte | 16 +++++++------- .../Controls/Controls.tsx | 22 +++++++++---------- .../additional-components/MiniMap/MiniMap.tsx | 6 ++--- .../src/components/A11yDescriptions/index.tsx | 10 ++++----- .../src/components/NodeWrapper/index.tsx | 4 ++-- .../src/components/StoreUpdater/index.tsx | 8 +++---- .../react/src/container/ReactFlow/index.tsx | 4 ++-- packages/react/src/index.ts | 2 +- packages/react/src/store/initialState.ts | 4 ++-- packages/react/src/types/component-props.ts | 4 ++-- packages/react/src/types/store.ts | 4 ++-- .../A11yDescriptions/A11yDescriptions.svelte | 6 ++--- .../src/lib/components/Handle/Handle.svelte | 4 ++-- .../components/NodeWrapper/NodeWrapper.svelte | 4 ++-- .../lib/container/SvelteFlow/Wrapper.svelte | 2 +- .../src/lib/container/SvelteFlow/types.ts | 4 ++-- packages/svelte/src/lib/index.ts | 2 +- .../src/lib/plugins/Controls/Controls.svelte | 20 ++++++++--------- .../src/lib/plugins/Minimap/Minimap.svelte | 6 ++--- .../src/lib/store/initial-store.svelte.ts | 8 ++++--- packages/system/src/constants.ts | 18 +++++++-------- packages/system/src/utils/general.ts | 6 ++--- 24 files changed, 94 insertions(+), 92 deletions(-) diff --git a/.changeset/quiet-forks-visit.md b/.changeset/quiet-forks-visit.md index 5706b767..89c3a0ee 100644 --- a/.changeset/quiet-forks-visit.md +++ b/.changeset/quiet-forks-visit.md @@ -4,5 +4,5 @@ "@xyflow/system": patch --- -Add `labelConfig` prop for customizing UI text like aria labels and descriptions. +Add `ariaLabelConfig` prop for customizing UI text like aria labels and descriptions. diff --git a/examples/react/src/examples/A11y/index.tsx b/examples/react/src/examples/A11y/index.tsx index cafa3d7f..b0e5e429 100644 --- a/examples/react/src/examples/A11y/index.tsx +++ b/examples/react/src/examples/A11y/index.tsx @@ -9,7 +9,7 @@ import { Node, Edge, OnNodeDrag, - LabelConfig, + AriaLabelConfig, } from '@xyflow/react'; const onNodeDrag: OnNodeDrag = (_, node: Node, nodes: Node[]) => console.log('drag', node, nodes); @@ -44,17 +44,17 @@ const initialEdges: Edge[] = [ { id: 'e1-3', source: '1', target: '3' }, ]; -const labelConfig: Partial = { - 'a11yDescription.node.default': 'Custom Node Desc.', - 'a11yDescription.node.keyboardDisabled': 'Custom Keyboard Desc.', - 'a11yDescription.edge.default': 'Custom Edge Desc.', +const ariaLabelConfig: Partial = { + 'node.a11yDescription.default': 'Custom Node Desc.', + 'node.a11yDescription.keyboardDisabled': 'Custom Keyboard Desc.', + 'edge.a11yDescription.default': 'Custom Edge Desc.', 'a11yDescription.ariaLiveMessage': ({ direction, x, y }) => `Custom Moved selected node ${direction}. New position, x: ${x}, y: ${y}`, 'controls.ariaLabel': 'Custom Controls Aria Label', - 'controls.zoomin.title': 'Custom Zoom in', - 'controls.zoomout.title': 'Custom Zoom Out', - // 'controls.fitview.title': 'Custom Fit View', - 'controls.interactive.title': 'Custom Toggle Interactivity', + 'controls.zoomin.ariaLabel': 'Custom Zoom in', + 'controls.zoomout.ariaLabel': 'Custom Zoom Out', + // 'controls.fitview.ariaLabel': 'Custom Fit View', + 'controls.interactive.ariaLabel': 'Custom Toggle Interactivity', 'minimap.ariaLabel': 'Custom Aria Label', }; @@ -76,7 +76,7 @@ const A11y = () => { elevateEdgesOnSelect elevateNodesOnSelect={false} nodeDragThreshold={0} - labelConfig={labelConfig} + ariaLabelConfig={ariaLabelConfig} > diff --git a/examples/svelte/src/routes/examples/a11y/+page.svelte b/examples/svelte/src/routes/examples/a11y/+page.svelte index 98f0bc23..a4a5afd8 100644 --- a/examples/svelte/src/routes/examples/a11y/+page.svelte +++ b/examples/svelte/src/routes/examples/a11y/+page.svelte @@ -25,17 +25,17 @@ bind:nodes bind:edges fitView - labelConfig={{ - 'a11yDescription.node.default': 'Svelte Custom Node Desc.', - 'a11yDescription.node.keyboardDisabled': 'Svelte Custom Keyboard Desc.', - 'a11yDescription.edge.default': 'Svelte Custom Edge Desc.', + ariaLabelConfig={{ + 'node.a11yDescription.default': 'Svelte Custom Node Desc.', + 'node.a11yDescription.keyboardDisabled': 'Svelte Custom Keyboard Desc.', + 'edge.a11yDescription.default': 'Svelte Custom Edge Desc.', 'a11yDescription.ariaLiveMessage': ({ direction, x, y }) => `Custom Moved selected node ${direction}. New position, x: ${x}, y: ${y}`, 'controls.ariaLabel': 'Svelte Custom Control Aria Label', - 'controls.zoomin.title': 'Svelte Custom Zoom in', - 'controls.zoomout.title': 'Svelte Custom Zoom Out', - // 'controls.fitview.title': 'Svelte Custom Fit View', - 'controls.interactive.title': 'Svelte Custom Toggle Interactivity', + 'controls.zoomin.ariaLabel': 'Svelte Custom Zoom in', + 'controls.zoomout.ariaLabel': 'Svelte Custom Zoom Out', + // 'controls.fitview.ariaLabel': 'Svelte Custom Fit View', + 'controls.interactive.ariaLabel': 'Svelte Custom Toggle Interactivity', 'minimap.ariaLabel': 'Svelte Custom Minimap' }} > diff --git a/packages/react/src/additional-components/Controls/Controls.tsx b/packages/react/src/additional-components/Controls/Controls.tsx index d92cff99..ccf9c29b 100644 --- a/packages/react/src/additional-components/Controls/Controls.tsx +++ b/packages/react/src/additional-components/Controls/Controls.tsx @@ -19,7 +19,7 @@ const selector = (s: ReactFlowState) => ({ isInteractive: s.nodesDraggable || s.nodesConnectable || s.elementsSelectable, minZoomReached: s.transform[2] <= s.minZoom, maxZoomReached: s.transform[2] >= s.maxZoom, - labelConfig: s.labelConfig, + ariaLabelConfig: s.ariaLabelConfig, }); function ControlsComponent({ @@ -39,7 +39,7 @@ function ControlsComponent({ 'aria-label': ariaLabel, }: ControlProps) { const store = useStoreApi(); - const { isInteractive, minZoomReached, maxZoomReached, labelConfig } = useStore(selector, shallow); + const { isInteractive, minZoomReached, maxZoomReached, ariaLabelConfig } = useStore(selector, shallow); const { zoomIn, zoomOut, fitView } = useReactFlow(); const onZoomInHandler = () => { @@ -74,15 +74,15 @@ function ControlsComponent({ position={position} style={style} data-testid="rf__controls" - aria-label={ariaLabel ?? labelConfig['controls.ariaLabel']} + aria-label={ariaLabel ?? ariaLabelConfig['controls.ariaLabel']} > {showZoom && ( <> @@ -90,8 +90,8 @@ function ControlsComponent({ @@ -102,8 +102,8 @@ function ControlsComponent({ @@ -112,8 +112,8 @@ function ControlsComponent({ {isInteractive ? : } diff --git a/packages/react/src/additional-components/MiniMap/MiniMap.tsx b/packages/react/src/additional-components/MiniMap/MiniMap.tsx index f23ff60f..891f280e 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMap.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMap.tsx @@ -36,7 +36,7 @@ const selector = (s: ReactFlowState) => { translateExtent: s.translateExtent, flowWidth: s.width, flowHeight: s.height, - labelConfig: s.labelConfig, + ariaLabelConfig: s.ariaLabelConfig, }; }; @@ -70,7 +70,7 @@ function MiniMapComponent({ }: MiniMapProps) { const store = useStoreApi(); const svg = useRef(null); - const { boundingRect, viewBB, rfId, panZoom, translateExtent, flowWidth, flowHeight, labelConfig } = useStore( + const { boundingRect, viewBB, rfId, panZoom, translateExtent, flowWidth, flowHeight, ariaLabelConfig } = useStore( selector, shallow ); @@ -133,7 +133,7 @@ function MiniMapComponent({ }, []) : undefined; - const _ariaLabel = ariaLabel ?? labelConfig['minimap.ariaLabel']; + const _ariaLabel = ariaLabel ?? ariaLabelConfig['minimap.ariaLabel']; return ( s.ariaLiveMessage; -const labelConfigSelector = (s: ReactFlowState) => s.labelConfig; +const ariaLabelConfigSelector = (s: ReactFlowState) => s.ariaLabelConfig; function AriaLiveMessage({ rfId }: { rfId: string }) { const ariaLiveMessage = useStore(ariaLiveSelector); @@ -34,17 +34,17 @@ function AriaLiveMessage({ rfId }: { rfId: string }) { } export function A11yDescriptions({ rfId, disableKeyboardA11y }: { rfId: string; disableKeyboardA11y: boolean }) { - const labelConfig = useStore(labelConfigSelector); + const ariaLabelConfig = useStore(ariaLabelConfigSelector); return ( <>
              {disableKeyboardA11y - ? labelConfig['a11yDescription.node.default'] - : labelConfig['a11yDescription.node.keyboardDisabled']} + ? ariaLabelConfig['node.a11yDescription.default'] + : ariaLabelConfig['node.a11yDescription.keyboardDisabled']}
              - {labelConfig['a11yDescription.edge.default']} + {ariaLabelConfig['edge.a11yDescription.default']}
              {!disableKeyboardA11y && } diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 8ac50a09..93152a98 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -142,10 +142,10 @@ export function NodeWrapper({ // prevent default scrolling behavior on arrow key press when node is moved event.preventDefault(); - const { labelConfig } = store.getState(); + const { ariaLabelConfig } = store.getState(); store.setState({ - ariaLiveMessage: labelConfig['a11yDescription.ariaLiveMessage']({ + ariaLiveMessage: ariaLabelConfig['a11yDescription.ariaLiveMessage']({ direction: event.key.replace('Arrow', '').toLowerCase(), x: ~~internals.positionAbsolute.x, y: ~~internals.positionAbsolute.y, diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index d2fb3cae..95b713af 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -5,7 +5,7 @@ */ import { useEffect, useRef } from 'react'; import { shallow } from 'zustand/shallow'; -import { infiniteExtent, type CoordinateExtent, mergeLabelConfig, LabelConfig } from '@xyflow/system'; +import { infiniteExtent, type CoordinateExtent, mergeAriaLabelConfig, AriaLabelConfig } from '@xyflow/system'; import { useStore, useStoreApi } from '../../hooks/useStore'; import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } from '../../types'; @@ -68,7 +68,7 @@ const reactFlowFieldsToTrack = [ 'debug', 'autoPanSpeed', 'paneClickDistance', - 'labelConfig', + 'ariaLabelConfig', ] as const; type ReactFlowFieldsToTrack = (typeof reactFlowFieldsToTrack)[number]; @@ -158,8 +158,8 @@ export function StoreUpdater( colorMode = 'light', debug, onScroll, - labelConfig, + ariaLabelConfig, ...rest }: ReactFlowProps, ref: ForwardedRef @@ -306,7 +306,7 @@ function ReactFlow( onBeforeDelete={onBeforeDelete} paneClickDistance={paneClickDistance} debug={debug} - labelConfig={labelConfig} + ariaLabelConfig={ariaLabelConfig} /> onSelectionChange={onSelectionChange} /> {children} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index f27dcd4a..305acea7 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -108,7 +108,7 @@ export { type NoConnection, type NodeConnection, type OnReconnect, - type LabelConfig, + type AriaLabelConfig, } from '@xyflow/system'; // we need this workaround to prevent a duplicate identifier error diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 186f1eca..15bfc1d8 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -10,7 +10,7 @@ import { NodeOrigin, initialConnection, CoordinateExtent, - defaultLabelConfig, + defaultAriaLabelConfig, } from '@xyflow/system'; import type { Edge, FitViewOptions, InternalNode, Node, ReactFlowStore } from '../types'; @@ -142,7 +142,7 @@ const getInitialState = ({ lib: 'react', debug: false, - labelConfig: defaultLabelConfig, + ariaLabelConfig: defaultAriaLabelConfig, }; }; diff --git a/packages/react/src/types/component-props.ts b/packages/react/src/types/component-props.ts index 315168a9..d088c26b 100644 --- a/packages/react/src/types/component-props.ts +++ b/packages/react/src/types/component-props.ts @@ -21,7 +21,7 @@ import type { ColorMode, SnapGrid, OnReconnect, - LabelConfig, + AriaLabelConfig, } from '@xyflow/system'; import type { @@ -671,5 +671,5 @@ export interface ReactFlowProps; + ariaLabelConfig?: Partial; } diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 5b3dec5b..ba909869 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -28,7 +28,7 @@ import { type NodeChange, type EdgeChange, type ParentLookup, - type LabelConfig, + type AriaLabelConfig, } from '@xyflow/system'; import type { @@ -148,7 +148,7 @@ export type ReactFlowStore = { diff --git a/packages/svelte/src/lib/components/A11yDescriptions/A11yDescriptions.svelte b/packages/svelte/src/lib/components/A11yDescriptions/A11yDescriptions.svelte index dcfbf93f..24b49111 100644 --- a/packages/svelte/src/lib/components/A11yDescriptions/A11yDescriptions.svelte +++ b/packages/svelte/src/lib/components/A11yDescriptions/A11yDescriptions.svelte @@ -8,11 +8,11 @@
              {store.disableKeyboardA11y - ? store.labelConfig['a11yDescription.node.default'] - : store.labelConfig['a11yDescription.node.keyboardDisabled']} + ? store.ariaLabelConfig['node.a11yDescription.default'] + : store.ariaLabelConfig['node.a11yDescription.keyboardDisabled']}
              - {store.labelConfig['a11yDescription.edge.default']} + {store.ariaLabelConfig['edge.a11yDescription.default']}
              {#if !store.disableKeyboardA11y} diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte index 055012bb..71ccbd5a 100644 --- a/packages/svelte/src/lib/components/Handle/Handle.svelte +++ b/packages/svelte/src/lib/components/Handle/Handle.svelte @@ -44,7 +44,7 @@ ); let store = useStore(); - let labelConfig = $derived(store.labelConfig); + let ariaLabelConfig = $derived(store.ariaLabelConfig); let prevConnections: Map | null = null; @@ -229,7 +229,7 @@ The Handle component is the part of a node that can be used to connect nodes. onkeypress={() => {}} {style} role="button" - aria-label={labelConfig[`handle.ariaLabel`]} + aria-label={ariaLabelConfig[`handle.ariaLabel`]} tabindex="-1" {...rest} > diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index b7c33ae0..850d1d8d 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -85,7 +85,7 @@ let prevTargetPosition: Position | undefined = targetPosition; let NodeComponent = $derived(store.nodeTypes[type] ?? DefaultNode); - let labelConfig = $derived(store.labelConfig); + let ariaLabelConfig = $derived(store.ariaLabelConfig); let connectableContext: ConnectableContext = { get value() { @@ -189,7 +189,7 @@ ) { // prevent default scrolling behavior on arrow key press when node is moved event.preventDefault(); - store.ariaLiveMessage = labelConfig['a11yDescription.ariaLiveMessage']({ + store.ariaLiveMessage = ariaLabelConfig['a11yDescription.ariaLiveMessage']({ direction: event.key.replace('Arrow', '').toLowerCase(), x: ~~node.internals.positionAbsolute.x, y: ~~node.internals.positionAbsolute.y diff --git a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte index 3734cb25..0f7e39f6 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/Wrapper.svelte @@ -92,7 +92,7 @@ noDragClass, noPanClass, noWheelClass, - labelConfig, + ariaLabelConfig, ...divAttributes } = $derived(rest); /* eslint-enable @typescript-eslint/no-unused-vars */ diff --git a/packages/svelte/src/lib/container/SvelteFlow/types.ts b/packages/svelte/src/lib/container/SvelteFlow/types.ts index 331bee8d..38e07f4a 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/types.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/types.ts @@ -21,7 +21,7 @@ import type { OnReconnect, OnReconnectStart, OnReconnectEnd, - LabelConfig + AriaLabelConfig } from '@xyflow/system'; import type { @@ -476,5 +476,5 @@ export type SvelteFlowProps< * Configuration for customizable labels, descriptions, and UI text. Provided keys will override the corresponding defaults. * Allows localization, customization of ARIA descriptions, control labels, minimap labels, and other UI strings. */ - labelConfig?: Partial; + ariaLabelConfig?: Partial; }; diff --git a/packages/svelte/src/lib/index.ts b/packages/svelte/src/lib/index.ts index 57f0c86e..2fc0befc 100644 --- a/packages/svelte/src/lib/index.ts +++ b/packages/svelte/src/lib/index.ts @@ -113,7 +113,7 @@ export { type ResizeDragEvent, type IsValidConnection, type NodeConnection, - type LabelConfig + type AriaLabelConfig } from '@xyflow/system'; // system utils diff --git a/packages/svelte/src/lib/plugins/Controls/Controls.svelte b/packages/svelte/src/lib/plugins/Controls/Controls.svelte index db1f2a57..4225bd4e 100644 --- a/packages/svelte/src/lib/plugins/Controls/Controls.svelte +++ b/packages/svelte/src/lib/plugins/Controls/Controls.svelte @@ -46,7 +46,7 @@ ); let minZoomReached = $derived(store.viewport.zoom <= store.minZoom); let maxZoomReached = $derived(store.viewport.zoom >= store.maxZoom); - let labelConfig = $derived(store.labelConfig); + let ariaLabelConfig = $derived(store.ariaLabelConfig); let orientationClass = $derived(orientation === 'horizontal' ? 'horizontal' : 'vertical'); const onZoomInHandler = () => { @@ -73,7 +73,7 @@ class={['svelte-flow__controls', orientationClass, className]} {position} data-testid="svelte-flow__controls" - aria-label={labelConfig['controls.ariaLabel']} + aria-label={ariaLabelConfig['controls.ariaLabel']} {style} {...rest} > @@ -84,8 +84,8 @@ @@ -94,8 +94,8 @@ @@ -106,8 +106,8 @@ @@ -117,8 +117,8 @@ {#if isInteractive}{:else}{/if} diff --git a/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte b/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte index d31f5771..ac346066 100644 --- a/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte +++ b/packages/svelte/src/lib/plugins/Minimap/Minimap.svelte @@ -42,7 +42,7 @@ }: MiniMapProps = $props(); let store = $derived(useStore()); - let labelConfig = $derived(store.labelConfig); + let ariaLabelConfig = $derived(store.ariaLabelConfig); const nodeColorFunc = nodeColor === undefined ? undefined : getAttrFunction(nodeColor); const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor); @@ -114,8 +114,8 @@ zoomable }} > - {#if ariaLabel ?? labelConfig['minimap.ariaLabel']} - {ariaLabel ?? labelConfig['minimap.ariaLabel']} + {#if ariaLabel ?? ariaLabelConfig['minimap.ariaLabel']} + {ariaLabel ?? ariaLabelConfig['minimap.ariaLabel']} {/if} {#each store.nodes as userNode (userNode.id)} diff --git a/packages/svelte/src/lib/store/initial-store.svelte.ts b/packages/svelte/src/lib/store/initial-store.svelte.ts index f5f0a356..7971f320 100644 --- a/packages/svelte/src/lib/store/initial-store.svelte.ts +++ b/packages/svelte/src/lib/store/initial-store.svelte.ts @@ -7,7 +7,7 @@ import { getViewportForBounds, updateConnectionLookup, initialConnection, - mergeLabelConfig, + mergeAriaLabelConfig, type SelectionRect, type SnapGrid, type MarkerProps, @@ -34,7 +34,7 @@ import { type OnReconnect, type OnReconnectStart, type OnReconnectEnd, - type LabelConfig + type AriaLabelConfig } from '@xyflow/system'; import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; @@ -291,7 +291,9 @@ export function getInitialStore `Moved selected node ${direction}. New position, x: ${x}, y: ${y}`, // Control elements 'controls.ariaLabel': 'Control Panel', - 'controls.zoomin.title': 'Zoom In', - 'controls.zoomout.title': 'Zoom Out', - 'controls.fitview.title': 'Fit View', - 'controls.interactive.title': 'Toggle Interactivity', + 'controls.zoomin.ariaLabel': 'Zoom In', + 'controls.zoomout.ariaLabel': 'Zoom Out', + 'controls.fitview.ariaLabel': 'Fit View', + 'controls.interactive.ariaLabel': 'Toggle Interactivity', // Mini map 'minimap.ariaLabel': 'Mini Map', @@ -61,4 +61,4 @@ export const defaultLabelConfig = { 'handle.ariaLabel': 'Handle', }; -export type LabelConfig = typeof defaultLabelConfig; +export type AriaLabelConfig = typeof defaultAriaLabelConfig; diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index 32623057..fdfd6b75 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -16,7 +16,7 @@ import type { import { type Viewport } from '../types'; import { getNodePositionWithOrigin, isInternalNodeBase } from './graph'; -import { defaultLabelConfig, type LabelConfig } from '../constants'; +import { defaultAriaLabelConfig, type AriaLabelConfig } from '../constants'; export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max); @@ -421,6 +421,6 @@ export function withResolvers(): { return { promise, resolve, reject }; } -export function mergeLabelConfig(partial?: Partial): LabelConfig { - return { ...defaultLabelConfig, ...(partial || {}) }; +export function mergeAriaLabelConfig(partial?: Partial): AriaLabelConfig { + return { ...defaultAriaLabelConfig, ...(partial || {}) }; } From d5dd91b920ac9310ba6b4f4e9e179bcdcce0683e Mon Sep 17 00:00:00 2001 From: Moritz Klack Date: Tue, 3 Jun 2025 20:54:51 +0200 Subject: [PATCH 34/35] Update dry-pianos-fail.md --- .changeset/dry-pianos-fail.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/dry-pianos-fail.md b/.changeset/dry-pianos-fail.md index 246cf930..d8c7acf4 100644 --- a/.changeset/dry-pianos-fail.md +++ b/.changeset/dry-pianos-fail.md @@ -1,7 +1,7 @@ --- "@xyflow/react": minor "@xyflow/svelte": minor -"@xyflow/system": minor +"@xyflow/system": patch --- -feat: add `ariaRole` prop to nodes +Add `ariaRole` prop to nodes and edges From 406c09aeff7e6bb6c45d0b88feda0ea4994dc3f4 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 3 Jun 2025 22:08:04 +0200 Subject: [PATCH 35/35] chore(a11y): cleanup config keys --- examples/react/src/examples/A11y/index.tsx | 10 +++++----- .../svelte/src/routes/examples/a11y/+page.svelte | 10 +++++----- .../src/additional-components/Controls/Controls.tsx | 12 ++++++------ packages/react/src/components/NodeWrapper/index.tsx | 2 +- .../lib/components/NodeWrapper/NodeWrapper.svelte | 2 +- .../svelte/src/lib/plugins/Controls/Controls.svelte | 12 ++++++------ packages/system/src/constants.ts | 10 +++++----- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/examples/react/src/examples/A11y/index.tsx b/examples/react/src/examples/A11y/index.tsx index b0e5e429..f7f0b5b2 100644 --- a/examples/react/src/examples/A11y/index.tsx +++ b/examples/react/src/examples/A11y/index.tsx @@ -47,13 +47,13 @@ const initialEdges: Edge[] = [ const ariaLabelConfig: Partial = { 'node.a11yDescription.default': 'Custom Node Desc.', 'node.a11yDescription.keyboardDisabled': 'Custom Keyboard Desc.', - 'edge.a11yDescription.default': 'Custom Edge Desc.', - 'a11yDescription.ariaLiveMessage': ({ direction, x, y }) => + 'node.a11yDescription.ariaLiveMessage': ({ direction, x, y }) => `Custom Moved selected node ${direction}. New position, x: ${x}, y: ${y}`, + 'edge.a11yDescription.default': 'Custom Edge Desc.', 'controls.ariaLabel': 'Custom Controls Aria Label', - 'controls.zoomin.ariaLabel': 'Custom Zoom in', - 'controls.zoomout.ariaLabel': 'Custom Zoom Out', - // 'controls.fitview.ariaLabel': 'Custom Fit View', + 'controls.zoomIn.ariaLabel': 'Custom Zoom in', + 'controls.zoomOut.ariaLabel': 'Custom Zoom Out', + 'controls.fitView.ariaLabel': 'Custom Fit View', 'controls.interactive.ariaLabel': 'Custom Toggle Interactivity', 'minimap.ariaLabel': 'Custom Aria Label', }; diff --git a/examples/svelte/src/routes/examples/a11y/+page.svelte b/examples/svelte/src/routes/examples/a11y/+page.svelte index a4a5afd8..07f53110 100644 --- a/examples/svelte/src/routes/examples/a11y/+page.svelte +++ b/examples/svelte/src/routes/examples/a11y/+page.svelte @@ -28,13 +28,13 @@ ariaLabelConfig={{ 'node.a11yDescription.default': 'Svelte Custom Node Desc.', 'node.a11yDescription.keyboardDisabled': 'Svelte Custom Keyboard Desc.', - 'edge.a11yDescription.default': 'Svelte Custom Edge Desc.', - 'a11yDescription.ariaLiveMessage': ({ direction, x, y }) => + 'node.a11yDescription.ariaLiveMessage': ({ direction, x, y }) => `Custom Moved selected node ${direction}. New position, x: ${x}, y: ${y}`, + 'edge.a11yDescription.default': 'Svelte Custom Edge Desc.', 'controls.ariaLabel': 'Svelte Custom Control Aria Label', - 'controls.zoomin.ariaLabel': 'Svelte Custom Zoom in', - 'controls.zoomout.ariaLabel': 'Svelte Custom Zoom Out', - // 'controls.fitview.ariaLabel': 'Svelte Custom Fit View', + 'controls.zoomIn.ariaLabel': 'Svelte Custom Zoom in', + 'controls.zoomOut.ariaLabel': 'Svelte Custom Zoom Out', + // 'controls.fitView.ariaLabel': 'Svelte Custom Fit View', 'controls.interactive.ariaLabel': 'Svelte Custom Toggle Interactivity', 'minimap.ariaLabel': 'Svelte Custom Minimap' }} diff --git a/packages/react/src/additional-components/Controls/Controls.tsx b/packages/react/src/additional-components/Controls/Controls.tsx index ccf9c29b..128286ee 100644 --- a/packages/react/src/additional-components/Controls/Controls.tsx +++ b/packages/react/src/additional-components/Controls/Controls.tsx @@ -81,8 +81,8 @@ function ControlsComponent({ @@ -90,8 +90,8 @@ function ControlsComponent({ @@ -102,8 +102,8 @@ function ControlsComponent({ diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 513b5bda..82302cc7 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -145,7 +145,7 @@ export function NodeWrapper({ const { ariaLabelConfig } = store.getState(); store.setState({ - ariaLiveMessage: ariaLabelConfig['a11yDescription.ariaLiveMessage']({ + ariaLiveMessage: ariaLabelConfig['node.a11yDescription.ariaLiveMessage']({ direction: event.key.replace('Arrow', '').toLowerCase(), x: ~~internals.positionAbsolute.x, y: ~~internals.positionAbsolute.y, diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index 00ba3436..28909dbc 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -189,7 +189,7 @@ ) { // prevent default scrolling behavior on arrow key press when node is moved event.preventDefault(); - store.ariaLiveMessage = ariaLabelConfig['a11yDescription.ariaLiveMessage']({ + store.ariaLiveMessage = ariaLabelConfig['node.a11yDescription.ariaLiveMessage']({ direction: event.key.replace('Arrow', '').toLowerCase(), x: ~~node.internals.positionAbsolute.x, y: ~~node.internals.positionAbsolute.y diff --git a/packages/svelte/src/lib/plugins/Controls/Controls.svelte b/packages/svelte/src/lib/plugins/Controls/Controls.svelte index 4225bd4e..be6a1d59 100644 --- a/packages/svelte/src/lib/plugins/Controls/Controls.svelte +++ b/packages/svelte/src/lib/plugins/Controls/Controls.svelte @@ -84,8 +84,8 @@ @@ -94,8 +94,8 @@ @@ -106,8 +106,8 @@ diff --git a/packages/system/src/constants.ts b/packages/system/src/constants.ts index 3e020762..ba92d9e8 100644 --- a/packages/system/src/constants.ts +++ b/packages/system/src/constants.ts @@ -42,16 +42,16 @@ export const defaultAriaLabelConfig = { 'Press enter or space to select a node. Press delete to remove it and escape to cancel.', 'node.a11yDescription.keyboardDisabled': 'Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.', + 'node.a11yDescription.ariaLiveMessage': ({ direction, x, y }: { direction: string; x: number; y: number }) => + `Moved selected node ${direction}. New position, x: ${x}, y: ${y}`, 'edge.a11yDescription.default': 'Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.', - 'a11yDescription.ariaLiveMessage': ({ direction, x, y }: { direction: string; x: number; y: number }) => - `Moved selected node ${direction}. New position, x: ${x}, y: ${y}`, // Control elements 'controls.ariaLabel': 'Control Panel', - 'controls.zoomin.ariaLabel': 'Zoom In', - 'controls.zoomout.ariaLabel': 'Zoom Out', - 'controls.fitview.ariaLabel': 'Fit View', + 'controls.zoomIn.ariaLabel': 'Zoom In', + 'controls.zoomOut.ariaLabel': 'Zoom Out', + 'controls.fitView.ariaLabel': 'Fit View', 'controls.interactive.ariaLabel': 'Toggle Interactivity', // Mini map