feat(canvas): keyboard-accessible edge anchors via Enter/Space #190

Merged
core-lead merged 4 commits from feat/canvas-keyboard-edge-anchors into main 2026-05-09 22:58:30 +00:00
2 changed files with 39 additions and 7 deletions

View File

@ -1,6 +1,6 @@
"use client";
import { useCallback, useMemo } from "react";
import { useCallback, useMemo, type KeyboardEvent } from "react";
import { Handle, NodeResizer, Position, type NodeProps, type Node } from "@xyflow/react";
import { useCanvasStore, type WorkspaceNodeData } from "@/store/canvas";
import { getConfigurationError, getConfigurationStatus } from "@/store/canvas-topology";
@ -191,7 +191,23 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
<Handle
type="target"
position={Position.Top}
className="!w-2.5 !h-1 !rounded-full !bg-surface-card/80 !border-0 !-top-0.5 hover:!bg-blue-400 hover:!h-1.5 transition-all"
tabIndex={0}
role="button"
aria-label={`Extract ${data.name} from its parent (Enter or Space)`}
onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
e.stopPropagation();
// Keyboard accessibility for edge anchors: pressing Enter/Space on
// the top handle extracts this node from its current parent,
// moving it to the root level. Mirrors the Figma/Excalidraw
// pattern of using the connector dot as a keyboard affordance.
if (data.parentId) {
void nestNode(id, null);
}
}
}}
className="!w-2.5 !h-1 !rounded-full !bg-surface-card/80 !border-0 !-top-0.5 hover:!bg-blue-400 hover:!h-1.5 focus-visible:!bg-blue-400 focus-visible:!h-1.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400/60 focus-visible:ring-offset-1 focus-visible:ring-offset-zinc-950 transition-all"
/>
<div className="relative px-3.5 py-2.5">
@ -358,7 +374,23 @@ export function WorkspaceNode({ id, data }: NodeProps<Node<WorkspaceNodeData>>)
<Handle
type="source"
position={Position.Bottom}
className="!w-2.5 !h-1 !rounded-full !bg-surface-card/80 !border-0 !-bottom-0.5 hover:!bg-blue-400 hover:!h-1.5 transition-all"
tabIndex={0}
role="button"
aria-label={`Nest selected workspace inside ${data.name} (Enter or Space)`}
onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
e.stopPropagation();
// Keyboard accessibility for edge anchors: pressing Enter/Space on
// the bottom handle nests the currently-selected node as a child
// of this node. Requires another node to be selected first.
const selected = selectedNodeId;
if (selected && selected !== id) {
void nestNode(selected, id);
}
}
}}
className="!w-2.5 !h-1 !rounded-full !bg-surface-card/80 !border-0 !-bottom-0.5 hover:!bg-blue-400 hover:!h-1.5 focus-visible:!bg-blue-400 focus-visible:!h-1.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400/60 focus-visible:ring-offset-1 focus-visible:ring-offset-zinc-950 transition-all"
/>
</div>
</>

View File

@ -62,9 +62,9 @@ canvas/src/
### Edge Wiring ✅
- **Edge rendering:** React Flow SVG paths
- **Edge click target:** 1.5px stroke (CSS `stroke-width: 1.5 !important` in globals.css)
- **Edge creation:** React Flow drag-from-handle
- **Edge anchors:** Visible on hover (`hover:!bg-blue-400`), not keyboard accessible
- **Status:** Partial — mouse users only
- **Edge creation:** React Flow drag-from-handle (mouse); keyboard via handle Enter/Space
- **Edge anchors:** Target handle (top): `Enter/Space` extracts node from parent. Source handle (bottom): `Enter/Space` nests selected node into this node. Both have `tabIndex=0`, `role="button"`, descriptive `aria-label`, and a blue focus ring ✅
- **Status:** Mouse + keyboard — keyboard users can nest and un-nest without a mouse
### Canvas Controls ✅
- **Zoom:** React Flow Controls component (verify if keyboard accessible)
@ -111,7 +111,7 @@ canvas/src/
| ~~HIGH~~ | ~~Screen reader announcements for canvas state changes~~ | ~~Canvas.tsx, canvas-events.ts, canvas.ts~~ | ✅ Done — PR #172 |
| MEDIUM | Keyboard shortcut help dialog | useKeyboardShortcuts.ts | ✅ Done (PR #175) |
| MEDIUM | Keyboard-accessible node drag | WorkspaceNode.tsx, useDragHandlers.ts | ✅ Done (this PR) |
| LOW | Edge anchor keyboard accessibility | A2AEdge.tsx | Not started |
| LOW | Keyboard-accessible edge anchors | A2AEdge.tsx, WorkspaceNode.tsx | ✅ Done |
| LOW | Node resize keyboard accessibility | WorkspaceNode.tsx (NodeResizer) | Not started |
---