fix(annotations): make resize overlay keyboard-interactive
- 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:
@@ -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}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user