feat(a11y): respect prefers-reduced-motion for scroll-sync

Uses scrollIntoView behavior 'instant' instead of 'smooth', skips
CSS animations (static highlight instead), and extends timeout to
2s for reduced-motion users.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-07 11:27:01 +02:00
parent 81b14e5026
commit 10cecb01f5
3 changed files with 32 additions and 7 deletions

View File

@@ -194,4 +194,11 @@ const containerStyle = $derived(
.annotation-flash { .annotation-flash {
animation: annotation-flash-anim 1.5s ease-out; animation: annotation-flash-anim 1.5s ease-out;
} }
@media (prefers-reduced-motion: reduce) {
.annotation-flash {
animation: none;
outline: 3px solid rgba(0, 199, 177, 0.8);
}
}
</style> </style>

View File

@@ -48,4 +48,11 @@ let sorted = $derived([...blocks].sort((a, b) => a.sortOrder - b.sortOrder));
.flash-highlight { .flash-highlight {
animation: flash 1.2s ease-out; animation: flash 1.2s ease-out;
} }
@media (prefers-reduced-motion: reduce) {
.flash-highlight {
animation: none;
background-color: rgba(0, 199, 177, 0.18);
}
}
</style> </style>

View File

@@ -56,6 +56,10 @@ let activeAnnotationId = $state<string | null>(null);
let highlightBlockId = $state<string | null>(null); let highlightBlockId = $state<string | null>(null);
let flashAnnotationId = $state<string | null>(null); let flashAnnotationId = $state<string | null>(null);
const prefersReducedMotion = $derived(
typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches
);
// ── Transcription blocks ───────────────────────────────────────────────────── // ── Transcription blocks ─────────────────────────────────────────────────────
let transcriptionBlocks = $state<TranscriptionBlockData[]>([]); let transcriptionBlocks = $state<TranscriptionBlockData[]>([]);
@@ -162,16 +166,20 @@ async function handleAnnotationClick(annotationId: string) {
const block = transcriptionBlocks.find((b) => b.annotationId === annotationId); const block = transcriptionBlocks.find((b) => b.annotationId === annotationId);
if (block) { if (block) {
highlightBlockId = block.id; highlightBlockId = block.id;
setTimeout(() => { setTimeout(
highlightBlockId = null; () => {
}, 1500); highlightBlockId = null;
},
prefersReducedMotion ? 2000 : 1500
);
} }
// Wait for DOM to render, then scroll to the matching block // Wait for DOM to render, then scroll to the matching block
const scrollBehavior = prefersReducedMotion ? 'instant' : 'smooth';
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (block) { if (block) {
const el = document.querySelector(`[data-block-id="${block.id}"]`); const el = document.querySelector(`[data-block-id="${block.id}"]`);
el?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); el?.scrollIntoView({ behavior: scrollBehavior, block: 'nearest' });
} }
}); });
} }
@@ -179,9 +187,12 @@ async function handleAnnotationClick(annotationId: string) {
function handleParagraphClick(annotationId: string) { function handleParagraphClick(annotationId: string) {
activeAnnotationId = annotationId; activeAnnotationId = annotationId;
flashAnnotationId = annotationId; flashAnnotationId = annotationId;
setTimeout(() => { setTimeout(
flashAnnotationId = null; () => {
}, 1500); flashAnnotationId = null;
},
prefersReducedMotion ? 2000 : 1500
);
} }
// Load blocks when transcribe mode is entered and set default panel mode // Load blocks when transcribe mode is entered and set default panel mode