diff --git a/frontend/src/lib/person/genealogy/StammbaumBottomSheet.svelte b/frontend/src/lib/person/genealogy/StammbaumBottomSheet.svelte
new file mode 100644
index 00000000..22cf3234
--- /dev/null
+++ b/frontend/src/lib/person/genealogy/StammbaumBottomSheet.svelte
@@ -0,0 +1,74 @@
+
+
+
+
+
+
diff --git a/frontend/src/lib/person/genealogy/StammbaumBottomSheet.svelte.test.ts b/frontend/src/lib/person/genealogy/StammbaumBottomSheet.svelte.test.ts
new file mode 100644
index 00000000..a0c1f72c
--- /dev/null
+++ b/frontend/src/lib/person/genealogy/StammbaumBottomSheet.svelte.test.ts
@@ -0,0 +1,46 @@
+import { describe, it, expect, vi } from 'vitest';
+import { render } from 'vitest-browser-svelte';
+import StammbaumBottomSheet from './StammbaumBottomSheet.svelte';
+
+const node = { id: 'p-1', displayName: 'Anna Schmidt', familyMember: true };
+
+describe('StammbaumBottomSheet (#692)', () => {
+ it('renders as a dialog with the person name as its accessible name', async () => {
+ render(StammbaumBottomSheet, { node, canWrite: false, onClose: () => {} });
+ const dialog = document.querySelector('[role="dialog"]')!;
+ expect(dialog).toBeTruthy();
+ expect(dialog.getAttribute('aria-label')).toBe('Anna Schmidt');
+ });
+
+ it('dismisses on Escape', async () => {
+ const onClose = vi.fn();
+ render(StammbaumBottomSheet, { node, canWrite: false, onClose });
+ const dialog = document.querySelector('[role="dialog"]') as HTMLElement;
+ dialog.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
+ expect(onClose).toHaveBeenCalled();
+ });
+
+ it('dismisses when the backdrop is tapped', async () => {
+ const onClose = vi.fn();
+ render(StammbaumBottomSheet, { node, canWrite: false, onClose });
+ const backdrop = document.querySelector('button[aria-label]') as HTMLButtonElement;
+ backdrop.click();
+ expect(onClose).toHaveBeenCalled();
+ });
+
+ it('dismisses on a downward swipe past the threshold', async () => {
+ const onClose = vi.fn();
+ render(StammbaumBottomSheet, { node, canWrite: false, onClose });
+ const handle = document.querySelector('[role="dialog"] > div') as HTMLElement;
+ handle.dispatchEvent(
+ new PointerEvent('pointerdown', { pointerId: 1, clientY: 100, bubbles: true })
+ );
+ handle.dispatchEvent(
+ new PointerEvent('pointermove', { pointerId: 1, clientY: 220, bubbles: true })
+ );
+ handle.dispatchEvent(
+ new PointerEvent('pointerup', { pointerId: 1, clientY: 220, bubbles: true })
+ );
+ expect(onClose).toHaveBeenCalled();
+ });
+});
diff --git a/frontend/src/routes/stammbaum/+page.svelte b/frontend/src/routes/stammbaum/+page.svelte
index 8d8923e2..084f60db 100644
--- a/frontend/src/routes/stammbaum/+page.svelte
+++ b/frontend/src/routes/stammbaum/+page.svelte
@@ -5,6 +5,7 @@ import { page } from '$app/state';
import { replaceState } from '$app/navigation';
import StammbaumTree from '$lib/person/genealogy/StammbaumTree.svelte';
import StammbaumSidePanel from '$lib/person/genealogy/StammbaumSidePanel.svelte';
+import StammbaumBottomSheet from '$lib/person/genealogy/StammbaumBottomSheet.svelte';
import StammbaumControls from '$lib/person/genealogy/StammbaumControls.svelte';
import {
type PanZoomState,
@@ -127,16 +128,12 @@ $effect(() => {
onClose={() => (selectedId = null)}
/>
-
-
- (selectedId = null)}
- />
-
+
+ (selectedId = null)}
+ />
{/if}
{/if}