Merge pull request #2464 from wbkd/refactor/a11y

Refactor: make keyboard a11y better configurable
This commit is contained in:
Moritz Klack
2022-10-03 14:48:17 +02:00
committed by GitHub
25 changed files with 134 additions and 36 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "@reactflow/examples",
"private": true,
"version": "0.0.1",
"version": "0.0.2",
"type": "module",
"scripts": {
"dev": "vite --port 3000 --open",

View File

@@ -92,7 +92,7 @@ const BasicFlow = () => {
defaultEdgeOptions={defaultEdgeOptions}
selectNodesOnDrag={false}
>
<Background variant={BackgroundVariant.Lines} />
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
<Controls />

View File

@@ -1,5 +1,14 @@
# @reactflow/background
## 11.0.1
### Patch Changes
- [`def11008`](https://github.com/wbkd/react-flow/commit/def11008d88749fec40e6fcba8bc41eea2511bab) Thanks [@moklick](https://github.com/moklick)! - default gap = 20
- Updated dependencies [[`def11008`](https://github.com/wbkd/react-flow/commit/def11008d88749fec40e6fcba8bc41eea2511bab), [`d00faa6b`](https://github.com/wbkd/react-flow/commit/d00faa6b3e77388bfd655d4c02e9a5375bc515e4)]:
- @reactflow/core@11.1.0
## 11.0.0
### Major Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@reactflow/background",
"version": "11.0.0",
"version": "11.0.1",
"description": "Background component with different variants for React Flow",
"keywords": [
"react",

View File

@@ -22,7 +22,7 @@ const selector = (s: ReactFlowState) => ({ transform: s.transform, rfId: s.rfId
function Background({
variant = BackgroundVariant.Dots,
gap = 25,
gap = 20,
// only used for dots and cross
size,
// only used for lines and cross

View File

@@ -1,5 +1,12 @@
# @reactflow/controls
## 11.0.1
### Patch Changes
- Updated dependencies [[`def11008`](https://github.com/wbkd/react-flow/commit/def11008d88749fec40e6fcba8bc41eea2511bab), [`d00faa6b`](https://github.com/wbkd/react-flow/commit/d00faa6b3e77388bfd655d4c02e9a5375bc515e4)]:
- @reactflow/core@11.1.0
## 11.0.0
### Major Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@reactflow/controls",
"version": "11.0.0",
"version": "11.0.1",
"description": "Component to control the viewport of a React Flow instance",
"keywords": [
"react",

View File

@@ -1,5 +1,15 @@
# @reactflow/core
## 11.1.0
### Minor Changes
- [`def11008`](https://github.com/wbkd/react-flow/commit/def11008d88749fec40e6fcba8bc41eea2511bab) Thanks [@moklick](https://github.com/moklick)! - New props: nodesFocusable and edgesFocusable
### Patch Changes
- [`d00faa6b`](https://github.com/wbkd/react-flow/commit/d00faa6b3e77388bfd655d4c02e9a5375bc515e4) Thanks [@moklick](https://github.com/moklick)! - Make nopan class name overwritable with class name option
## 11.0.0
### Major Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@reactflow/core",
"version": "11.0.0",
"version": "11.1.0",
"description": "Core components and util functions of React Flow.",
"keywords": [
"react",

View File

@@ -51,7 +51,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
markerStart,
rfId,
ariaLabel,
disableKeyboardA11y,
isFocusable,
pathOptions,
interactionWidth,
}: WrapEdgeProps): JSX.Element | null => {
@@ -158,12 +158,12 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
onMouseEnter={onEdgeMouseEnter}
onMouseMove={onEdgeMouseMove}
onMouseLeave={onEdgeMouseLeave}
onKeyDown={disableKeyboardA11y ? undefined : onKeyDown}
tabIndex={disableKeyboardA11y ? undefined : 0}
role={disableKeyboardA11y ? undefined : 'button'}
onKeyDown={isFocusable ? onKeyDown : undefined}
tabIndex={isFocusable ? 0 : undefined}
role={isFocusable ? 'button' : undefined}
data-testid={`rf__edge-${id}`}
aria-label={ariaLabel === null ? undefined : ariaLabel ? ariaLabel : `Edge from ${source} to ${target}`}
aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_EDGE_DESC_KEY}-${rfId}`}
aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined}
ref={edgeRef}
>
{!updating && (

View File

@@ -11,10 +11,10 @@ import { NodeProps, WrapNodeProps, XYPosition } from '../../types';
import { elementSelectionKeys } from '../../utils';
export const arrowKeyDiffs: Record<string, XYPosition> = {
ArrowUp: { x: 0, y: -10 },
ArrowDown: { x: 0, y: 10 },
ArrowLeft: { x: -10, y: 0 },
ArrowRight: { x: 10, y: 0 },
ArrowUp: { x: 0, y: -1 },
ArrowDown: { x: 0, y: 1 },
ArrowLeft: { x: -1, y: 0 },
ArrowRight: { x: 1, y: 0 },
};
export default (NodeComponent: ComponentType<NodeProps>) => {
@@ -38,6 +38,7 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
isDraggable,
isSelectable,
isConnectable,
isFocusable,
selectNodesOnDrag,
sourcePosition,
targetPosition,
@@ -82,6 +83,7 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
};
const onKeyDown = (event: KeyboardEvent) => {
const { snapGrid, snapToGrid } = store.getState();
if (elementSelectionKeys.includes(event.key) && isSelectable) {
const unselect = event.key === 'Escape';
if (unselect) {
@@ -92,14 +94,28 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
store,
unselect,
});
} else if (selected && Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)) {
} else if (
!disableKeyboardA11y &&
isDraggable &&
selected &&
Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)
) {
store.setState({
ariaLiveMessage: `Moved selected node ten pixels ${event.key
ariaLiveMessage: `Moved selected node ${event.key
.replace('Arrow', '')
.toLowerCase()}. New position, x: ${~~xPos}, y: ${~~yPos}`,
});
updatePositions(arrowKeyDiffs[event.key]);
// by default a node moves 5px on each key press, or 20px if shift is pressed
// if snap grid is enabled, we use that for the velocity.
const xVelo = snapToGrid ? snapGrid[0] : 5;
const yVelo = snapToGrid ? snapGrid[1] : 5;
const factor = event.shiftKey ? 4 : 1;
updatePositions({
x: arrowKeyDiffs[event.key].x * xVelo * factor,
y: arrowKeyDiffs[event.key].y * yVelo * factor,
});
}
};
@@ -151,13 +167,16 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
className={cc([
'react-flow__node',
`react-flow__node-${type}`,
{
// this is overwritable by passing `nopan` as a class name
[noPanClassName]: isDraggable,
},
className,
{
selected,
selectable: isSelectable,
parent: isParent,
dragging,
[noPanClassName]: isDraggable,
},
])}
ref={nodeRef}
@@ -176,9 +195,9 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
onContextMenu={onContextMenuHandler}
onClick={onSelectNodeHandler}
onDoubleClick={onDoubleClickHandler}
onKeyDown={disableKeyboardA11y ? undefined : onKeyDown}
tabIndex={disableKeyboardA11y ? undefined : 0}
role={disableKeyboardA11y ? undefined : 'button'}
onKeyDown={isFocusable ? onKeyDown : undefined}
tabIndex={isFocusable ? 0 : undefined}
role={isFocusable ? 'button' : undefined}
aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`}
aria-label={ariaLabel}
>

View File

@@ -18,6 +18,8 @@ type StoreUpdaterProps = Pick<
| 'onClickConnectEnd'
| 'nodesDraggable'
| 'nodesConnectable'
| 'nodesFocusable'
| 'edgesFocusable'
| 'minZoom'
| 'maxZoom'
| 'nodeExtent'
@@ -89,6 +91,8 @@ const StoreUpdater = ({
onClickConnectEnd,
nodesDraggable,
nodesConnectable,
nodesFocusable,
edgesFocusable,
minZoom,
maxZoom,
nodeExtent,
@@ -145,6 +149,8 @@ const StoreUpdater = ({
useDirectStoreUpdater('onClickConnectEnd', onClickConnectEnd, store.setState);
useDirectStoreUpdater('nodesDraggable', nodesDraggable, store.setState);
useDirectStoreUpdater('nodesConnectable', nodesConnectable, store.setState);
useDirectStoreUpdater('nodesFocusable', nodesFocusable, store.setState);
useDirectStoreUpdater('edgesFocusable', edgesFocusable, store.setState);
useDirectStoreUpdater('elementsSelectable', elementsSelectable, store.setState);
useDirectStoreUpdater('snapToGrid', snapToGrid, store.setState);
useDirectStoreUpdater('snapGrid', snapGrid, store.setState);

View File

@@ -46,6 +46,7 @@ const selector = (s: ReactFlowState) => ({
connectionNodeId: s.connectionNodeId,
connectionHandleType: s.connectionHandleType,
nodesConnectable: s.nodesConnectable,
edgesFocusable: s.edgesFocusable,
elementsSelectable: s.elementsSelectable,
width: s.width,
height: s.height,
@@ -58,6 +59,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
connectionNodeId,
connectionHandleType,
nodesConnectable,
edgesFocusable,
elementsSelectable,
width,
height,
@@ -119,6 +121,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle || null);
const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top;
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
if (!sourceHandle || !targetHandle) {
devWarn(
@@ -181,7 +184,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
onEdgeUpdateEnd={props.onEdgeUpdateEnd}
rfId={props.rfId}
ariaLabel={edge.ariaLabel}
disableKeyboardA11y={props.disableKeyboardA11y}
isFocusable={isFocusable}
pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
interactionWidth={edge.interactionWidth}
/>

View File

@@ -31,12 +31,16 @@ type NodeRendererProps = Pick<
const selector = (s: ReactFlowState) => ({
nodesDraggable: s.nodesDraggable,
nodesConnectable: s.nodesConnectable,
nodesFocusable: s.nodesFocusable,
elementsSelectable: s.elementsSelectable,
updateNodeDimensions: s.updateNodeDimensions,
});
const NodeRenderer = (props: NodeRendererProps) => {
const { nodesDraggable, nodesConnectable, elementsSelectable, updateNodeDimensions } = useStore(selector, shallow);
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, updateNodeDimensions } = useStore(
selector,
shallow
);
const nodes = useVisibleNodes(props.onlyRenderVisibleElements);
const resizeObserverRef = useRef<ResizeObserver>();
@@ -82,6 +86,8 @@ const NodeRenderer = (props: NodeRendererProps) => {
const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'));
const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined'));
const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined'));
const isFocusable = !!(node.focusable || (nodesFocusable && typeof node.focusable === 'undefined'));
const clampedPosition = props.nodeExtent
? clampPosition(node.positionAbsolute, props.nodeExtent)
: node.positionAbsolute;
@@ -122,6 +128,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
isDraggable={isDraggable}
isSelectable={isSelectable}
isConnectable={isConnectable}
isFocusable={isFocusable}
resizeObserver={resizeObserver}
dragHandle={node.dragHandle}
zIndex={node[internalsSymbol]?.z ?? 0}

View File

@@ -108,7 +108,9 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
selectNodesOnDrag = true,
nodesDraggable,
nodesConnectable,
nodesFocusable,
nodeOrigin = initNodeOrigin,
edgesFocusable,
elementsSelectable,
defaultViewport = initDefaultViewport,
minZoom = 0.5,
@@ -245,6 +247,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onClickConnectEnd={onClickConnectEnd}
nodesDraggable={nodesDraggable}
nodesConnectable={nodesConnectable}
nodesFocusable={nodesFocusable}
edgesFocusable={edgesFocusable}
elementsSelectable={elementsSelectable}
minZoom={minZoom}
maxZoom={maxZoom}

View File

@@ -9,17 +9,19 @@ function useUpdateNodePositions() {
const store = useStoreApi();
const updatePositions = useCallback((positionDiff: XYPosition) => {
const { nodeInternals, nodeExtent, updateNodePositions } = store.getState();
const { nodeInternals, nodeExtent, updateNodePositions, snapToGrid, snapGrid } = store.getState();
const selectedNodes = Array.from(nodeInternals.values()).filter((n) => n.selected);
const nodeUpdates = selectedNodes.map((n) => {
if (n.positionAbsolute) {
const updatedPos = calcNextPosition(
n,
{ x: n.positionAbsolute.x + positionDiff.x, y: n.positionAbsolute.y + positionDiff.y },
nodeInternals,
nodeExtent
);
const nextPosition = { x: n.positionAbsolute.x + positionDiff.x, y: n.positionAbsolute.y + positionDiff.y };
if (snapToGrid) {
nextPosition.x = snapGrid[0] * Math.round(nextPosition.x / snapGrid[0]);
nextPosition.y = snapGrid[1] * Math.round(nextPosition.y / snapGrid[1]);
}
const updatedPos = calcNextPosition(n, nextPosition, nodeInternals, nodeExtent);
n.position = updatedPos.position;
n.positionAbsolute = updatedPos.positionAbsolute;

View File

@@ -40,6 +40,8 @@ const initialState: ReactFlowStore = {
nodesDraggable: true,
nodesConnectable: true,
nodesFocusable: true,
edgesFocusable: true,
elementsSelectable: true,
fitViewOnInit: false,
fitViewOnInitDone: false,

View File

@@ -92,7 +92,9 @@ export interface ReactFlowProps extends HTMLAttributes<HTMLDivElement> {
onlyRenderVisibleElements?: boolean;
nodesDraggable?: boolean;
nodesConnectable?: boolean;
nodesFocusable?: boolean;
nodeOrigin?: NodeOrigin;
edgesFocusable?: boolean;
initNodeOrigin?: NodeOrigin;
elementsSelectable?: boolean;
selectNodesOnDrag?: boolean;

View File

@@ -33,6 +33,7 @@ type DefaultEdge<T = any> = {
zIndex?: number;
ariaLabel?: string;
interactionWidth?: number;
focusable?: boolean;
};
export type SmoothStepPathOptions = {
@@ -132,7 +133,7 @@ export type WrapEdgeProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandl
onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge, handleType: HandleType) => void;
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge, handleType: HandleType) => void;
rfId?: string;
disableKeyboardA11y: boolean;
isFocusable: boolean;
pathOptions?: BezierPathOptions | SmoothStepPathOptions;
};

View File

@@ -167,6 +167,8 @@ export type ReactFlowStore = {
nodesDraggable: boolean;
nodesConnectable: boolean;
nodesFocusable: boolean;
edgesFocusable: boolean;
elementsSelectable: boolean;
multiSelectionActive: boolean;

View File

@@ -31,6 +31,7 @@ export interface Node<T = any> {
expandParent?: boolean;
positionAbsolute?: XYPosition;
ariaLabel?: string;
focusable?: boolean;
// only used internally
[internalsSymbol]?: {
@@ -73,6 +74,7 @@ export interface WrapNodeProps<T = any> {
initialized: boolean;
isSelectable: boolean;
isDraggable: boolean;
isFocusable: boolean;
selectNodesOnDrag: boolean;
onClick?: NodeMouseHandler;
onDoubleClick?: NodeMouseHandler;

View File

@@ -1,5 +1,12 @@
# @reactflow/minimap
## 11.0.1
### Patch Changes
- Updated dependencies [[`def11008`](https://github.com/wbkd/react-flow/commit/def11008d88749fec40e6fcba8bc41eea2511bab), [`d00faa6b`](https://github.com/wbkd/react-flow/commit/d00faa6b3e77388bfd655d4c02e9a5375bc515e4)]:
- @reactflow/core@11.1.0
## 11.0.0
### Major Changes
@@ -29,7 +36,6 @@
- @reactflow/controls
- @reactflow/minimap
### Patch Changes
- Updated dependencies:

View File

@@ -1,6 +1,6 @@
{
"name": "@reactflow/minimap",
"version": "11.0.0",
"version": "11.0.1",
"description": "Minimap component for React Flow.",
"keywords": [
"react",

View File

@@ -1,9 +1,25 @@
# reactflow
## 11.1.0
### Minor Changes
- [`def11008`](https://github.com/wbkd/react-flow/commit/def11008d88749fec40e6fcba8bc41eea2511bab) Thanks [@moklick](https://github.com/moklick)! - New props: nodesFocusable and edgesFocusable
### Patch Changes
- [`d00faa6b`](https://github.com/wbkd/react-flow/commit/d00faa6b3e77388bfd655d4c02e9a5375bc515e4) Thanks [@moklick](https://github.com/moklick)! - Make nopan class name overwritable with class name option
- Updated dependencies [[`def11008`](https://github.com/wbkd/react-flow/commit/def11008d88749fec40e6fcba8bc41eea2511bab), [`def11008`](https://github.com/wbkd/react-flow/commit/def11008d88749fec40e6fcba8bc41eea2511bab), [`d00faa6b`](https://github.com/wbkd/react-flow/commit/d00faa6b3e77388bfd655d4c02e9a5375bc515e4)]:
- @reactflow/background@11.0.1
- @reactflow/core@11.1.0
- @reactflow/controls@11.0.1
- @reactflow/minimap@11.0.1
## 11.0.0
Finally it's here! A new version that comes with lots of improvements and the new package name `reactflow`.
From now on you can install it via `npm install reactflow`.
From now on you can install it via `npm install reactflow`.
## Major Changes

View File

@@ -1,6 +1,6 @@
{
"name": "reactflow",
"version": "11.0.0",
"version": "11.1.0",
"description": "A highly customizable React library for building node-based editors and interactive flow charts",
"keywords": [
"react",