From 5028082da401beb6581326f0a00e08ea24d2fd4a Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 8 May 2026 09:56:20 +0200 Subject: [PATCH] feat(documents): aria-live drag preview for screen readers (#385) Adds a visually-hidden polite live region whose text reflects the current drag range using the existing timeline_dragging_aria_live i18n key. Closes Leonie's WCAG follow-the-drag-preview gap and turns the previously orphaned i18n key into used markup. Co-Authored-By: Claude Opus 4.7 --- .../lib/document/TimelineDensityFilter.svelte | 17 +++++++ .../TimelineDensityFilter.svelte.spec.ts | 47 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/frontend/src/lib/document/TimelineDensityFilter.svelte b/frontend/src/lib/document/TimelineDensityFilter.svelte index 973d3c1b..d13d6e61 100644 --- a/frontend/src/lib/document/TimelineDensityFilter.svelte +++ b/frontend/src/lib/document/TimelineDensityFilter.svelte @@ -225,6 +225,19 @@ const dragWindowRightPct = $derived.by(() => { }); const tickIndices = $derived(tickIndicesFor(filled)); + +// While dragging, expose the live preview range to assistive tech via a +// polite live region. Empty text outside drag avoids announcing residual state. +const dragLiveMessage = $derived.by(() => { + if (!isDragging || dragLowIndex === null || dragHighIndex === null) return ''; + const fromLabel = filled[dragLowIndex]?.month; + const toLabel = filled[dragHighIndex]?.month; + if (!fromLabel || !toLabel) return ''; + return m.timeline_dragging_aria_live({ + from: formatTickLabel(fromLabel, getLocale()), + to: formatTickLabel(toLabel, getLocale()) + }); +}); const omitTickYear = $derived.by(() => { if (filled.length === 0 || filled[0].month.length === 4) return false; const firstYear = filled[0].month.slice(0, 4); @@ -306,6 +319,10 @@ const omitTickYear = $derived.by(() => { +
+ {dragLiveMessage} +
+
{#if isZoomed}