Files
familienarchiv/frontend/src/routes/briefwechsel/CorrespondentSuggestionsDropdown.svelte
2026-05-05 14:35:15 +02:00

104 lines
3.1 KiB
Svelte

<script lang="ts">
import { m } from '$lib/paraglide/messages.js';
import { clickOutside } from '$lib/shared/actions/clickOutside';
interface Correspondent {
id: string;
firstName?: string | null;
lastName: string;
displayName: string;
}
interface Props {
correspondents: Correspondent[];
loading: boolean;
senderName: string;
onselect: (id: string) => void;
onclose: () => void;
}
let { correspondents, loading, senderName, onselect, onclose }: Props = $props();
function getOptionElements(container: HTMLElement): HTMLElement[] {
return Array.from(container.querySelectorAll<HTMLElement>('[role="option"]'));
}
function handleKeydown(event: KeyboardEvent, container: HTMLElement) {
const options = getOptionElements(container);
const focused = document.activeElement as HTMLElement;
const idx = options.indexOf(focused);
if (event.key === 'ArrowDown') {
event.preventDefault();
const next = options[idx + 1] ?? options[0];
next?.focus();
} else if (event.key === 'ArrowUp') {
event.preventDefault();
const prev = options[idx - 1] ?? options[options.length - 1];
prev?.focus();
} else if (event.key === 'Escape') {
onclose();
}
}
function getInitials(person: Correspondent): string {
if (person.firstName)
return `${person.firstName.charAt(0)}${person.lastName.charAt(0)}`.toUpperCase();
return person.lastName.substring(0, 2).toUpperCase();
}
</script>
<div
use:clickOutside
onclickoutside={onclose}
role="listbox"
tabindex="-1"
aria-label={m.conv_suggestions_heading()}
class="absolute top-full right-0 left-0 z-30 mt-1 rounded-sm border border-line bg-surface shadow-lg"
onkeydown={(e) => handleKeydown(e, e.currentTarget as HTMLElement)}
>
<!-- Heading -->
<div class="px-3 pt-2 pb-1 text-[10px] font-bold tracking-widest text-ink-3 uppercase">
{m.conv_suggestions_heading()}
</div>
<!-- Correspondent rows -->
{#if !loading}
{#each correspondents as person (person.id)}
<div
role="option"
aria-selected="false"
tabindex="0"
class="flex cursor-pointer items-center gap-2 px-3 py-2 text-sm text-ink hover:bg-muted focus:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-inset"
onclick={() => onselect(person.id)}
onkeydown={(e) => e.key === 'Enter' && onselect(person.id)}
>
<!-- Avatar with initials -->
<span
class="flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-primary text-[10px] font-bold text-primary-fg"
aria-hidden="true"
>
{getInitials(person)}
</span>
<!-- Svelte auto-escapes — do not use {@html} here. -->
{person.displayName}
</div>
{/each}
{/if}
<!-- Separator -->
<div class="mt-1 border-t border-line"></div>
<!-- "Alle Korrespondenten" row -->
<div
role="option"
aria-selected="false"
tabindex="0"
class="flex cursor-pointer items-center gap-2 px-3 py-2 text-sm text-ink hover:bg-muted focus:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-inset"
onclick={() => onselect('')}
onkeydown={(e) => e.key === 'Enter' && onselect('')}
>
{m.conv_suggestions_all_label({ name: senderName })}
</div>
</div>