feat(core,nodes): implement a11y in nodes

Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com>
This commit is contained in:
braks
2022-12-20 00:38:56 +01:00
committed by Braks
parent 0267b2f19e
commit 9f50dd0865
17 changed files with 167 additions and 35 deletions
@@ -1,12 +1,14 @@
<script lang="ts" setup>
import { isNumber } from '@vueuse/core'
import type { GraphNode, HandleConnectable, NodeComponent, SnapGrid, XYZPosition } from '../../types'
import { ARIA_NODE_DESC_KEY } from '../../utils/a11y'
const { id, type, name, draggable, selectable, connectable, ...props } = defineProps<{
id: string
draggable: boolean
selectable: boolean
connectable: HandleConnectable
focusable: boolean
snapGrid?: SnapGrid
type: NodeComponent | Function | Object | false
name: string
@@ -17,6 +19,7 @@ const { id, type, name, draggable, selectable, connectable, ...props } = defineP
provide(NodeId, id)
const {
id: vueFlowId,
edges,
noPanClassName,
selectNodesOnDrag,
@@ -33,8 +36,12 @@ const {
nodeExtent,
onNodesInitialized,
elevateNodesOnSelect,
disableKeyboardA11y,
ariaLiveMessage,
} = $(useVueFlow())
const updateNodePositions = useUpdateNodePositions()
const node = $(useVModel(props, 'node'))
const parentNode = $computed(() => (node.parentNode ? getNode(node.parentNode) : undefined))
@@ -138,33 +145,22 @@ onNodesInitialized(() => {
initialized.value = true
})
/** Initial clamp of node position */
until(initialized)
.toBe(true)
.then(() => {
const extent = applyExtent(node, nodeExtent, parentNode)
const { computedPosition, position } = calcNextPosition(node, node.position, nodeExtent, parentNode)
const clampedPos = clampPosition(node.computedPosition, extent)
node.computedPosition = { ...node.computedPosition, ...clampedPos }
node.position = {
x: node.computedPosition.x - (parentNode?.computedPosition.x || 0),
y: node.computedPosition.y - (parentNode?.computedPosition.y || 0),
}
node.computedPosition = { ...node.computedPosition, ...computedPosition }
node.position = position
})
function updateInternals() {
if (nodeElement.value) updateNodeDimensions([{ id, nodeElement: nodeElement.value, forceUpdate: true }])
updatePosition(
{
x: node.position.x,
y: node.position.y,
// should be using computedPosition.z but in case it's not present, fall back to selected state
z: node.computedPosition.z ? node.computedPosition.z : node.selected ? 1000 : 0,
},
parentNode ? { ...parentNode.computedPosition } : undefined,
)
const { computedPosition, position } = calcNextPosition(node, node.position, nodeExtent, parentNode)
node.computedPosition = { ...node.computedPosition, ...computedPosition }
node.position = position
}
function onMouseEnter(event: MouseEvent) {
@@ -200,6 +196,36 @@ function onSelectNode(event: MouseEvent) {
emit.click({ event, node, connectedEdges })
}
const onKeyDown = (event: KeyboardEvent) => {
if (isInputDOMNode(event)) return
event.preventDefault()
if (elementSelectionKeys.includes(event.key) && selectable) {
const unselect = event.key === 'Escape'
if (unselect) {
nodeElement.value?.blur()
}
handleNodeClick(node, multiSelectionActive, addSelectedNodes, removeSelectedElements, $$(nodesSelectionActive), unselect)
} else if (!disableKeyboardA11y && draggable && node.selected && arrowKeyDiffs[event.key]) {
$$(ariaLiveMessage).value = `Moved selected node ${event.key.replace('Arrow', '').toLowerCase()}. New position, x: ${~~node
.position.x}, y: ${~~node.position.y}`
// 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 = props.snapGrid ? props.snapGrid[0] : 5
const yVelo = props.snapGrid ? props.snapGrid[1] : 5
const factor = event.shiftKey ? 4 : 1
updateNodePositions({
x: arrowKeyDiffs[event.key].x * xVelo * factor,
y: arrowKeyDiffs[event.key].y * yVelo * factor,
})
}
}
</script>
<script lang="ts">
@@ -229,6 +255,10 @@ export default {
pointerEvents: selectable || draggable ? 'all' : 'none',
...getStyle,
}"
:tabIndex="focusable ? 0 : undefined"
:role="focusable ? 'button' : undefined"
:aria-describedby="disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${vueFlowId}`"
:aria-label="node.ariaLabel"
:data-id="node.id"
@mouseenter="onMouseEnter"
@mousemove="onMouseMove"
@@ -236,6 +266,7 @@ export default {
@contextmenu="onContextMenu"
@click="onSelectNode"
@dblclick="onDoubleClick"
@keydown="onKeyDown"
>
<component
:is="type === false ? getNodeTypes.default : type"