Timeline: curator event create/edit forms (#781) #832

Open
marcel wants to merge 23 commits from feat/issue-781-timeline-curator-forms into main
2 changed files with 96 additions and 0 deletions
Showing only changes of commit 54f9d8fdd5 - Show all commits

View File

@@ -0,0 +1,69 @@
<script lang="ts">
import { untrack } from 'svelte';
import { radioGroupNav } from '$lib/shared/actions/radioGroupNav';
import { m } from '$lib/paraglide/messages.js';
type EventType = 'PERSONAL' | 'HISTORICAL';
const TYPES: EventType[] = ['PERSONAL', 'HISTORICAL'];
let {
value = 'PERSONAL',
name = 'type',
onchange
}: { value?: string; name?: string; onchange?: (type: EventType) => void } = $props();
let selected = $state<EventType>(
untrack(() => (TYPES.includes(value as EventType) ? (value as EventType) : 'PERSONAL'))
);
let announcement = $state('');
const labels: Record<EventType, () => string> = {
PERSONAL: m.event_type_PERSONAL,
HISTORICAL: m.event_type_HISTORICAL
};
// Decorative accents only — never the sole differentiator (text label is always
// present). aria-hidden so AT announces the label, not the glyph.
const icons: Record<EventType, string> = {
PERSONAL: '👤',
HISTORICAL: '🏛'
};
function select(type: EventType) {
selected = type;
announcement = m.a11y_type_changed({ type: labels[type]() });
onchange?.(type);
}
</script>
<div
role="radiogroup"
aria-label={m.event_editor_type_label()}
class="grid grid-cols-2 gap-2"
use:radioGroupNav={(v) => {
if (TYPES.includes(v as EventType)) select(v as EventType);
}}
>
{#each TYPES as type (type)}
<button
type="button"
role="radio"
value={type}
aria-checked={selected === type}
tabindex={selected === type ? 0 : -1}
onclick={() => select(type)}
class="flex min-h-[48px] cursor-pointer items-center gap-2 rounded-sm border px-3 py-2 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:outline-none {selected ===
type
? 'border-primary bg-primary text-primary-fg'
: 'border-line bg-surface text-ink hover:border-primary/50'}"
>
<span class="text-lg leading-none" aria-hidden="true">{icons[type]}</span>
<span>{labels[type]()}</span>
</button>
{/each}
</div>
<input type="hidden" name={name} value={selected} />
<div class="sr-only" aria-live="polite" aria-atomic="true">{announcement}</div>

View File

@@ -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');
});
});