implemented keyboard interactions

This commit is contained in:
peterkogo
2025-04-16 11:39:50 +02:00
parent 7c776a31a7
commit 3ef63ad10e
14 changed files with 201 additions and 54 deletions
@@ -1,7 +1,7 @@
<script lang="ts">
import { setContext } from 'svelte';
import { getMarkerId } from '@xyflow/system';
import { elementSelectionKeys, getMarkerId } from '@xyflow/system';
import { BezierEdgeInternal } from '$lib/components/edges';
@@ -38,7 +38,8 @@
targetHandle,
markerStart,
markerEnd,
selectable: edgeSelectable,
selectable: _selectable,
focusable: _focusable,
deletable = true,
hidden,
zIndex,
@@ -46,12 +47,15 @@
ariaLabel
} = $derived(edge);
$inspect(edge);
// svelte-ignore non_reactive_update
let edgeRef: SVGGElement | null = null;
const { id } = edge;
setContext('svelteflow__edge_id', id);
let selectable = $derived(edgeSelectable ?? store.elementsSelectable);
let selectable = $derived(_selectable ?? store.elementsSelectable);
let focusable = $derived(_focusable ?? store.edgesFocusable);
let EdgeComponent = $derived(store.edgeTypes[type] ?? BezierEdgeInternal);
let markerStartUrl = $derived(
@@ -70,7 +74,7 @@
}
}
function onMouseEvent<T = MouseEvent>(
function onmouseevent<T = MouseEvent>(
event: T,
callback: ({ edge, event }: { edge: Edge; event: T }) => void
) {
@@ -80,13 +84,29 @@
callback({ event, edge });
}
}
onkeydown = (event: KeyboardEvent) => {
// TODO: Possible Svelte Bug? onkeydown is always firing for the last edge
if (!store.disableKeyboardA11y && elementSelectionKeys.includes(event.key) && selectable) {
const { unselectNodesAndEdges, addSelectedEdges } = store;
const unselect = event.key === 'Escape';
if (unselect) {
edgeRef?.blur();
unselectNodesAndEdges({ edges: [edge] });
} else {
console.log(id);
addSelectedEdges([id]);
}
}
};
</script>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<!-- TODO: aria-label, describedby -->
{#if !hidden}
<svg style:z-index={zIndex}>
<g
bind:this={edgeRef}
class={['svelte-flow__edge', className]}
class:animated
class:selected
@@ -95,17 +115,17 @@
{onclick}
oncontextmenu={onedgecontextmenu
? (e) => {
onMouseEvent(e, onedgecontextmenu);
onmouseevent(e, onedgecontextmenu);
}
: undefined}
onpointerenter={onedgepointerenter
? (e) => {
onMouseEvent(e, onedgepointerenter);
onmouseevent(e, onedgepointerenter);
}
: undefined}
onpointerleave={onedgepointerleave
? (e) => {
onMouseEvent(e, onedgepointerleave);
onmouseevent(e, onedgepointerleave);
}
: undefined}
aria-label={ariaLabel === null
@@ -113,7 +133,9 @@
: ariaLabel
? ariaLabel
: `Edge from ${source} to ${target}`}
role="img"
role={focusable ? 'button' : 'img'}
onkeydown={focusable ? onkeydown : undefined}
tabIndex={focusable ? 0 : undefined}
>
<EdgeComponent
{id}
@@ -1,5 +1,5 @@
<script lang="ts">
import { getInternalNodesBounds, isNumeric, type Rect } from '@xyflow/system';
import { arrowKeyDiffs, getInternalNodesBounds, isNumeric, type Rect } from '@xyflow/system';
import { Selection } from '$lib/components/Selection';
import drag from '$lib/actions/drag';
@@ -16,6 +16,16 @@
onselectioncontextmenu
}: NodeSelectionProps = $props();
let ref = $state<HTMLDivElement>();
$effect(() => {
if (!store.disableKeyboardA11y) {
ref?.focus({
preventScroll: true
});
}
});
let bounds: Rect | null = $derived.by(() => {
if (store.selectionRectMode === 'nodes') {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
@@ -34,6 +44,14 @@
const selectedNodes = store.nodes.filter((n) => n.selected);
onselectionclick?.({ nodes: selectedNodes, event });
}
function onkeydown(event: KeyboardEvent) {
console.log('yas');
if (Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)) {
event.preventDefault();
store.moveSelectedNodes(arrowKeyDiffs[event.key], event.shiftKey ? 4 : 1);
}
}
</script>
{#if store.selectionRectMode === 'nodes' && bounds && isNumeric(bounds.x) && isNumeric(bounds.y)}
@@ -57,9 +75,10 @@
}}
{oncontextmenu}
{onclick}
role="button"
tabindex="-1"
onkeyup={() => {}}
role={store.disableKeyboardA11y ? undefined : 'button'}
tabIndex={store.disableKeyboardA11y ? undefined : -1}
onkeydown={store.disableKeyboardA11y ? undefined : onkeydown}
bind:this={ref}
>
<Selection width="100%" height="100%" x={0} y={0} />
</div>
@@ -1,6 +1,13 @@
<script lang="ts">
import { setContext, onDestroy } from 'svelte';
import { errorMessages, nodeHasDimensions, Position } from '@xyflow/system';
import {
arrowKeyDiffs,
elementSelectionKeys,
errorMessages,
isInputDOMNode,
nodeHasDimensions,
Position
} from '@xyflow/system';
import drag from '$lib/actions/drag';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -31,6 +38,7 @@
selectable: _selectable,
deletable = true,
connectable: _connectable,
focusable: _focusable,
hidden = false,
dragging = false,
style = '',
@@ -57,6 +65,7 @@
let selectable = $derived(_selectable ?? store.elementsSelectable);
let connectable = $derived(_connectable ?? store.nodesConnectable);
let initialized = $derived(nodeHasDimensions(node) && !!node.internals.handleBounds);
let focusable = $derived(_focusable ?? store.nodesFocusable);
function isInParentLookup(id: string) {
return store.parentLookup.has(id);
@@ -155,10 +164,36 @@
onnodeclick?.({ node, event });
}
function onKeyDown(event: KeyboardEvent) {
if (isInputDOMNode(event) || store.disableKeyboardA11y) {
return;
}
if (elementSelectionKeys.includes(event.key) && selectable) {
const unselect = event.key === 'Escape';
store.handleNodeSelection(id, unselect, nodeRef);
} else if (
draggable &&
node.selected &&
Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)
) {
// prevent default scrolling behavior on arrow key press when node is moved
event.preventDefault();
// TODO: aria live message
// store.setState({
// ariaLiveMessage: `Moved selected node ${event.key
// .replace('Arrow', '')
// .toLowerCase()}. New position, x: ${~~internals.positionAbsolute.x}, y: ${~~internals.positionAbsolute.y}`,
// });
store.moveSelectedNodes(arrowKeyDiffs[event.key], event.shiftKey ? 4 : 1);
}
}
</script>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
{#if !hidden}
<div
use:drag={{
@@ -199,6 +234,9 @@
onpointerleave={onnodepointerleave ? (event) => onnodepointerleave({ node, event }) : undefined}
onpointermove={onnodepointermove ? (event) => onnodepointermove({ node, event }) : undefined}
oncontextmenu={onnodecontextmenu ? (event) => onnodecontextmenu({ node, event }) : undefined}
onkeydown={focusable ? onKeyDown : undefined}
tabIndex={focusable ? 0 : undefined}
role={focusable ? 'button' : undefined}
>
<NodeComponent
{data}