feat(a11y): add aria live message on move by keyboard

This commit is contained in:
moklick
2022-08-16 18:21:20 +02:00
parent ee5352df8f
commit d2d4b76e65
8 changed files with 43 additions and 17 deletions
@@ -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 -4
View File
@@ -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 }]));
+2
View File
@@ -48,6 +48,8 @@ const initialState: ReactFlowStore = {
connectionStartHandle: null, connectionStartHandle: null,
connectOnClick: true, connectOnClick: true,
ariaLiveMessage: '',
}; };
export default initialState; export default initialState;
+2
View File
@@ -202,6 +202,8 @@ export type ReactFlowStore = {
onViewportChangeEnd?: OnViewportChange; onViewportChangeEnd?: OnViewportChange;
onSelectionChange?: OnSelectionChangeFunc; onSelectionChange?: OnSelectionChangeFunc;
ariaLiveMessage: string;
}; };
export type ReactFlowActions = { export type ReactFlowActions = {