Merge branch 'main' of https://github.com/xyflow/xyflow into 5030-focus-nodes-when-user-selects-via-tab
This commit is contained in:
@@ -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_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 }) {
|
||||
const ariaLiveMessage = useStore(selector);
|
||||
const ariaLiveMessage = useStore(ariaLiveSelector);
|
||||
|
||||
return (
|
||||
<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 }) {
|
||||
const ariaLabelConfig = useStore(ariaLabelConfigSelector);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id={`${ARIA_NODE_DESC_KEY}-${rfId}`} style={style}>
|
||||
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.{' '}
|
||||
{disableKeyboardA11y
|
||||
? ariaLabelConfig['node.a11yDescription.default']
|
||||
: ariaLabelConfig['node.a11yDescription.keyboardDisabled']}
|
||||
</div>
|
||||
<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>
|
||||
{!disableKeyboardA11y && <AriaLiveMessage rfId={rfId} />}
|
||||
</>
|
||||
|
||||
@@ -136,28 +136,28 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
|
||||
|
||||
const onEdgeDoubleClick = onDoubleClick
|
||||
? (event: React.MouseEvent) => {
|
||||
onDoubleClick(event, { ...edge });
|
||||
}
|
||||
onDoubleClick(event, { ...edge });
|
||||
}
|
||||
: undefined;
|
||||
const onEdgeContextMenu = onContextMenu
|
||||
? (event: React.MouseEvent) => {
|
||||
onContextMenu(event, { ...edge });
|
||||
}
|
||||
onContextMenu(event, { ...edge });
|
||||
}
|
||||
: undefined;
|
||||
const onEdgeMouseEnter = onMouseEnter
|
||||
? (event: React.MouseEvent) => {
|
||||
onMouseEnter(event, { ...edge });
|
||||
}
|
||||
onMouseEnter(event, { ...edge });
|
||||
}
|
||||
: undefined;
|
||||
const onEdgeMouseMove = onMouseMove
|
||||
? (event: React.MouseEvent) => {
|
||||
onMouseMove(event, { ...edge });
|
||||
}
|
||||
onMouseMove(event, { ...edge });
|
||||
}
|
||||
: undefined;
|
||||
const onEdgeMouseLeave = onMouseLeave
|
||||
? (event: React.MouseEvent) => {
|
||||
onMouseLeave(event, { ...edge });
|
||||
}
|
||||
onMouseLeave(event, { ...edge });
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
@@ -198,7 +198,8 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
|
||||
onMouseLeave={onEdgeMouseLeave}
|
||||
onKeyDown={isFocusable ? onKeyDown : undefined}
|
||||
tabIndex={isFocusable ? 0 : undefined}
|
||||
role={isFocusable ? 'button' : 'img'}
|
||||
role={edge.ariaRole ?? (isFocusable ? 'group' : 'img')}
|
||||
aria-roledescription={edge.ariaRoleDescription || 'edge'}
|
||||
data-id={id}
|
||||
data-testid={`rf__edge-${id}`}
|
||||
aria-label={
|
||||
|
||||
@@ -145,10 +145,14 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
// prevent default scrolling behavior on arrow key press when node is moved
|
||||
event.preventDefault();
|
||||
|
||||
const { ariaLabelConfig } = store.getState();
|
||||
|
||||
store.setState({
|
||||
ariaLiveMessage: `Moved selected node ${event.key
|
||||
.replace('Arrow', '')
|
||||
.toLowerCase()}. New position, x: ${~~internals.positionAbsolute.x}, y: ${~~internals.positionAbsolute.y}`,
|
||||
ariaLiveMessage: ariaLabelConfig['node.a11yDescription.ariaLiveMessage']({
|
||||
direction: event.key.replace('Arrow', '').toLowerCase(),
|
||||
x: ~~internals.positionAbsolute.x,
|
||||
y: ~~internals.positionAbsolute.y,
|
||||
}),
|
||||
});
|
||||
|
||||
moveSelectedNodes({
|
||||
@@ -223,7 +227,8 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
onKeyDown={isFocusable ? onKeyDown : undefined}
|
||||
tabIndex={isFocusable ? 0 : undefined}
|
||||
onFocus={isFocusable ? onFocus : undefined}
|
||||
role={isFocusable ? 'button' : undefined}
|
||||
role={node.ariaRole ?? (isFocusable ? 'group' : undefined)}
|
||||
aria-roledescription={node.ariaRoleDescription || 'node'}
|
||||
aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`}
|
||||
aria-label={node.ariaLabel}
|
||||
>
|
||||
|
||||
@@ -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, mergeAriaLabelConfig, AriaLabelConfig } from '@xyflow/system';
|
||||
|
||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||
import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } from '../../types';
|
||||
@@ -68,6 +68,7 @@ const reactFlowFieldsToTrack = [
|
||||
'debug',
|
||||
'autoPanSpeed',
|
||||
'paneClickDistance',
|
||||
'ariaLabelConfig',
|
||||
] as const;
|
||||
|
||||
type ReactFlowFieldsToTrack = (typeof reactFlowFieldsToTrack)[number];
|
||||
@@ -156,6 +157,10 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
|
||||
// Renamed fields
|
||||
else if (fieldName === 'fitView') store.setState({ fitViewQueued: fieldValue as boolean });
|
||||
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOptions: fieldValue as FitViewOptions });
|
||||
|
||||
if (fieldName === 'ariaLabelConfig') {
|
||||
store.setState({ ariaLabelConfig: mergeAriaLabelConfig(fieldValue as AriaLabelConfig) });
|
||||
}
|
||||
// General case
|
||||
else store.setState({ [fieldName]: fieldValue });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user