chore: update LabelConfig name, add to store, create a11y example

This commit is contained in:
Abbey Yacoe
2025-05-20 17:20:46 +02:00
parent 07265ed277
commit ee8709b5fb
12 changed files with 142 additions and 37 deletions
+6
View File
@@ -1,3 +1,4 @@
import A11y from '../examples/A11y';
import Basic from '../examples/Basic'; import Basic from '../examples/Basic';
import Backgrounds from '../examples/Backgrounds'; import Backgrounds from '../examples/Backgrounds';
import BrokenNodes from '../examples/BrokenNodes'; import BrokenNodes from '../examples/BrokenNodes';
@@ -68,6 +69,11 @@ const routes: IRoute[] = [
path: 'add-node-edge-drop', path: 'add-node-edge-drop',
component: AddNodeOnEdgeDrop, component: AddNodeOnEdgeDrop,
}, },
{
name: 'A11y',
path: 'a11y',
component: A11y,
},
{ {
name: 'Basic', name: 'Basic',
path: 'basic', path: 'basic',
+101
View File
@@ -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 (
<ReactFlow
defaultNodes={initialNodes}
defaultEdges={initialEdges}
onNodesChange={console.log}
onNodeClick={onNodeClick}
onNodeDragStop={onNodeDragStop}
onNodeDragStart={onNodeDragStart}
onNodeDrag={onNodeDrag}
className="react-flow-basic-example"
minZoom={0.2}
maxZoom={4}
fitView
selectNodesOnDrag={false}
elevateEdgesOnSelect
elevateNodesOnSelect={false}
nodeDragThreshold={0}
labelConfig={{
'a11yDescription.node.default': 'Custom Node Desc.',
'a11yDescription.node.keyboardDisabled': 'Custom Keyboard Desc.',
'a11yDescription.edge.default': 'Custom Edge Desc.',
}}
>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
<Controls />
</ReactFlow>
);
};
export default function App() {
return (
<ReactFlowProvider>
<A11y />
</ReactFlowProvider>
);
}
@@ -235,11 +235,6 @@ const OverviewFlow = () => {
onBeforeDelete={onBeforeDelete} onBeforeDelete={onBeforeDelete}
onDelete={onDelete} onDelete={onDelete}
onPaneMouseMove={onPaneMouseMove} onPaneMouseMove={onPaneMouseMove}
descriptions={{
'a11yDescription.node.default': 'Custom Node Desc.',
'a11yDescription.node.keyboardDisabled': 'Custom Keyboard Desc.',
'a11yDescription.edge.default': 'Custom Edge Desc.',
}}
> >
<MiniMap nodeBorderRadius={2} /> <MiniMap nodeBorderRadius={2} />
<Controls orientation="horizontal" /> <Controls orientation="horizontal" />
@@ -220,7 +220,7 @@
console.log('on selection changed via prop', { nodes, edges }); console.log('on selection changed via prop', { nodes, edges });
}} }}
selectNodesOnDrag selectNodesOnDrag
descriptions={{ labelConfig={{
'a11yDescription.node.default': 'Svelte Custom Node Description.', 'a11yDescription.node.default': 'Svelte Custom Node Description.',
'a11yDescription.node.keyboardDisabled': 'Svelte Custom Keyboard Description', 'a11yDescription.node.keyboardDisabled': 'Svelte Custom Keyboard Description',
'a11yDescription.edge.default': 'Svelte Custom Edge Desc.', 'a11yDescription.edge.default': 'Svelte Custom Edge Desc.',
@@ -2,7 +2,6 @@ import { CSSProperties } from 'react';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import type { ReactFlowState } from '../../types'; import type { ReactFlowState } from '../../types';
import type { descriptions } from '@xyflow/system';
const style: CSSProperties = { display: 'none' }; const style: CSSProperties = { display: 'none' };
const ariaLiveStyle: CSSProperties = { 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_EDGE_DESC_KEY = 'react-flow__edge-desc';
export const ARIA_LIVE_MESSAGE = 'react-flow__aria-live'; export const ARIA_LIVE_MESSAGE = 'react-flow__aria-live';
const defaultDescriptions: Required<descriptions> = { const ariaLiveSelector = (s: ReactFlowState) => s.ariaLiveMessage;
'a11yDescription.node.default': const labelConfigSelector = (s: ReactFlowState) => s.labelConfig;
'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 }) { function AriaLiveMessage({ rfId }: { rfId: string }) {
const ariaLiveMessage = useStore(selector); const ariaLiveMessage = useStore(ariaLiveSelector);
return ( return (
<div id={`${ARIA_LIVE_MESSAGE}-${rfId}`} aria-live="assertive" aria-atomic="true" style={ariaLiveStyle}> <div id={`${ARIA_LIVE_MESSAGE}-${rfId}`} aria-live="assertive" aria-atomic="true" style={ariaLiveStyle}>
@@ -42,20 +33,13 @@ function AriaLiveMessage({ rfId }: { rfId: string }) {
); );
} }
export function A11yDescriptions({ export function A11yDescriptions({ rfId, disableKeyboardA11y }: { rfId: string; disableKeyboardA11y: boolean }) {
rfId, const labelConfig = useStore(labelConfigSelector);
disableKeyboardA11y,
descriptions = {},
}: {
rfId: string;
disableKeyboardA11y: boolean;
descriptions?: descriptions;
}) {
const nodeDesc = disableKeyboardA11y const nodeDesc = disableKeyboardA11y
? descriptions['a11yDescription.node.default'] || defaultDescriptions['a11yDescription.node.default'] ? labelConfig['a11yDescription.node.default']
: descriptions['a11yDescription.node.keyboardDisabled'] || : labelConfig['a11yDescription.node.keyboardDisabled'];
defaultDescriptions['a11yDescription.node.keyboardDisabled']; const edgeDesc = labelConfig['a11yDescription.edge.default'];
const edgeDesc = descriptions['a11yDescription.edge.default'] || defaultDescriptions['a11yDescription.edge.default'];
return ( return (
<> <>
@@ -145,7 +145,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
colorMode = 'light', colorMode = 'light',
debug, debug,
onScroll, onScroll,
descriptions, labelConfig,
...rest ...rest
}: ReactFlowProps<NodeType, EdgeType>, }: ReactFlowProps<NodeType, EdgeType>,
ref: ForwardedRef<HTMLDivElement> ref: ForwardedRef<HTMLDivElement>
@@ -310,7 +310,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
<SelectionListener<NodeType, EdgeType> onSelectionChange={onSelectionChange} /> <SelectionListener<NodeType, EdgeType> onSelectionChange={onSelectionChange} />
{children} {children}
<Attribution proOptions={proOptions} position={attributionPosition} /> <Attribution proOptions={proOptions} position={attributionPosition} />
<A11yDescriptions rfId={rfId} disableKeyboardA11y={disableKeyboardA11y} descriptions={descriptions} /> <A11yDescriptions rfId={rfId} disableKeyboardA11y={disableKeyboardA11y} />
</Wrapper> </Wrapper>
</div> </div>
); );
+6
View File
@@ -14,6 +14,7 @@ import {
NodeOrigin, NodeOrigin,
CoordinateExtent, CoordinateExtent,
fitViewport, fitViewport,
LabelConfig,
} from '@xyflow/system'; } from '@xyflow/system';
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
@@ -370,6 +371,11 @@ const createStore = ({
}, },
reset: () => set({ ...getInitialState() }), reset: () => set({ ...getInitialState() }),
updateLabelConfig: (newLabelConfig: Partial<LabelConfig>) => {
set((state) => ({
labelConfig: { ...state.labelConfig, ...newLabelConfig },
}));
},
}; };
}, Object.is); }, Object.is);
+2
View File
@@ -10,6 +10,7 @@ import {
NodeOrigin, NodeOrigin,
initialConnection, initialConnection,
CoordinateExtent, CoordinateExtent,
defaultLabelConfig,
} from '@xyflow/system'; } from '@xyflow/system';
import type { Edge, FitViewOptions, InternalNode, Node, ReactFlowStore } from '../types'; import type { Edge, FitViewOptions, InternalNode, Node, ReactFlowStore } from '../types';
@@ -141,6 +142,7 @@ const getInitialState = ({
lib: 'react', lib: 'react',
debug: false, debug: false,
labelConfig: defaultLabelConfig,
}; };
}; };
+2 -2
View File
@@ -21,7 +21,7 @@ import type {
ColorMode, ColorMode,
SnapGrid, SnapGrid,
OnReconnect, OnReconnect,
descriptions, LabelConfig,
} from '@xyflow/system'; } from '@xyflow/system';
import type { import type {
@@ -671,5 +671,5 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* Custom accessibility messages for screen readers and a11y features. * Custom accessibility messages for screen readers and a11y features.
* Allows localization and customization of ARIA descriptions. * Allows localization and customization of ARIA descriptions.
*/ */
descriptions?: descriptions; labelConfig?: LabelConfig;
} }
+3 -1
View File
@@ -28,6 +28,7 @@ import {
type NodeChange, type NodeChange,
type EdgeChange, type EdgeChange,
type ParentLookup, type ParentLookup,
type LabelConfig,
} from '@xyflow/system'; } from '@xyflow/system';
import type { import type {
@@ -67,7 +68,6 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
domNode: HTMLDivElement | null; domNode: HTMLDivElement | null;
paneDragging: boolean; paneDragging: boolean;
noPanClassName: string; noPanClassName: string;
panZoom: PanZoomInstance | null; panZoom: PanZoomInstance | null;
minZoom: number; minZoom: number;
maxZoom: number; maxZoom: number;
@@ -148,6 +148,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
lib: string; lib: string;
debug: boolean; debug: boolean;
labelConfig: Required<LabelConfig>;
}; };
export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = { export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
@@ -171,6 +172,7 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void; triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void;
panBy: PanBy; panBy: PanBy;
setPaneClickDistance: (distance: number) => void; setPaneClickDistance: (distance: number) => void;
updateLabelConfig: (newLabelConfig: Partial<LabelConfig>) => void;
}; };
export type ReactFlowState<NodeType extends Node = Node, EdgeType extends Edge = Edge> = ReactFlowStore< export type ReactFlowState<NodeType extends Node = Node, EdgeType extends Edge = Edge> = ReactFlowStore<
+9
View File
@@ -36,3 +36,12 @@ export const infiniteExtent: CoordinateExtent = [
]; ];
export const elementSelectionKeys = ['Enter', ' ', 'Escape']; 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.',
};
+1 -1
View File
@@ -314,7 +314,7 @@ export type OnBeforeDeleteBase<NodeType extends NodeBase = NodeBase, EdgeType ex
edges: EdgeType[]; edges: EdgeType[];
}) => Promise<boolean | { nodes: NodeType[]; edges: EdgeType[] }>; }) => Promise<boolean | { nodes: NodeType[]; edges: EdgeType[] }>;
export type descriptions = { export type LabelConfig = {
'a11yDescription.node.default'?: string; 'a11yDescription.node.default'?: string;
'a11yDescription.node.keyboardDisabled'?: string; 'a11yDescription.node.keyboardDisabled'?: string;
'a11yDescription.edge.default'?: string; 'a11yDescription.edge.default'?: string;