From 54f9d8fdd5233fa418a967b27e3bda43895e943c Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 13 Jun 2026 22:40:00 +0200 Subject: [PATCH] feat(timeline): add EventTypeSelect segmented radio (PERSONAL/HISTORICAL) grid-cols-2 segmented radio group modelled on PersonTypeSelector: role=radiogroup with role=radio buttons, roving tabindex, radioGroupNav arrow-key support, and an sr-only aria-live type-change announcement. Each option pairs a decorative aria-hidden icon with a visible localized text label (icon is never the sole differentiator), min-h-48px target. Emits a hidden input for form submission. Refs #781 Co-Authored-By: Claude Opus 4.8 --- .../src/lib/timeline/EventTypeSelect.svelte | 69 +++++++++++++++++++ .../timeline/EventTypeSelect.svelte.spec.ts | 27 ++++++++ 2 files changed, 96 insertions(+) create mode 100644 frontend/src/lib/timeline/EventTypeSelect.svelte create mode 100644 frontend/src/lib/timeline/EventTypeSelect.svelte.spec.ts diff --git a/frontend/src/lib/timeline/EventTypeSelect.svelte b/frontend/src/lib/timeline/EventTypeSelect.svelte new file mode 100644 index 00000000..18a6f347 --- /dev/null +++ b/frontend/src/lib/timeline/EventTypeSelect.svelte @@ -0,0 +1,69 @@ + + +
{ + if (TYPES.includes(v as EventType)) select(v as EventType); + }} +> + {#each TYPES as type (type)} + + {/each} +
+ + + +
{announcement}
diff --git a/frontend/src/lib/timeline/EventTypeSelect.svelte.spec.ts b/frontend/src/lib/timeline/EventTypeSelect.svelte.spec.ts new file mode 100644 index 00000000..f95708c5 --- /dev/null +++ b/frontend/src/lib/timeline/EventTypeSelect.svelte.spec.ts @@ -0,0 +1,27 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import EventTypeSelect from './EventTypeSelect.svelte'; + +afterEach(() => cleanup()); + +describe('EventTypeSelect — segmented PERSONAL/HISTORICAL radio', () => { + it('renders exactly two radio options', async () => { + render(EventTypeSelect, { value: 'PERSONAL' }); + const radios = document.querySelectorAll('[role="radio"]'); + expect(radios.length).toBe(2); + }); + + it('marks the initial value as checked and seeds the hidden input', async () => { + render(EventTypeSelect, { value: 'HISTORICAL', name: 'type' }); + const hidden = document.querySelector('input[type="hidden"][name="type"]') as HTMLInputElement; + expect(hidden.value).toBe('HISTORICAL'); + }); + + it('selects HISTORICAL and updates the hidden input when clicked', async () => { + render(EventTypeSelect, { value: 'PERSONAL', name: 'type' }); + await page.getByRole('radio', { name: 'Historisch' }).click(); + const hidden = document.querySelector('input[type="hidden"][name="type"]') as HTMLInputElement; + expect(hidden.value).toBe('HISTORICAL'); + }); +});