diff --git a/frontend/src/lib/timeline/GroupingControl.svelte b/frontend/src/lib/timeline/GroupingControl.svelte new file mode 100644 index 00000000..681c289d --- /dev/null +++ b/frontend/src/lib/timeline/GroupingControl.svelte @@ -0,0 +1,114 @@ + + +
+ {#each segments as segment (segment.value)} + + {/each} +
+{#if disabled} + {m.timeline_grouping_disabled_reason()} +{/if} + + diff --git a/frontend/src/lib/timeline/GroupingControl.svelte.spec.ts b/frontend/src/lib/timeline/GroupingControl.svelte.spec.ts new file mode 100644 index 00000000..6516881d --- /dev/null +++ b/frontend/src/lib/timeline/GroupingControl.svelte.spec.ts @@ -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'); + }); +});