fix(annotations): make resize overlay keyboard-interactive
Some checks failed
CI / Unit & Component Tests (push) Failing after 1s
CI / Backend Unit Tests (push) Failing after 1s
CI / Unit & Component Tests (pull_request) Failing after 2s
CI / Backend Unit Tests (pull_request) Failing after 1s

- Add tabindex="0" so the SVG can receive DOM focus
- Auto-focus the SVG on mount so arrow keys work immediately after
  clicking an annotation to select it
- Show preview rect during keyboard nudging (not just pointer drag) by
  checking hasLiveChanges instead of only checking dragState
- Suppress default browser focus outline (outline: none) on the SVG

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-14 11:47:41 +02:00
parent 9fe5b32a69
commit 2350b4f845
2 changed files with 24 additions and 4 deletions

View File

@@ -42,6 +42,11 @@ $effect(() => {
return () => ro.disconnect();
});
// Auto-focus the SVG when the overlay mounts so arrow keys work immediately.
$effect(() => {
svgEl?.focus({ preventScroll: true });
});
type HandleId = 'nw' | 'ne' | 'sw' | 'se' | 'n' | 's' | 'e' | 'w';
// L-bracket arm length in pixels. Each corner shows two short lines meeting at 90°.
@@ -221,24 +226,31 @@ function handleKeyDown(event: KeyboardEvent): void {
}, 300);
}
// Preview rect in pixel space (maps live normalized coords back to SVG pixel coordinates)
// Preview rect in pixel space (maps live normalized coords back to SVG pixel coordinates).
// Shown during pointer drag and during keyboard nudging (whenever live coords differ from stored).
let previewX = $derived(((liveX - annotation.x) / annotation.width) * svgWidth);
let previewY = $derived(((liveY - annotation.y) / annotation.height) * svgHeight);
let previewW = $derived((liveWidth / annotation.width) * svgWidth);
let previewH = $derived((liveHeight / annotation.height) * svgHeight);
let hasLiveChanges = $derived(
liveX !== annotation.x ||
liveY !== annotation.y ||
liveWidth !== annotation.width ||
liveHeight !== annotation.height
);
</script>
<div aria-live="polite" class="sr-only">
{m.annotation_edit_mode_active()}
</div>
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<svg
bind:this={svgEl}
viewBox="0 0 {svgWidth} {svgHeight}"
role="application"
tabindex="0"
aria-label="Annotation resize handles"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; touch-action: none; overflow: visible;"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; touch-action: none; overflow: visible; outline: none;"
onpointermove={handlePointerMove}
onpointerup={handlePointerUp}
onkeydown={handleKeyDown}
@@ -255,7 +267,7 @@ let previewH = $derived((liveHeight / annotation.height) * svgHeight);
onpointerdown={(e) => handlePointerDown(e, 'move')}
/>
{#if dragState}
{#if dragState || hasLiveChanges}
<rect
x={previewX}
y={previewY}

View File

@@ -60,4 +60,12 @@ describe('AnnotationEditOverlay', () => {
const liveRegion = document.querySelector('[aria-live="polite"]');
expect(liveRegion).not.toBeNull();
});
it('SVG root has tabindex="0" so it can receive keyboard focus', async () => {
render(AnnotationEditOverlay, { annotation });
const svg = document.querySelector('svg[role="application"]');
expect(svg).not.toBeNull();
expect(svg!.getAttribute('tabindex')).toBe('0');
});
});