Files
clearity/src/lib/components/form-combobox.svelte
T

97 lines
2.4 KiB
Svelte

<script lang="ts">
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
import type { ComponentProps } from 'svelte';
import { tick } from 'svelte';
import { Button } from '$lib/components/ui/button/index.js';
import * as Command from '$lib/components/ui/command/index.js';
import * as Popover from '$lib/components/ui/popover/index.js';
import { cn } from '$lib/utils.js';
type Option = {
value: string;
label: string;
};
const emptyValue = '__form-combobox-empty__';
let {
name,
value = $bindable(''),
options,
placeholder = 'Select an option',
searchPlaceholder = 'Search...',
emptyMessage = 'No results found.',
triggerProps,
onValueChange
}: {
name: string;
value: string;
options: readonly Option[];
placeholder?: string;
searchPlaceholder?: string;
emptyMessage?: string;
triggerProps?: ComponentProps<typeof Button>;
onValueChange?: (value: string) => void;
} = $props();
let open = $state(false);
let triggerRef = $state<HTMLButtonElement>(null!);
const selectedLabel = $derived(
options.find((option) => option.value === value)?.label ?? placeholder
);
function closeAndFocusTrigger() {
open = false;
tick().then(() => triggerRef.focus());
}
</script>
<Popover.Root bind:open>
<Popover.Trigger bind:ref={triggerRef}>
{#snippet child({ props })}
<Button
{...props}
{...triggerProps}
type="button"
variant="outline"
role="combobox"
aria-expanded={open}
class={cn(
'w-full justify-between font-normal',
!value && 'text-muted-foreground',
triggerProps?.class
)}
>
{selectedLabel}
<ChevronsUpDownIcon class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-[var(--bits-popover-anchor-width)] p-0" align="start">
<Command.Root>
<Command.Input placeholder={searchPlaceholder} />
<Command.List>
<Command.Empty>{emptyMessage}</Command.Empty>
<Command.Group>
{#each options as option (option.value)}
<Command.Item
value={option.value || emptyValue}
keywords={[option.label]}
data-checked={value === option.value}
onSelect={() => {
value = option.value;
onValueChange?.(option.value);
closeAndFocusTrigger();
}}
>
{option.label}
</Command.Item>
{/each}
</Command.Group>
</Command.List>
</Command.Root>
</Popover.Content>
</Popover.Root>
<input type="hidden" {name} {value} />