From 1e5a45a0273189fb0b4df40cb511b6dce299b91a Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 29 May 2026 17:06:55 +0200 Subject: [PATCH] feat(stammbaum): dismissible accessible mobile bottom sheet (#692) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap the mobile person panel in StammbaumBottomSheet: drag-handle grip with swipe-down-to-dismiss (≥80px), full-screen backdrop button for tap-outside dismiss, role=dialog + aria-label, focus trap, and Escape (NFR-A11Y-004). Pan/zoom state is untouched by open/close (US-PANEL-001/002). Co-Authored-By: Claude Opus 4.8 --- .../genealogy/StammbaumBottomSheet.svelte | 74 +++++++++++++++++++ .../StammbaumBottomSheet.svelte.test.ts | 46 ++++++++++++ frontend/src/routes/stammbaum/+page.svelte | 17 ++--- 3 files changed, 127 insertions(+), 10 deletions(-) create mode 100644 frontend/src/lib/person/genealogy/StammbaumBottomSheet.svelte create mode 100644 frontend/src/lib/person/genealogy/StammbaumBottomSheet.svelte.test.ts 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}