Feat: basic keyboard controls and better WAI-ARIA defaults (#2333)

* feat(nodes): focusable and moveable with keys
* refactor(key-handling): cleanup, handle selections
* feat(edges): selectable with keys
* refactor(nodes-edges): cleanup keyboard controls and aria- attrs
* refactor(nodes): node needs to be selected for arrow keys
* refactor(minimap): create const for labelledby

closes #1033
This commit is contained in:
Moritz Klack
2022-08-01 13:58:18 +02:00
committed by GitHub
parent 8eb1053aed
commit f7dc75b0f3
32 changed files with 7921 additions and 5200 deletions
+14 -4
View File
@@ -5,7 +5,7 @@ import { select } from 'd3-selection';
import { useStoreApi } from '../../store';
import { pointToRendererPoint } from '../../utils/graph';
import { NodeDragItem, Node, SelectionDragHandler } from '../../types';
import { getDragItems, getEventHandlerParams, hasSelector, updatePosition } from './utils';
import { getDragItems, getEventHandlerParams, hasSelector, calcNextPosition } from './utils';
import { handleNodeClick } from '../../components/Nodes/utils';
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
@@ -102,9 +102,19 @@ function useDrag({
// skip events without movement
if ((lastPos.current.x !== pointerPos.x || lastPos.current.y !== pointerPos.y) && dragItems.current) {
lastPos.current = pointerPos;
dragItems.current = dragItems.current.map((n) =>
updatePosition(n, pointerPos, nodeInternals, nodeExtent)
);
dragItems.current = dragItems.current.map((n) => {
const updatedPos = calcNextPosition(
n,
{ x: pointerPos.x - n.distance.x, y: pointerPos.y - n.distance.y },
nodeInternals,
nodeExtent
);
n.position = updatedPos.position;
n.positionAbsolute = updatedPos.positionAbsolute;
return n;
});
const onDrag = nodeId ? onNodeDrag : wrapSelectionDragFunc(onSelectionDrag);
+23 -23
View File
@@ -56,25 +56,24 @@ export function getDragItems(nodeInternals: NodeInternals, mousePos: XYPosition,
}));
}
export function updatePosition(
dragItem: NodeDragItem,
mousePos: XYPosition,
export function calcNextPosition(
node: NodeDragItem | Node,
nextPosition: XYPosition,
nodeInternals: NodeInternals,
nodeExtent?: CoordinateExtent
): NodeDragItem {
let currentExtent = dragItem.extent || nodeExtent;
const nextPosition = { x: mousePos.x - dragItem.distance.x, y: mousePos.y - dragItem.distance.y };
): { position: XYPosition; positionAbsolute: XYPosition } {
let currentExtent = node.extent || nodeExtent;
if (dragItem.extent === 'parent') {
if (dragItem.parentNode && dragItem.width && dragItem.height) {
const parent = nodeInternals.get(dragItem.parentNode);
if (node.extent === 'parent') {
if (node.parentNode && node.width && node.height) {
const parent = nodeInternals.get(node.parentNode);
currentExtent =
parent?.positionAbsolute && parent?.width && parent?.height
? [
[parent.positionAbsolute.x, parent.positionAbsolute.y],
[
parent.positionAbsolute.x + parent.width - dragItem.width,
parent.positionAbsolute.y + parent.height - dragItem.height,
parent.positionAbsolute.x + parent.width - node.width,
parent.positionAbsolute.y + parent.height - node.height,
],
]
: currentExtent;
@@ -85,33 +84,34 @@ export function updatePosition(
}
currentExtent = nodeExtent;
}
} else if (dragItem.extent && dragItem.parentNode) {
const parent = nodeInternals.get(dragItem.parentNode);
} else if (node.extent && node.parentNode) {
const parent = nodeInternals.get(node.parentNode);
const parentX = parent?.positionAbsolute?.x ?? 0;
const parentY = parent?.positionAbsolute?.y ?? 0;
currentExtent = [
[dragItem.extent[0][0] + parentX, dragItem.extent[0][1] + parentY],
[dragItem.extent[1][0] + parentX, dragItem.extent[1][1] + parentY],
[node.extent[0][0] + parentX, node.extent[0][1] + parentY],
[node.extent[1][0] + parentX, node.extent[1][1] + parentY],
];
}
let parentPosition = { x: 0, y: 0 };
if (dragItem.parentNode) {
const parentNode = nodeInternals.get(dragItem.parentNode);
if (node.parentNode) {
const parentNode = nodeInternals.get(node.parentNode);
parentPosition = { x: parentNode?.positionAbsolute?.x ?? 0, y: parentNode?.positionAbsolute?.y ?? 0 };
}
dragItem.positionAbsolute = currentExtent
const positionAbsolute = currentExtent
? clampPosition(nextPosition, currentExtent as CoordinateExtent)
: nextPosition;
dragItem.position = {
x: dragItem.positionAbsolute.x - parentPosition.x,
y: dragItem.positionAbsolute.y - parentPosition.y,
return {
position: {
x: positionAbsolute.x - parentPosition.x,
y: positionAbsolute.y - parentPosition.y,
},
positionAbsolute,
};
return dragItem;
}
// returns two params:
+26 -20
View File
@@ -1,35 +1,40 @@
import { useEffect } from 'react';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../store';
import { useStoreApi } from '../store';
import useKeyPress from './useKeyPress';
import { getConnectedEdges } from '../utils/graph';
import { EdgeChange, KeyCode, NodeChange, Node, ReactFlowState } from '../types';
import { KeyCode, NodeChange, Node } from '../types';
interface HookParams {
deleteKeyCode: KeyCode | null;
multiSelectionKeyCode: KeyCode | null;
}
const selector = (s: ReactFlowState) => ({
onNodesChange: s.onNodesChange,
onEdgesChange: s.onEdgesChange,
});
export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
const store = useStoreApi();
const { onNodesChange, onEdgesChange } = useStore(selector, shallow);
const deleteKeyPressed = useKeyPress(deleteKeyCode);
const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode);
useEffect(() => {
const { nodeInternals, edges, hasDefaultNodes, hasDefaultEdges, onNodesDelete, onEdgesDelete } = store.getState();
if (!deleteKeyPressed) {
return;
}
const {
nodeInternals,
edges,
hasDefaultNodes,
hasDefaultEdges,
onNodesDelete,
onEdgesDelete,
onNodesChange,
onEdgesChange,
} = store.getState();
const nodes = Array.from(nodeInternals.values());
const nodesToRemove = nodes.reduce<Node[]>((res, node) => {
if (!node.selected && node.parentNode && res.find((n) => n.id === node.parentNode)) {
res.push(node);
} else if (node.selected) {
const parentSelected = !node.selected && node.parentNode && res.find((n) => n.id === node.parentNode);
if (node.selected || parentSelected) {
res.push(node);
}
@@ -37,7 +42,7 @@ export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
}, []);
const selectedEdges = edges.filter((e) => e.selected);
if (deleteKeyPressed && (nodesToRemove || selectedEdges)) {
if (nodesToRemove || selectedEdges) {
const connectedEdges = getConnectedEdges(nodesToRemove, edges);
const edgesToRemove = [...selectedEdges, ...connectedEdges];
const edgeIdsToRemove = edgesToRemove.reduce<string[]>((res, edge) => {
@@ -69,11 +74,12 @@ export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
onEdgesDelete?.(edgesToRemove);
if (onEdgesChange) {
const edgeChanges: EdgeChange[] = edgeIdsToRemove.map((id) => ({
id,
type: 'remove',
}));
onEdgesChange(edgeChanges);
onEdgesChange(
edgeIdsToRemove.map((id) => ({
id,
type: 'remove',
}))
);
}
}
@@ -88,7 +94,7 @@ export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
store.setState({ nodesSelectionActive: false });
}
}, [deleteKeyPressed, onNodesChange, onEdgesChange]);
}, [deleteKeyPressed]);
useEffect(() => {
store.setState({ multiSelectionActive: multiSelectionKeyPressed });
@@ -0,0 +1,37 @@
import { useCallback } from 'react';
import { useStoreApi } from '../store';
import { calcNextPosition } from './useDrag/utils';
import { XYPosition } from '../types';
function useUpdateNodePositions() {
const store = useStoreApi();
const updatePositions = useCallback((positionDiff: XYPosition) => {
const { nodeInternals, nodeExtent, updateNodePositions } = 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
);
n.position = updatedPos.position;
n.positionAbsolute = updatedPos.positionAbsolute;
}
return n;
});
updateNodePositions(nodeUpdates, true, true);
}, []);
return updatePositions;
}
export default useUpdateNodePositions;