feat(timeline): add the Datum·Ereignis·Thema grouping control
An ARIA radiogroup with roving tabindex (#827, REQ-010/011/018): three arrow-key-navigable ≥44px segments with text labels, full-word aria-labels and ≤360px abbreviations, semantic-token colours that hold contrast in dark mode, defaulting to Datum. When disabled it stays in place, retains its selection, and announces a screen-reader reason — deliberately distinct from #780's aria-pressed layer toggles. Refs #827 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
106
frontend/src/lib/timeline/GroupingControl.svelte.spec.ts
Normal file
106
frontend/src/lib/timeline/GroupingControl.svelte.spec.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { cleanup, render } from 'vitest-browser-svelte';
|
||||
import { tick } from 'svelte';
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
import GroupingControl from './GroupingControl.svelte';
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
const radios = () => Array.from(document.querySelectorAll('[role="radio"]')) as HTMLElement[];
|
||||
const group = () => document.querySelector('[role="radiogroup"]') as HTMLElement;
|
||||
const checkedValue = () =>
|
||||
radios()
|
||||
.find((r) => r.getAttribute('aria-checked') === 'true')
|
||||
?.getAttribute('data-value');
|
||||
|
||||
describe('GroupingControl (REQ-010)', () => {
|
||||
it('renders three radios inside a radiogroup, each with aria-checked (a)', () => {
|
||||
render(GroupingControl, {});
|
||||
expect(group()).not.toBeNull();
|
||||
const r = radios();
|
||||
expect(r).toHaveLength(3);
|
||||
r.forEach((radio) => expect(radio.hasAttribute('aria-checked')).toBe(true));
|
||||
});
|
||||
|
||||
it('defaults to Datum (f)', () => {
|
||||
render(GroupingControl, {});
|
||||
expect(radios().filter((r) => r.getAttribute('aria-checked') === 'true')).toHaveLength(1);
|
||||
expect(checkedValue()).toBe('date');
|
||||
});
|
||||
|
||||
it('exposes a text label on every segment, not colour alone (d)', () => {
|
||||
render(GroupingControl, {});
|
||||
radios().forEach((r) => expect((r.textContent ?? '').trim().length).toBeGreaterThan(0));
|
||||
});
|
||||
|
||||
it('gives the radiogroup an accessible name (e)', () => {
|
||||
render(GroupingControl, {});
|
||||
expect(group().getAttribute('aria-label')).toBe(m.timeline_grouping_aria_label());
|
||||
});
|
||||
|
||||
it('each segment has a tap target of at least 44×44px (c)', () => {
|
||||
render(GroupingControl, {});
|
||||
radios().forEach((r) => {
|
||||
const rect = r.getBoundingClientRect();
|
||||
expect(rect.width).toBeGreaterThanOrEqual(44);
|
||||
expect(rect.height).toBeGreaterThanOrEqual(44);
|
||||
});
|
||||
});
|
||||
|
||||
it('exposes each segment full word as an aria-label (REQ-011)', () => {
|
||||
render(GroupingControl, {});
|
||||
const labels = radios().map((r) => r.getAttribute('aria-label'));
|
||||
expect(labels).toEqual([
|
||||
m.timeline_grouping_segment_date(),
|
||||
m.timeline_grouping_segment_event(),
|
||||
m.timeline_grouping_segment_thema()
|
||||
]);
|
||||
});
|
||||
|
||||
it('moves the selection forward with the right arrow key (b)', async () => {
|
||||
render(GroupingControl, { mode: 'date' });
|
||||
group().dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
|
||||
await tick();
|
||||
expect(checkedValue()).toBe('event');
|
||||
});
|
||||
|
||||
it('wraps to the last segment with the left arrow from Datum (b)', async () => {
|
||||
render(GroupingControl, { mode: 'date' });
|
||||
group().dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }));
|
||||
await tick();
|
||||
expect(checkedValue()).toBe('thema');
|
||||
});
|
||||
|
||||
it('selects a segment on click', async () => {
|
||||
render(GroupingControl, { mode: 'date' });
|
||||
const thema = radios().find((r) => r.getAttribute('data-value') === 'thema')!;
|
||||
thema.click();
|
||||
await tick();
|
||||
expect(thema.getAttribute('aria-checked')).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GroupingControl — disabled (REQ-018)', () => {
|
||||
it('marks the radiogroup aria-disabled and keeps all radios in the DOM', () => {
|
||||
render(GroupingControl, { mode: 'event', disabled: true });
|
||||
expect(group().getAttribute('aria-disabled')).toBe('true');
|
||||
expect(radios()).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('announces a screen-reader reason that letters are hidden', () => {
|
||||
render(GroupingControl, { disabled: true });
|
||||
const reason = document.querySelector('[data-testid="grouping-disabled-reason"]');
|
||||
expect(reason?.textContent).toContain(m.timeline_grouping_disabled_reason());
|
||||
});
|
||||
|
||||
it('retains the active mode while disabled (no reset to Datum)', () => {
|
||||
render(GroupingControl, { mode: 'thema', disabled: true });
|
||||
expect(checkedValue()).toBe('thema');
|
||||
});
|
||||
|
||||
it('ignores arrow keys while disabled', () => {
|
||||
render(GroupingControl, { mode: 'event', disabled: true });
|
||||
group().dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
|
||||
expect(checkedValue()).toBe('event');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user