Merge pull request #5277 from xyflow/a11y
feat(a11y): add static support for a11y message localization
This commit is contained in:
@@ -2,4 +2,4 @@
|
|||||||
'@xyflow/svelte': patch
|
'@xyflow/svelte': patch
|
||||||
---
|
---
|
||||||
|
|
||||||
Fix data in EdgeProps not typed correctly
|
Fix data in `EdgeProps` that was not typed correctly
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
'@xyflow/react': minor
|
'@xyflow/react': minor
|
||||||
'@xyflow/svelte': minor
|
'@xyflow/svelte': minor
|
||||||
'@xyflow/system': minor
|
'@xyflow/system': patch
|
||||||
---
|
---
|
||||||
|
|
||||||
Improve typing for Nodes
|
Improve typing for Nodes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
'@xyflow/react': minor
|
'@xyflow/react': minor
|
||||||
'@xyflow/svelte': minor
|
'@xyflow/svelte': minor
|
||||||
'@xyflow/system': minor
|
'@xyflow/system': patch
|
||||||
---
|
---
|
||||||
|
|
||||||
Add an `ease` and `interpolate` option to all function that alter the viewport
|
Add an `ease` and `interpolate` option to all function that alter the viewport
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
"@xyflow/react": minor
|
||||||
|
"@xyflow/svelte": minor
|
||||||
|
"@xyflow/system": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add `ariaLabelConfig` prop for customizing UI text like aria labels and descriptions.
|
||||||
|
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { MouseEvent } from 'react';
|
||||||
|
import {
|
||||||
|
ReactFlow,
|
||||||
|
MiniMap,
|
||||||
|
Background,
|
||||||
|
BackgroundVariant,
|
||||||
|
Controls,
|
||||||
|
ReactFlowProvider,
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
OnNodeDrag,
|
||||||
|
AriaLabelConfig,
|
||||||
|
} 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 ariaLabelConfig: Partial<AriaLabelConfig> = {
|
||||||
|
'node.a11yDescription.default': 'Custom Node Desc.',
|
||||||
|
'node.a11yDescription.keyboardDisabled': 'Custom Keyboard Desc.',
|
||||||
|
'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.interactive.ariaLabel': 'Custom Toggle Interactivity',
|
||||||
|
'minimap.ariaLabel': 'Custom Aria Label',
|
||||||
|
};
|
||||||
|
|
||||||
|
const A11y = () => {
|
||||||
|
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}
|
||||||
|
ariaLabelConfig={ariaLabelConfig}
|
||||||
|
>
|
||||||
|
<Background variant={BackgroundVariant.Dots} />
|
||||||
|
<MiniMap />
|
||||||
|
<Controls />
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<A11y />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
|
'a11y',
|
||||||
'add-node-on-drop',
|
'add-node-on-drop',
|
||||||
'color-mode',
|
'color-mode',
|
||||||
'custom-connection-line',
|
'custom-connection-line',
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { SvelteFlow, Controls, Background, MiniMap } from '@xyflow/svelte';
|
||||||
|
|
||||||
|
import '@xyflow/svelte/dist/style.css';
|
||||||
|
|
||||||
|
let nodes = $state.raw([
|
||||||
|
{
|
||||||
|
id: 'A',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
data: { label: 'A' }
|
||||||
|
},
|
||||||
|
{ 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' } }
|
||||||
|
]);
|
||||||
|
|
||||||
|
let edges = $state.raw([
|
||||||
|
{ id: 'A-B', source: 'A', target: 'B' },
|
||||||
|
{ id: 'A-C', source: 'A', target: 'C' },
|
||||||
|
{ id: 'A-D', source: 'A', target: 'D' }
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SvelteFlow
|
||||||
|
bind:nodes
|
||||||
|
bind:edges
|
||||||
|
fitView
|
||||||
|
ariaLabelConfig={{
|
||||||
|
'node.a11yDescription.default': 'Svelte Custom Node Desc.',
|
||||||
|
'node.a11yDescription.keyboardDisabled': 'Svelte Custom Keyboard Desc.',
|
||||||
|
'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.interactive.ariaLabel': 'Svelte Custom Toggle Interactivity',
|
||||||
|
'minimap.ariaLabel': 'Svelte Custom Minimap'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Controls />
|
||||||
|
<Background />
|
||||||
|
<MiniMap />
|
||||||
|
</SvelteFlow>
|
||||||
@@ -19,6 +19,7 @@ const selector = (s: ReactFlowState) => ({
|
|||||||
isInteractive: s.nodesDraggable || s.nodesConnectable || s.elementsSelectable,
|
isInteractive: s.nodesDraggable || s.nodesConnectable || s.elementsSelectable,
|
||||||
minZoomReached: s.transform[2] <= s.minZoom,
|
minZoomReached: s.transform[2] <= s.minZoom,
|
||||||
maxZoomReached: s.transform[2] >= s.maxZoom,
|
maxZoomReached: s.transform[2] >= s.maxZoom,
|
||||||
|
ariaLabelConfig: s.ariaLabelConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
function ControlsComponent({
|
function ControlsComponent({
|
||||||
@@ -35,10 +36,10 @@ function ControlsComponent({
|
|||||||
children,
|
children,
|
||||||
position = 'bottom-left',
|
position = 'bottom-left',
|
||||||
orientation = 'vertical',
|
orientation = 'vertical',
|
||||||
'aria-label': ariaLabel = 'React Flow controls',
|
'aria-label': ariaLabel,
|
||||||
}: ControlProps) {
|
}: ControlProps) {
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const { isInteractive, minZoomReached, maxZoomReached } = useStore(selector, shallow);
|
const { isInteractive, minZoomReached, maxZoomReached, ariaLabelConfig } = useStore(selector, shallow);
|
||||||
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
||||||
|
|
||||||
const onZoomInHandler = () => {
|
const onZoomInHandler = () => {
|
||||||
@@ -67,22 +68,21 @@ function ControlsComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const orientationClass = orientation === 'horizontal' ? 'horizontal' : 'vertical';
|
const orientationClass = orientation === 'horizontal' ? 'horizontal' : 'vertical';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
className={cc(['react-flow__controls', orientationClass, className])}
|
className={cc(['react-flow__controls', orientationClass, className])}
|
||||||
position={position}
|
position={position}
|
||||||
style={style}
|
style={style}
|
||||||
data-testid="rf__controls"
|
data-testid="rf__controls"
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel ?? ariaLabelConfig['controls.ariaLabel']}
|
||||||
>
|
>
|
||||||
{showZoom && (
|
{showZoom && (
|
||||||
<>
|
<>
|
||||||
<ControlButton
|
<ControlButton
|
||||||
onClick={onZoomInHandler}
|
onClick={onZoomInHandler}
|
||||||
className="react-flow__controls-zoomin"
|
className="react-flow__controls-zoomin"
|
||||||
title="zoom in"
|
title={ariaLabelConfig['controls.zoomIn.ariaLabel']}
|
||||||
aria-label="zoom in"
|
aria-label={ariaLabelConfig['controls.zoomIn.ariaLabel']}
|
||||||
disabled={maxZoomReached}
|
disabled={maxZoomReached}
|
||||||
>
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
@@ -90,8 +90,8 @@ function ControlsComponent({
|
|||||||
<ControlButton
|
<ControlButton
|
||||||
onClick={onZoomOutHandler}
|
onClick={onZoomOutHandler}
|
||||||
className="react-flow__controls-zoomout"
|
className="react-flow__controls-zoomout"
|
||||||
title="zoom out"
|
title={ariaLabelConfig['controls.zoomOut.ariaLabel']}
|
||||||
aria-label="zoom out"
|
aria-label={ariaLabelConfig['controls.zoomOut.ariaLabel']}
|
||||||
disabled={minZoomReached}
|
disabled={minZoomReached}
|
||||||
>
|
>
|
||||||
<MinusIcon />
|
<MinusIcon />
|
||||||
@@ -102,8 +102,8 @@ function ControlsComponent({
|
|||||||
<ControlButton
|
<ControlButton
|
||||||
className="react-flow__controls-fitview"
|
className="react-flow__controls-fitview"
|
||||||
onClick={onFitViewHandler}
|
onClick={onFitViewHandler}
|
||||||
title="fit view"
|
title={ariaLabelConfig['controls.fitView.ariaLabel']}
|
||||||
aria-label="fit view"
|
aria-label={ariaLabelConfig['controls.fitView.ariaLabel']}
|
||||||
>
|
>
|
||||||
<FitViewIcon />
|
<FitViewIcon />
|
||||||
</ControlButton>
|
</ControlButton>
|
||||||
@@ -112,8 +112,8 @@ function ControlsComponent({
|
|||||||
<ControlButton
|
<ControlButton
|
||||||
className="react-flow__controls-interactive"
|
className="react-flow__controls-interactive"
|
||||||
onClick={onToggleInteractivity}
|
onClick={onToggleInteractivity}
|
||||||
title="toggle interactivity"
|
title={ariaLabelConfig['controls.interactive.ariaLabel']}
|
||||||
aria-label="toggle interactivity"
|
aria-label={ariaLabelConfig['controls.interactive.ariaLabel']}
|
||||||
>
|
>
|
||||||
{isInteractive ? <UnlockIcon /> : <LockIcon />}
|
{isInteractive ? <UnlockIcon /> : <LockIcon />}
|
||||||
</ControlButton>
|
</ControlButton>
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ const selector = (s: ReactFlowState) => {
|
|||||||
translateExtent: s.translateExtent,
|
translateExtent: s.translateExtent,
|
||||||
flowWidth: s.width,
|
flowWidth: s.width,
|
||||||
flowHeight: s.height,
|
flowHeight: s.height,
|
||||||
|
ariaLabelConfig: s.ariaLabelConfig,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const ARIA_LABEL_KEY = 'react-flow__minimap-desc';
|
const ARIA_LABEL_KEY = 'react-flow__minimap-desc';
|
||||||
|
|
||||||
function MiniMapComponent<NodeType extends Node = Node>({
|
function MiniMapComponent<NodeType extends Node = Node>({
|
||||||
style,
|
style,
|
||||||
className,
|
className,
|
||||||
@@ -63,14 +63,17 @@ function MiniMapComponent<NodeType extends Node = Node>({
|
|||||||
onNodeClick,
|
onNodeClick,
|
||||||
pannable = false,
|
pannable = false,
|
||||||
zoomable = false,
|
zoomable = false,
|
||||||
ariaLabel = 'React Flow mini map',
|
ariaLabel,
|
||||||
inversePan,
|
inversePan,
|
||||||
zoomStep = 10,
|
zoomStep = 10,
|
||||||
offsetScale = 5,
|
offsetScale = 5,
|
||||||
}: MiniMapProps<NodeType>) {
|
}: MiniMapProps<NodeType>) {
|
||||||
const store = useStoreApi<NodeType>();
|
const store = useStoreApi<NodeType>();
|
||||||
const svg = useRef<SVGSVGElement>(null);
|
const svg = useRef<SVGSVGElement>(null);
|
||||||
const { boundingRect, viewBB, rfId, panZoom, translateExtent, flowWidth, flowHeight } = useStore(selector, shallow);
|
const { boundingRect, viewBB, rfId, panZoom, translateExtent, flowWidth, flowHeight, ariaLabelConfig } = useStore(
|
||||||
|
selector,
|
||||||
|
shallow
|
||||||
|
);
|
||||||
const elementWidth = (style?.width as number) ?? defaultWidth;
|
const elementWidth = (style?.width as number) ?? defaultWidth;
|
||||||
const elementHeight = (style?.height as number) ?? defaultHeight;
|
const elementHeight = (style?.height as number) ?? defaultHeight;
|
||||||
const scaledWidth = boundingRect.width / elementWidth;
|
const scaledWidth = boundingRect.width / elementWidth;
|
||||||
@@ -130,6 +133,8 @@ function MiniMapComponent<NodeType extends Node = Node>({
|
|||||||
}, [])
|
}, [])
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const _ariaLabel = ariaLabel ?? ariaLabelConfig['minimap.ariaLabel'];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
position={position}
|
position={position}
|
||||||
@@ -159,7 +164,8 @@ function MiniMapComponent<NodeType extends Node = Node>({
|
|||||||
ref={svg}
|
ref={svg}
|
||||||
onClick={onSvgClick}
|
onClick={onSvgClick}
|
||||||
>
|
>
|
||||||
{ariaLabel && <title id={labelledBy}>{ariaLabel}</title>}
|
{_ariaLabel && <title id={labelledBy}>{_ariaLabel}</title>}
|
||||||
|
|
||||||
<MiniMapNodes<NodeType>
|
<MiniMapNodes<NodeType>
|
||||||
onClick={onSvgNodeClick}
|
onClick={onSvgNodeClick}
|
||||||
nodeColor={nodeColor}
|
nodeColor={nodeColor}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export type MiniMapProps<NodeType extends Node = Node> = Omit<HTMLAttributes<SVG
|
|||||||
* There is no text inside the minimap for a screen reader to use as an accessible name, so it's
|
* There is no text inside the minimap for a screen reader to use as an accessible name, so it's
|
||||||
* important we provide one to make the minimap accessible. The default is sufficient, but you may
|
* important we provide one to make the minimap accessible. The default is sufficient, but you may
|
||||||
* want to replace it with something more relevant to your app or product.
|
* want to replace it with something more relevant to your app or product.
|
||||||
* @default "React Flow mini map"
|
* @default "Mini Map"
|
||||||
*/
|
*/
|
||||||
ariaLabel?: string | null;
|
ariaLabel?: string | null;
|
||||||
/** Invert direction when panning the minimap viewport. */
|
/** Invert direction when panning the minimap viewport. */
|
||||||
|
|||||||
@@ -20,10 +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 selector = (s: ReactFlowState) => s.ariaLiveMessage;
|
const ariaLiveSelector = (s: ReactFlowState) => s.ariaLiveMessage;
|
||||||
|
const ariaLabelConfigSelector = (s: ReactFlowState) => s.ariaLabelConfig;
|
||||||
|
|
||||||
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}>
|
||||||
@@ -33,15 +34,17 @@ function AriaLiveMessage({ rfId }: { rfId: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function A11yDescriptions({ rfId, disableKeyboardA11y }: { rfId: string; disableKeyboardA11y: boolean }) {
|
export function A11yDescriptions({ rfId, disableKeyboardA11y }: { rfId: string; disableKeyboardA11y: boolean }) {
|
||||||
|
const ariaLabelConfig = useStore(ariaLabelConfigSelector);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div id={`${ARIA_NODE_DESC_KEY}-${rfId}`} style={style}>
|
<div id={`${ARIA_NODE_DESC_KEY}-${rfId}`} style={style}>
|
||||||
Press enter or space to select a node.
|
{disableKeyboardA11y
|
||||||
{!disableKeyboardA11y && 'You can then use the arrow keys to move the node around.'} Press delete to remove it
|
? ariaLabelConfig['node.a11yDescription.default']
|
||||||
and escape to cancel.{' '}
|
: ariaLabelConfig['node.a11yDescription.keyboardDisabled']}
|
||||||
</div>
|
</div>
|
||||||
<div id={`${ARIA_EDGE_DESC_KEY}-${rfId}`} style={style}>
|
<div id={`${ARIA_EDGE_DESC_KEY}-${rfId}`} style={style}>
|
||||||
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.
|
{ariaLabelConfig['edge.a11yDescription.default']}
|
||||||
</div>
|
</div>
|
||||||
{!disableKeyboardA11y && <AriaLiveMessage rfId={rfId} />}
|
{!disableKeyboardA11y && <AriaLiveMessage rfId={rfId} />}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -142,10 +142,14 @@ export function NodeWrapper<NodeType extends Node>({
|
|||||||
// prevent default scrolling behavior on arrow key press when node is moved
|
// prevent default scrolling behavior on arrow key press when node is moved
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
|
const { ariaLabelConfig } = store.getState();
|
||||||
|
|
||||||
store.setState({
|
store.setState({
|
||||||
ariaLiveMessage: `Moved selected node ${event.key
|
ariaLiveMessage: ariaLabelConfig['node.a11yDescription.ariaLiveMessage']({
|
||||||
.replace('Arrow', '')
|
direction: event.key.replace('Arrow', '').toLowerCase(),
|
||||||
.toLowerCase()}. New position, x: ${~~internals.positionAbsolute.x}, y: ${~~internals.positionAbsolute.y}`,
|
x: ~~internals.positionAbsolute.x,
|
||||||
|
y: ~~internals.positionAbsolute.y,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
moveSelectedNodes({
|
moveSelectedNodes({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
import { infiniteExtent, type CoordinateExtent } from '@xyflow/system';
|
import { infiniteExtent, type CoordinateExtent, mergeAriaLabelConfig, AriaLabelConfig } from '@xyflow/system';
|
||||||
|
|
||||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||||
import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } from '../../types';
|
import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } from '../../types';
|
||||||
@@ -68,6 +68,7 @@ const reactFlowFieldsToTrack = [
|
|||||||
'debug',
|
'debug',
|
||||||
'autoPanSpeed',
|
'autoPanSpeed',
|
||||||
'paneClickDistance',
|
'paneClickDistance',
|
||||||
|
'ariaLabelConfig',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
type ReactFlowFieldsToTrack = (typeof reactFlowFieldsToTrack)[number];
|
type ReactFlowFieldsToTrack = (typeof reactFlowFieldsToTrack)[number];
|
||||||
@@ -156,6 +157,10 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
|
|||||||
// Renamed fields
|
// Renamed fields
|
||||||
else if (fieldName === 'fitView') store.setState({ fitViewQueued: fieldValue as boolean });
|
else if (fieldName === 'fitView') store.setState({ fitViewQueued: fieldValue as boolean });
|
||||||
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOptions: fieldValue as FitViewOptions });
|
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOptions: fieldValue as FitViewOptions });
|
||||||
|
|
||||||
|
if (fieldName === 'ariaLabelConfig') {
|
||||||
|
store.setState({ ariaLabelConfig: mergeAriaLabelConfig(fieldValue as AriaLabelConfig) });
|
||||||
|
}
|
||||||
// General case
|
// General case
|
||||||
else store.setState({ [fieldName]: fieldValue });
|
else store.setState({ [fieldName]: fieldValue });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
|||||||
colorMode = 'light',
|
colorMode = 'light',
|
||||||
debug,
|
debug,
|
||||||
onScroll,
|
onScroll,
|
||||||
|
ariaLabelConfig,
|
||||||
...rest
|
...rest
|
||||||
}: ReactFlowProps<NodeType, EdgeType>,
|
}: ReactFlowProps<NodeType, EdgeType>,
|
||||||
ref: ForwardedRef<HTMLDivElement>
|
ref: ForwardedRef<HTMLDivElement>
|
||||||
@@ -306,6 +307,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
|||||||
onBeforeDelete={onBeforeDelete}
|
onBeforeDelete={onBeforeDelete}
|
||||||
paneClickDistance={paneClickDistance}
|
paneClickDistance={paneClickDistance}
|
||||||
debug={debug}
|
debug={debug}
|
||||||
|
ariaLabelConfig={ariaLabelConfig}
|
||||||
/>
|
/>
|
||||||
<SelectionListener<NodeType, EdgeType> onSelectionChange={onSelectionChange} />
|
<SelectionListener<NodeType, EdgeType> onSelectionChange={onSelectionChange} />
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ export {
|
|||||||
type NoConnection,
|
type NoConnection,
|
||||||
type NodeConnection,
|
type NodeConnection,
|
||||||
type OnReconnect,
|
type OnReconnect,
|
||||||
|
type AriaLabelConfig,
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
// we need this workaround to prevent a duplicate identifier error
|
// we need this workaround to prevent a duplicate identifier error
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
NodeOrigin,
|
NodeOrigin,
|
||||||
initialConnection,
|
initialConnection,
|
||||||
CoordinateExtent,
|
CoordinateExtent,
|
||||||
|
defaultAriaLabelConfig,
|
||||||
} 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,
|
||||||
|
ariaLabelConfig: defaultAriaLabelConfig,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import type {
|
|||||||
ColorMode,
|
ColorMode,
|
||||||
SnapGrid,
|
SnapGrid,
|
||||||
OnReconnect,
|
OnReconnect,
|
||||||
|
AriaLabelConfig,
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -666,4 +667,9 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
|
|||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
ariaLabelConfig?: Partial<AriaLabelConfig>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
type NodeChange,
|
type NodeChange,
|
||||||
type EdgeChange,
|
type EdgeChange,
|
||||||
type ParentLookup,
|
type ParentLookup,
|
||||||
|
type AriaLabelConfig,
|
||||||
} 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;
|
||||||
|
ariaLabelConfig: AriaLabelConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
|
export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
|
||||||
|
|||||||
@@ -7,15 +7,12 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div id={`${ARIA_NODE_DESC_KEY}-${store.flowId}`} class="a11y-hidden">
|
<div id={`${ARIA_NODE_DESC_KEY}-${store.flowId}`} class="a11y-hidden">
|
||||||
Press enter or space to select a node.
|
{store.disableKeyboardA11y
|
||||||
{#if !store.disableKeyboardA11y}
|
? store.ariaLabelConfig['node.a11yDescription.default']
|
||||||
You can then use the arrow keys to move the node around.
|
: store.ariaLabelConfig['node.a11yDescription.keyboardDisabled']}
|
||||||
{/if}
|
|
||||||
Press delete to remove it and escape to cancel.
|
|
||||||
</div>
|
</div>
|
||||||
<div id={`${ARIA_EDGE_DESC_KEY}-${store.flowId}`} class="a11y-hidden">
|
<div id={`${ARIA_EDGE_DESC_KEY}-${store.flowId}`} class="a11y-hidden">
|
||||||
Press enter or space to select an edge. You can then press delete to remove it or escape to
|
{store.ariaLabelConfig['edge.a11yDescription.default']}
|
||||||
cancel.
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !store.disableKeyboardA11y}
|
{#if !store.disableKeyboardA11y}
|
||||||
|
|||||||
@@ -44,6 +44,8 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
let store = useStore();
|
let store = useStore();
|
||||||
|
let ariaLabelConfig = $derived(store.ariaLabelConfig);
|
||||||
|
|
||||||
|
|
||||||
let prevConnections: Map<string, HandleConnection> | null = null;
|
let prevConnections: Map<string, HandleConnection> | null = null;
|
||||||
$effect.pre(() => {
|
$effect.pre(() => {
|
||||||
@@ -227,6 +229,7 @@ The Handle component is the part of a node that can be used to connect nodes.
|
|||||||
onkeypress={() => {}}
|
onkeypress={() => {}}
|
||||||
{style}
|
{style}
|
||||||
role="button"
|
role="button"
|
||||||
|
aria-label={ariaLabelConfig[`handle.ariaLabel`]}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -85,6 +85,7 @@
|
|||||||
let prevTargetPosition: Position | undefined = targetPosition;
|
let prevTargetPosition: Position | undefined = targetPosition;
|
||||||
|
|
||||||
let NodeComponent = $derived(store.nodeTypes[type] ?? DefaultNode);
|
let NodeComponent = $derived(store.nodeTypes[type] ?? DefaultNode);
|
||||||
|
let ariaLabelConfig = $derived(store.ariaLabelConfig);
|
||||||
|
|
||||||
let connectableContext: ConnectableContext = {
|
let connectableContext: ConnectableContext = {
|
||||||
get value() {
|
get value() {
|
||||||
@@ -188,11 +189,11 @@
|
|||||||
) {
|
) {
|
||||||
// prevent default scrolling behavior on arrow key press when node is moved
|
// prevent default scrolling behavior on arrow key press when node is moved
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
store.ariaLiveMessage = ariaLabelConfig['node.a11yDescription.ariaLiveMessage']({
|
||||||
store.ariaLiveMessage = `Moved selected node ${event.key
|
direction: event.key.replace('Arrow', '').toLowerCase(),
|
||||||
.replace('Arrow', '')
|
x: ~~node.internals.positionAbsolute.x,
|
||||||
.toLowerCase()}. New position, x: ${node.internals.positionAbsolute.x}, y: ${node.internals.positionAbsolute.y}`;
|
y: ~~node.internals.positionAbsolute.y
|
||||||
|
});
|
||||||
store.moveSelectedNodes(arrowKeyDiffs[event.key], event.shiftKey ? 4 : 1);
|
store.moveSelectedNodes(arrowKeyDiffs[event.key], event.shiftKey ? 4 : 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
noDragClass,
|
noDragClass,
|
||||||
noPanClass,
|
noPanClass,
|
||||||
noWheelClass,
|
noWheelClass,
|
||||||
|
ariaLabelConfig,
|
||||||
...divAttributes
|
...divAttributes
|
||||||
} = $derived(rest);
|
} = $derived(rest);
|
||||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ import type {
|
|||||||
OnConnectEnd,
|
OnConnectEnd,
|
||||||
OnReconnect,
|
OnReconnect,
|
||||||
OnReconnectStart,
|
OnReconnectStart,
|
||||||
OnReconnectEnd
|
OnReconnectEnd,
|
||||||
|
AriaLabelConfig
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -471,4 +472,9 @@ export type SvelteFlowProps<
|
|||||||
onselectionstart?: (event: PointerEvent) => void;
|
onselectionstart?: (event: PointerEvent) => void;
|
||||||
/** This event handler gets called when the user finishes dragging a selection box */
|
/** This event handler gets called when the user finishes dragging a selection box */
|
||||||
onselectionend?: (event: PointerEvent) => void;
|
onselectionend?: (event: PointerEvent) => void;
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
ariaLabelConfig?: Partial<AriaLabelConfig>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -112,7 +112,8 @@ export {
|
|||||||
type ResizeParamsWithDirection,
|
type ResizeParamsWithDirection,
|
||||||
type ResizeDragEvent,
|
type ResizeDragEvent,
|
||||||
type IsValidConnection,
|
type IsValidConnection,
|
||||||
type NodeConnection
|
type NodeConnection,
|
||||||
|
type AriaLabelConfig
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
// system utils
|
// system utils
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
);
|
);
|
||||||
let minZoomReached = $derived(store.viewport.zoom <= store.minZoom);
|
let minZoomReached = $derived(store.viewport.zoom <= store.minZoom);
|
||||||
let maxZoomReached = $derived(store.viewport.zoom >= store.maxZoom);
|
let maxZoomReached = $derived(store.viewport.zoom >= store.maxZoom);
|
||||||
|
let ariaLabelConfig = $derived(store.ariaLabelConfig);
|
||||||
let orientationClass = $derived(orientation === 'horizontal' ? 'horizontal' : 'vertical');
|
let orientationClass = $derived(orientation === 'horizontal' ? 'horizontal' : 'vertical');
|
||||||
|
|
||||||
const onZoomInHandler = () => {
|
const onZoomInHandler = () => {
|
||||||
@@ -72,7 +73,7 @@
|
|||||||
class={['svelte-flow__controls', orientationClass, className]}
|
class={['svelte-flow__controls', orientationClass, className]}
|
||||||
{position}
|
{position}
|
||||||
data-testid="svelte-flow__controls"
|
data-testid="svelte-flow__controls"
|
||||||
aria-label={ariaLabel ?? 'Svelte Flow controls'}
|
aria-label={ariaLabelConfig['controls.ariaLabel']}
|
||||||
{style}
|
{style}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
@@ -83,8 +84,8 @@
|
|||||||
<ControlButton
|
<ControlButton
|
||||||
onclick={onZoomInHandler}
|
onclick={onZoomInHandler}
|
||||||
class="svelte-flow__controls-zoomin"
|
class="svelte-flow__controls-zoomin"
|
||||||
title="zoom in"
|
title={ariaLabelConfig['controls.zoomIn.ariaLabel']}
|
||||||
aria-label="zoom in"
|
aria-label={ariaLabelConfig['controls.zoomIn.ariaLabel']}
|
||||||
disabled={maxZoomReached}
|
disabled={maxZoomReached}
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
>
|
>
|
||||||
@@ -93,8 +94,8 @@
|
|||||||
<ControlButton
|
<ControlButton
|
||||||
onclick={onZoomOutHandler}
|
onclick={onZoomOutHandler}
|
||||||
class="svelte-flow__controls-zoomout"
|
class="svelte-flow__controls-zoomout"
|
||||||
title="zoom out"
|
title={ariaLabelConfig['controls.zoomOut.ariaLabel']}
|
||||||
aria-label="zoom out"
|
aria-label={ariaLabelConfig['controls.zoomOut.ariaLabel']}
|
||||||
disabled={minZoomReached}
|
disabled={minZoomReached}
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
>
|
>
|
||||||
@@ -105,8 +106,8 @@
|
|||||||
<ControlButton
|
<ControlButton
|
||||||
class="svelte-flow__controls-fitview"
|
class="svelte-flow__controls-fitview"
|
||||||
onclick={onFitViewHandler}
|
onclick={onFitViewHandler}
|
||||||
title="fit view"
|
title={ariaLabelConfig['controls.fitView.ariaLabel']}
|
||||||
aria-label="fit view"
|
aria-label={ariaLabelConfig['controls.fitView.ariaLabel']}
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
>
|
>
|
||||||
<FitViewIcon />
|
<FitViewIcon />
|
||||||
@@ -116,8 +117,8 @@
|
|||||||
<ControlButton
|
<ControlButton
|
||||||
class="svelte-flow__controls-interactive"
|
class="svelte-flow__controls-interactive"
|
||||||
onclick={onToggleInteractivity}
|
onclick={onToggleInteractivity}
|
||||||
title="toggle interactivity"
|
title={ariaLabelConfig['controls.interactive.ariaLabel']}
|
||||||
aria-label="toggle interactivity"
|
aria-label={ariaLabelConfig['controls.interactive.ariaLabel']}
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
>
|
>
|
||||||
{#if isInteractive}<UnlockIcon />{:else}<LockIcon />{/if}
|
{#if isInteractive}<UnlockIcon />{:else}<LockIcon />{/if}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
position = 'bottom-right',
|
position = 'bottom-right',
|
||||||
ariaLabel = 'Mini map',
|
ariaLabel,
|
||||||
nodeStrokeColor = 'transparent',
|
nodeStrokeColor = 'transparent',
|
||||||
nodeColor,
|
nodeColor,
|
||||||
nodeClass = '',
|
nodeClass = '',
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
}: MiniMapProps = $props();
|
}: MiniMapProps = $props();
|
||||||
|
|
||||||
let store = $derived(useStore());
|
let store = $derived(useStore());
|
||||||
|
let ariaLabelConfig = $derived(store.ariaLabelConfig);
|
||||||
|
|
||||||
const nodeColorFunc = nodeColor === undefined ? undefined : getAttrFunction(nodeColor);
|
const nodeColorFunc = nodeColor === undefined ? undefined : getAttrFunction(nodeColor);
|
||||||
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
|
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
|
||||||
@@ -113,7 +114,9 @@
|
|||||||
zoomable
|
zoomable
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if ariaLabel}<title id={labelledBy}>{ariaLabel}</title>{/if}
|
{#if ariaLabel ?? ariaLabelConfig['minimap.ariaLabel']}
|
||||||
|
<title id={labelledBy}>{ariaLabel ?? ariaLabelConfig['minimap.ariaLabel']}</title>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#each store.nodes as userNode (userNode.id)}
|
{#each store.nodes as userNode (userNode.id)}
|
||||||
{@const node = store.nodeLookup.get(userNode.id)}
|
{@const node = store.nodeLookup.get(userNode.id)}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
getViewportForBounds,
|
getViewportForBounds,
|
||||||
updateConnectionLookup,
|
updateConnectionLookup,
|
||||||
initialConnection,
|
initialConnection,
|
||||||
|
mergeAriaLabelConfig,
|
||||||
type SelectionRect,
|
type SelectionRect,
|
||||||
type SnapGrid,
|
type SnapGrid,
|
||||||
type MarkerProps,
|
type MarkerProps,
|
||||||
@@ -32,7 +33,8 @@ import {
|
|||||||
type Handle,
|
type Handle,
|
||||||
type OnReconnect,
|
type OnReconnect,
|
||||||
type OnReconnectStart,
|
type OnReconnectStart,
|
||||||
type OnReconnectEnd
|
type OnReconnectEnd,
|
||||||
|
type AriaLabelConfig
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
|
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
|
||||||
@@ -289,6 +291,9 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
|
|||||||
noPanClass: string = $derived(signals.props.noPanClass ?? 'nopan');
|
noPanClass: string = $derived(signals.props.noPanClass ?? 'nopan');
|
||||||
noDragClass: string = $derived(signals.props.noDragClass ?? 'nodrag');
|
noDragClass: string = $derived(signals.props.noDragClass ?? 'nodrag');
|
||||||
noWheelClass: string = $derived(signals.props.noWheelClass ?? 'nowheel');
|
noWheelClass: string = $derived(signals.props.noWheelClass ?? 'nowheel');
|
||||||
|
ariaLabelConfig: AriaLabelConfig = $derived(
|
||||||
|
mergeAriaLabelConfig(signals.props.ariaLabelConfig)
|
||||||
|
);
|
||||||
|
|
||||||
// _viewport is the internal viewport.
|
// _viewport is the internal viewport.
|
||||||
// when binding to viewport, we operate on signals.viewport instead
|
// when binding to viewport, we operate on signals.viewport instead
|
||||||
|
|||||||
@@ -36,3 +36,29 @@ export const infiniteExtent: CoordinateExtent = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const elementSelectionKeys = ['Enter', ' ', 'Escape'];
|
export const elementSelectionKeys = ['Enter', ' ', 'Escape'];
|
||||||
|
|
||||||
|
export const defaultAriaLabelConfig = {
|
||||||
|
'node.a11yDescription.default':
|
||||||
|
'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.',
|
||||||
|
|
||||||
|
// Control elements
|
||||||
|
'controls.ariaLabel': 'Control Panel',
|
||||||
|
'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',
|
||||||
|
|
||||||
|
// Handle
|
||||||
|
'handle.ariaLabel': 'Handle',
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AriaLabelConfig = typeof defaultAriaLabelConfig;
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import type {
|
|||||||
import { type Viewport } from '../types';
|
import { type Viewport } from '../types';
|
||||||
import { getNodePositionWithOrigin, isInternalNodeBase } from './graph';
|
import { getNodePositionWithOrigin, isInternalNodeBase } from './graph';
|
||||||
|
|
||||||
|
import { defaultAriaLabelConfig, type AriaLabelConfig } from '../constants';
|
||||||
|
|
||||||
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max);
|
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max);
|
||||||
|
|
||||||
export const clampPosition = (
|
export const clampPosition = (
|
||||||
@@ -418,3 +420,7 @@ export function withResolvers<T>(): {
|
|||||||
});
|
});
|
||||||
return { promise, resolve, reject };
|
return { promise, resolve, reject };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mergeAriaLabelConfig(partial?: Partial<AriaLabelConfig>): AriaLabelConfig {
|
||||||
|
return { ...defaultAriaLabelConfig, ...(partial || {}) };
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user