feat(documents): localise timeline bar aria-label (#385)
Replaces the raw "1915-08 · 5" aria-label, which a screen reader
announces as "1915 dash 08 middle dot 5", with the i18n template
timeline_bar_aria("{when}, {count} ...") and a getLocale-formatted
month/year string. Closes Leonie's WCAG 1.3.1 / 4.1.2 finding and
Felix's localisation flag.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1050,5 +1050,6 @@
|
|||||||
"timeline_aria_label": "Zeitachse Dokumentdichte",
|
"timeline_aria_label": "Zeitachse Dokumentdichte",
|
||||||
"timeline_clear_selection": "Auswahl zurücksetzen",
|
"timeline_clear_selection": "Auswahl zurücksetzen",
|
||||||
"timeline_zoom_reset": "Zurück zur Übersicht",
|
"timeline_zoom_reset": "Zurück zur Übersicht",
|
||||||
|
"timeline_bar_aria": "{when}, {count} Dokumente",
|
||||||
"timeline_dragging_aria_live": "Zeitraum {from} bis {to} ausgewählt"
|
"timeline_dragging_aria_live": "Zeitraum {from} bis {to} ausgewählt"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1050,5 +1050,6 @@
|
|||||||
"timeline_aria_label": "Document density timeline",
|
"timeline_aria_label": "Document density timeline",
|
||||||
"timeline_clear_selection": "Clear selection",
|
"timeline_clear_selection": "Clear selection",
|
||||||
"timeline_zoom_reset": "Reset zoom",
|
"timeline_zoom_reset": "Reset zoom",
|
||||||
|
"timeline_bar_aria": "{when}, {count} documents",
|
||||||
"timeline_dragging_aria_live": "Range {from} to {to} selected"
|
"timeline_dragging_aria_live": "Range {from} to {to} selected"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1050,5 +1050,6 @@
|
|||||||
"timeline_aria_label": "Cronología de densidad de documentos",
|
"timeline_aria_label": "Cronología de densidad de documentos",
|
||||||
"timeline_clear_selection": "Borrar selección",
|
"timeline_clear_selection": "Borrar selección",
|
||||||
"timeline_zoom_reset": "Restablecer zoom",
|
"timeline_zoom_reset": "Restablecer zoom",
|
||||||
|
"timeline_bar_aria": "{when}, {count} documentos",
|
||||||
"timeline_dragging_aria_live": "Rango {from} a {to} seleccionado"
|
"timeline_dragging_aria_live": "Rango {from} a {to} seleccionado"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,7 +260,10 @@ const omitTickYear = $derived.by(() => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-testid="timeline-bar"
|
data-testid="timeline-bar"
|
||||||
aria-label="{bucket.month} · {bucket.count}"
|
aria-label={m.timeline_bar_aria({
|
||||||
|
when: formatTickLabel(bucket.month, getLocale()),
|
||||||
|
count: bucket.count
|
||||||
|
})}
|
||||||
aria-pressed={isSelected(bucket.month)}
|
aria-pressed={isSelected(bucket.month)}
|
||||||
onpointerdown={(e) => handlePointerDown(e, i)}
|
onpointerdown={(e) => handlePointerDown(e, i)}
|
||||||
onpointerenter={() => handlePointerEnter(i)}
|
onpointerenter={() => handlePointerEnter(i)}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { cleanup, render } from 'vitest-browser-svelte';
|
|||||||
import { page } from 'vitest/browser';
|
import { page } from 'vitest/browser';
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import TimelineDensityFilter from './TimelineDensityFilter.svelte';
|
import TimelineDensityFilter from './TimelineDensityFilter.svelte';
|
||||||
|
import { formatTickLabel } from './timeline';
|
||||||
|
import { getLocale } from '$lib/paraglide/runtime';
|
||||||
import type { components } from '$lib/generated/api';
|
import type { components } from '$lib/generated/api';
|
||||||
|
|
||||||
type MonthBucket = components['schemas']['MonthBucket'];
|
type MonthBucket = components['schemas']['MonthBucket'];
|
||||||
@@ -206,8 +208,11 @@ describe('TimelineDensityFilter — year-granularity fallback', () => {
|
|||||||
|
|
||||||
const bars = document.querySelectorAll('[data-testid="timeline-bar"]');
|
const bars = document.querySelectorAll('[data-testid="timeline-bar"]');
|
||||||
expect(bars.length).toBe(21);
|
expect(bars.length).toBe(21);
|
||||||
const firstLabel = bars[0].getAttribute('aria-label');
|
const firstLabel = bars[0].getAttribute('aria-label') ?? '';
|
||||||
expect(firstLabel?.startsWith('1900 ·')).toBe(true);
|
// Localised, not the raw machine string "1900 · 1".
|
||||||
|
expect(firstLabel).not.toMatch(/^\d{4} · \d+$/);
|
||||||
|
expect(firstLabel).toContain('1900');
|
||||||
|
expect(firstLabel).toContain('1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clicking a year bar zooms into that year (filter + zoom atomic)', async () => {
|
it('clicking a year bar zooms into that year (filter + zoom atomic)', async () => {
|
||||||
@@ -293,8 +298,32 @@ describe('TimelineDensityFilter — zoom', () => {
|
|||||||
const bars = document.querySelectorAll('[data-testid="timeline-bar"]');
|
const bars = document.querySelectorAll('[data-testid="timeline-bar"]');
|
||||||
// 3 months in zoom range
|
// 3 months in zoom range
|
||||||
expect(bars.length).toBe(3);
|
expect(bars.length).toBe(3);
|
||||||
expect(bars[0].getAttribute('aria-label')?.startsWith('1910-06 ·')).toBe(true);
|
const firstLabel = bars[0].getAttribute('aria-label') ?? '';
|
||||||
expect(bars[2].getAttribute('aria-label')?.startsWith('1910-08 ·')).toBe(true);
|
const lastLabel = bars[2].getAttribute('aria-label') ?? '';
|
||||||
|
// Localised — must NOT contain the raw "YYYY-MM" machine string.
|
||||||
|
expect(firstLabel).not.toMatch(/\d{4}-\d{2}/);
|
||||||
|
expect(lastLabel).not.toMatch(/\d{4}-\d{2}/);
|
||||||
|
expect(firstLabel).toContain(formatTickLabel('1910-06', getLocale()));
|
||||||
|
expect(lastLabel).toContain(formatTickLabel('1910-08', getLocale()));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TimelineDensityFilter — accessibility', () => {
|
||||||
|
it('bar aria-label is built from a localised template, never the raw YYYY-MM', async () => {
|
||||||
|
render(
|
||||||
|
TimelineDensityFilter,
|
||||||
|
makeProps({
|
||||||
|
density: [{ month: '1915-08', count: 5 }],
|
||||||
|
minDate: '1915-08-01',
|
||||||
|
maxDate: '1915-08-31'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const bar = document.querySelector('[data-testid="timeline-bar"]') as HTMLElement;
|
||||||
|
const label = bar.getAttribute('aria-label') ?? '';
|
||||||
|
expect(label).not.toMatch(/1915-08/);
|
||||||
|
expect(label).toContain(formatTickLabel('1915-08', getLocale()));
|
||||||
|
expect(label).toContain('5');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user