refactor(geschichten): migrate TypeSelector onto SegmentedControl primitive
Refs #857 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { radioGroupNav } from '$lib/shared/actions/radioGroupNav';
|
||||
import SegmentedControl from '$lib/shared/primitives/SegmentedControl.svelte';
|
||||
|
||||
type GeschichteType = 'STORY' | 'JOURNEY';
|
||||
|
||||
const TYPES: GeschichteType[] = ['STORY', 'JOURNEY'];
|
||||
|
||||
interface Props {
|
||||
onweiter: (type: GeschichteType) => void;
|
||||
}
|
||||
@@ -15,10 +13,6 @@ let { onweiter }: Props = $props();
|
||||
let selected = $state<GeschichteType | null>(null);
|
||||
let announcement = $state('');
|
||||
|
||||
// Roving-tabindex holder: falls back to the first card so keyboard nav can start
|
||||
// even when nothing is selected (all cards at tabindex=-1 would be a keyboard dead-spot).
|
||||
const rovingFocusType = $derived(selected ?? TYPES[0]);
|
||||
|
||||
function select(type: GeschichteType) {
|
||||
selected = type;
|
||||
announcement = '';
|
||||
@@ -32,15 +26,10 @@ function handleWeiter() {
|
||||
onweiter(selected);
|
||||
}
|
||||
|
||||
const titles: Record<GeschichteType, () => string> = {
|
||||
STORY: m.journey_selector_story_title,
|
||||
JOURNEY: m.journey_selector_journey_title
|
||||
};
|
||||
|
||||
const descs: Record<GeschichteType, () => string> = {
|
||||
STORY: m.journey_selector_story_desc,
|
||||
JOURNEY: m.journey_selector_journey_desc
|
||||
};
|
||||
const typeOptions = [
|
||||
{ value: 'STORY', label: m.journey_selector_story_title() },
|
||||
{ value: 'JOURNEY', label: m.journey_selector_journey_title() }
|
||||
];
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@@ -48,31 +37,14 @@ const descs: Record<GeschichteType, () => string> = {
|
||||
{m.journey_selector_question()}
|
||||
</p>
|
||||
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-labelledby="type-selector-label"
|
||||
class="grid grid-cols-1 gap-4 sm:grid-cols-2"
|
||||
use:radioGroupNav={(v) => {
|
||||
if (TYPES.includes(v as GeschichteType)) select(v as GeschichteType);
|
||||
<SegmentedControl
|
||||
options={typeOptions}
|
||||
value={selected ?? ''}
|
||||
onChange={(v) => {
|
||||
if (v === 'STORY' || v === 'JOURNEY') select(v);
|
||||
}}
|
||||
>
|
||||
{#each TYPES as type (type)}
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
value={type}
|
||||
aria-checked={selected === type}
|
||||
tabindex={type === rovingFocusType ? 0 : -1}
|
||||
onclick={() => select(type)}
|
||||
class="min-h-[64px] cursor-pointer rounded border px-4 py-3 text-left transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring {selected === type
|
||||
? 'border-primary bg-primary text-primary-fg'
|
||||
: 'border-line bg-surface text-ink hover:border-primary/50'}"
|
||||
>
|
||||
<span class="block font-sans text-sm font-bold">{titles[type]()}</span>
|
||||
<span class="mt-1 block font-sans text-xs text-current opacity-70">{descs[type]()}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
label={m.journey_selector_question()}
|
||||
/>
|
||||
|
||||
<div aria-live="polite" aria-atomic="true" class="sr-only">{announcement}</div>
|
||||
|
||||
|
||||
@@ -18,13 +18,18 @@ describe('TypeSelector', () => {
|
||||
await expect.element(page.getByRole('radio', { name: /Lesereise/i })).toBeVisible();
|
||||
});
|
||||
|
||||
it('radiogroup is correctly labelled', async () => {
|
||||
it('radiogroup is correctly labelled (via aria-label or aria-labelledby)', async () => {
|
||||
render(TypeSelector, { props: { onweiter: vi.fn() } });
|
||||
|
||||
const group = document.querySelector('[role="radiogroup"]');
|
||||
const labelledBy = group?.getAttribute('aria-labelledby');
|
||||
// SegmentedControl uses aria-label; the old TypeSelector used aria-labelledby.
|
||||
// Accept either as long as the accessible name is non-empty.
|
||||
const ariaLabel = group?.getAttribute('aria-label') ?? '';
|
||||
const labelledBy = group?.getAttribute('aria-labelledby') ?? '';
|
||||
const labelEl = labelledBy ? document.getElementById(labelledBy) : null;
|
||||
expect(labelEl?.textContent?.trim().length).toBeGreaterThan(0);
|
||||
const hasAccessibleName =
|
||||
ariaLabel.trim().length > 0 || (labelEl?.textContent?.trim().length ?? 0) > 0;
|
||||
expect(hasAccessibleName).toBe(true);
|
||||
});
|
||||
|
||||
it('Weiter button has aria-disabled=true when nothing is selected', async () => {
|
||||
|
||||
Reference in New Issue
Block a user