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();
|
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';
|
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°.
|
// 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);
|
}, 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 previewX = $derived(((liveX - annotation.x) / annotation.width) * svgWidth);
|
||||||
let previewY = $derived(((liveY - annotation.y) / annotation.height) * svgHeight);
|
let previewY = $derived(((liveY - annotation.y) / annotation.height) * svgHeight);
|
||||||
let previewW = $derived((liveWidth / annotation.width) * svgWidth);
|
let previewW = $derived((liveWidth / annotation.width) * svgWidth);
|
||||||
let previewH = $derived((liveHeight / annotation.height) * svgHeight);
|
let previewH = $derived((liveHeight / annotation.height) * svgHeight);
|
||||||
|
let hasLiveChanges = $derived(
|
||||||
|
liveX !== annotation.x ||
|
||||||
|
liveY !== annotation.y ||
|
||||||
|
liveWidth !== annotation.width ||
|
||||||
|
liveHeight !== annotation.height
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div aria-live="polite" class="sr-only">
|
<div aria-live="polite" class="sr-only">
|
||||||
{m.annotation_edit_mode_active()}
|
{m.annotation_edit_mode_active()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
||||||
<svg
|
<svg
|
||||||
bind:this={svgEl}
|
bind:this={svgEl}
|
||||||
viewBox="0 0 {svgWidth} {svgHeight}"
|
viewBox="0 0 {svgWidth} {svgHeight}"
|
||||||
role="application"
|
role="application"
|
||||||
|
tabindex="0"
|
||||||
aria-label="Annotation resize handles"
|
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}
|
onpointermove={handlePointerMove}
|
||||||
onpointerup={handlePointerUp}
|
onpointerup={handlePointerUp}
|
||||||
onkeydown={handleKeyDown}
|
onkeydown={handleKeyDown}
|
||||||
@@ -255,7 +267,7 @@ let previewH = $derived((liveHeight / annotation.height) * svgHeight);
|
|||||||
onpointerdown={(e) => handlePointerDown(e, 'move')}
|
onpointerdown={(e) => handlePointerDown(e, 'move')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if dragState}
|
{#if dragState || hasLiveChanges}
|
||||||
<rect
|
<rect
|
||||||
x={previewX}
|
x={previewX}
|
||||||
y={previewY}
|
y={previewY}
|
||||||
|
|||||||
@@ -60,4 +60,12 @@ describe('AnnotationEditOverlay', () => {
|
|||||||
const liveRegion = document.querySelector('[aria-live="polite"]');
|
const liveRegion = document.querySelector('[aria-live="polite"]');
|
||||||
expect(liveRegion).not.toBeNull();
|
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