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;