diff --git a/frontend/src/routes/search/DisambiguationPicker.svelte b/frontend/src/routes/search/DisambiguationPicker.svelte
new file mode 100644
index 00000000..b99fea10
--- /dev/null
+++ b/frontend/src/routes/search/DisambiguationPicker.svelte
@@ -0,0 +1,86 @@
+
+
+
+
+
open && closePicker()}>
+
+
+ {#if open}
+
+ {#each persons as person (person.id)}
+ -
+
+
+ {/each}
+
+ {/if}
+
diff --git a/frontend/src/routes/search/DisambiguationPicker.svelte.spec.ts b/frontend/src/routes/search/DisambiguationPicker.svelte.spec.ts
new file mode 100644
index 00000000..5b87a996
--- /dev/null
+++ b/frontend/src/routes/search/DisambiguationPicker.svelte.spec.ts
@@ -0,0 +1,71 @@
+import { describe, expect, it, vi, afterEach } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { page } from 'vitest/browser';
+import DisambiguationPicker from './DisambiguationPicker.svelte';
+import type { components } from '$lib/generated/api';
+
+type PersonHint = components['schemas']['PersonHint'];
+
+afterEach(() => cleanup());
+
+const persons: PersonHint[] = [
+ { id: 'w1', displayName: 'Walter Raddatz' },
+ { id: 'w2', displayName: 'Walter Müller' }
+];
+
+function pressEscape() {
+ (document.activeElement as HTMLElement).dispatchEvent(
+ new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
+ );
+}
+
+describe('DisambiguationPicker', () => {
+ it('opens the picker and shows a select option per ambiguous person', async () => {
+ render(DisambiguationPicker, { persons, onSelect: vi.fn() });
+ await page.getByRole('button', { name: /Mehrere Personen gefunden/ }).click();
+ await expect
+ .element(page.getByRole('button', { name: 'Walter Raddatz auswählen' }))
+ .toBeInTheDocument();
+ await expect
+ .element(page.getByRole('button', { name: 'Walter Müller auswählen' }))
+ .toBeInTheDocument();
+ });
+
+ it('moves focus into the picker list on open', async () => {
+ render(DisambiguationPicker, { persons, onSelect: vi.fn() });
+ await page.getByRole('button', { name: /Mehrere Personen gefunden/ }).click();
+ await expect
+ .element(page.getByRole('button', { name: 'Walter Raddatz auswählen' }))
+ .toHaveFocus();
+ });
+
+ it('returns focus to the trigger when closed with Escape', async () => {
+ render(DisambiguationPicker, { persons, onSelect: vi.fn() });
+ const trigger = page.getByRole('button', { name: /Mehrere Personen gefunden/ });
+ await trigger.click();
+ await expect
+ .element(page.getByRole('button', { name: 'Walter Raddatz auswählen' }))
+ .toHaveFocus();
+ pressEscape();
+ await expect.element(trigger).toHaveFocus();
+ });
+
+ it('does not call onSelect when dismissed without choosing', async () => {
+ const onSelect = vi.fn();
+ render(DisambiguationPicker, { persons, onSelect });
+ await page.getByRole('button', { name: /Mehrere Personen gefunden/ }).click();
+ await expect
+ .element(page.getByRole('button', { name: 'Walter Raddatz auswählen' }))
+ .toHaveFocus();
+ pressEscape();
+ expect(onSelect).not.toHaveBeenCalled();
+ });
+
+ it('calls onSelect with the chosen person', async () => {
+ const onSelect = vi.fn();
+ render(DisambiguationPicker, { persons, onSelect });
+ await page.getByRole('button', { name: /Mehrere Personen gefunden/ }).click();
+ await page.getByRole('button', { name: 'Walter Müller auswählen' }).click();
+ expect(onSelect).toHaveBeenCalledWith(persons[1]);
+ });
+});