feat(a11y): add aria live message on move by keyboard
This commit is contained in:
@@ -1,12 +1,28 @@
|
|||||||
const style = { display: 'none' };
|
import { CSSProperties } from 'react';
|
||||||
|
import { useStore } from '../../hooks/useStore';
|
||||||
|
import { ReactFlowState } from '../../types';
|
||||||
|
|
||||||
|
const style: CSSProperties = { display: 'none' };
|
||||||
|
const ariaLiveStyle: CSSProperties = {
|
||||||
|
position: 'absolute',
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
margin: -1,
|
||||||
|
border: 0,
|
||||||
|
padding: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
clip: 'rect(0px, 0px, 0px, 0px)',
|
||||||
|
clipPath: 'inset(100%)',
|
||||||
|
};
|
||||||
|
|
||||||
export const ARIA_NODE_DESC_KEY = 'react-flow__node-desc';
|
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__arai-live';
|
||||||
|
|
||||||
function A11yDescriptions({ rfId, disableKeyboardA11y }: { rfId: string; disableKeyboardA11y: boolean }) {
|
const selector = (s: ReactFlowState) => s.ariaLiveMessage;
|
||||||
if (disableKeyboardA11y) {
|
|
||||||
return null;
|
function A11yDescriptions({ rfId }: { rfId: string }) {
|
||||||
}
|
const ariaLiveMessage = useStore(selector);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -17,6 +33,9 @@ function A11yDescriptions({ rfId, disableKeyboardA11y }: { rfId: string; disable
|
|||||||
<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 press escape to cancel.
|
Press enter or space to select an edge. You can then press delete to remove it or press escape to cancel.
|
||||||
</div>
|
</div>
|
||||||
|
<div id={`${ARIA_LIVE_MESSAGE}-${rfId}`} aria-live="assertive" aria-atomic="true" style={ariaLiveStyle}>
|
||||||
|
{ariaLiveMessage}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,12 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
|||||||
unselect,
|
unselect,
|
||||||
});
|
});
|
||||||
} else if (selected && Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)) {
|
} else if (selected && Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)) {
|
||||||
|
store.setState({
|
||||||
|
ariaLiveMessage: `Move selected node ten pixels to the ${event.key
|
||||||
|
.replace('Arrow', '')
|
||||||
|
.toLowerCase()}. New position, x: ${xPos}, y: ${yPos}`,
|
||||||
|
});
|
||||||
|
|
||||||
updatePositions(arrowKeyDiffs[event.key]);
|
updatePositions(arrowKeyDiffs[event.key]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
|||||||
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
|
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
|
||||||
{children}
|
{children}
|
||||||
<Attribution proOptions={proOptions} position={attributionPosition} />
|
<Attribution proOptions={proOptions} position={attributionPosition} />
|
||||||
<A11yDescriptions rfId={rfId} disableKeyboardA11y={disableKeyboardA11y} />
|
{!disableKeyboardA11y && <A11yDescriptions rfId={rfId} />}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ interface HookParams {
|
|||||||
|
|
||||||
export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
|
export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
|
|
||||||
const deleteKeyPressed = useKeyPress(deleteKeyCode);
|
const deleteKeyPressed = useKeyPress(deleteKeyCode);
|
||||||
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode);
|
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode);
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ import useReactFlow from './useReactFlow';
|
|||||||
import { OnInit } from '../types';
|
import { OnInit } from '../types';
|
||||||
|
|
||||||
function useOnInitHandler(onInit: OnInit | undefined) {
|
function useOnInitHandler(onInit: OnInit | undefined) {
|
||||||
const ReactFlowInstance = useReactFlow();
|
const rfInstance = useReactFlow();
|
||||||
const isInitialized = useRef<boolean>(false);
|
const isInitialized = useRef<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isInitialized.current && ReactFlowInstance.viewportInitialized && onInit) {
|
if (!isInitialized.current && rfInstance.viewportInitialized && onInit) {
|
||||||
setTimeout(() => onInit(ReactFlowInstance), 1);
|
setTimeout(() => onInit(rfInstance), 1);
|
||||||
isInitialized.current = true;
|
isInitialized.current = true;
|
||||||
}
|
}
|
||||||
}, [onInit, ReactFlowInstance.viewportInitialized]);
|
}, [onInit, rfInstance.viewportInitialized]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useOnInitHandler;
|
export default useOnInitHandler;
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { useStore, useStoreApi } from '../hooks/useStore';
|
import { useStoreApi } from '../hooks/useStore';
|
||||||
import { UpdateNodeInternals, ReactFlowState } from '../types';
|
import { UpdateNodeInternals } from '../types';
|
||||||
|
|
||||||
const selector = (state: ReactFlowState) => state.updateNodeDimensions;
|
|
||||||
|
|
||||||
function useUpdateNodeInternals(): UpdateNodeInternals {
|
function useUpdateNodeInternals(): UpdateNodeInternals {
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const updateNodeDimensions = useStore(selector);
|
|
||||||
|
|
||||||
return useCallback<UpdateNodeInternals>((id: string) => {
|
return useCallback<UpdateNodeInternals>((id: string) => {
|
||||||
const nodeElement = store.getState().domNode?.querySelector(`.react-flow__node[data-id="${id}"]`) as HTMLDivElement;
|
const { domNode, updateNodeDimensions } = store.getState();
|
||||||
|
const nodeElement = domNode?.querySelector(`.react-flow__node[data-id="${id}"]`) as HTMLDivElement;
|
||||||
|
|
||||||
if (nodeElement) {
|
if (nodeElement) {
|
||||||
requestAnimationFrame(() => updateNodeDimensions([{ id, nodeElement, forceUpdate: true }]));
|
requestAnimationFrame(() => updateNodeDimensions([{ id, nodeElement, forceUpdate: true }]));
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ const initialState: ReactFlowStore = {
|
|||||||
|
|
||||||
connectionStartHandle: null,
|
connectionStartHandle: null,
|
||||||
connectOnClick: true,
|
connectOnClick: true,
|
||||||
|
|
||||||
|
ariaLiveMessage: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default initialState;
|
export default initialState;
|
||||||
|
|||||||
@@ -202,6 +202,8 @@ export type ReactFlowStore = {
|
|||||||
onViewportChangeEnd?: OnViewportChange;
|
onViewportChangeEnd?: OnViewportChange;
|
||||||
|
|
||||||
onSelectionChange?: OnSelectionChangeFunc;
|
onSelectionChange?: OnSelectionChangeFunc;
|
||||||
|
|
||||||
|
ariaLiveMessage: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ReactFlowActions = {
|
export type ReactFlowActions = {
|
||||||
|
|||||||
Reference in New Issue
Block a user