forked from EduCraft/curriculum-project-hub
103 lines
2.6 KiB
Svelte
103 lines
2.6 KiB
Svelte
<script lang="ts">
|
|
import { Combobox } from 'bits-ui';
|
|
import Icon from './Icon.svelte';
|
|
import type { SelectItem } from './SelectField.svelte';
|
|
|
|
let {
|
|
items,
|
|
value = $bindable(''),
|
|
class: className = '',
|
|
disabled = false,
|
|
placeholder = '请选择…',
|
|
searchPlaceholder = '搜索…',
|
|
emptyText = '无匹配项',
|
|
onchange,
|
|
}: {
|
|
items: SelectItem[];
|
|
value?: string;
|
|
class?: string;
|
|
disabled?: boolean;
|
|
placeholder?: string;
|
|
searchPlaceholder?: string;
|
|
emptyText?: string;
|
|
onchange?: (value: string) => void;
|
|
} = $props();
|
|
|
|
let searchValue = $state('');
|
|
|
|
const filteredItems = $derived.by(() => {
|
|
const q = searchValue.trim().toLowerCase();
|
|
if (q === '') return items;
|
|
return items.filter(
|
|
(item) => item.label.toLowerCase().includes(q) || item.value.toLowerCase().includes(q),
|
|
);
|
|
});
|
|
</script>
|
|
|
|
<Combobox.Root
|
|
type="single"
|
|
{items}
|
|
{disabled}
|
|
{value}
|
|
allowDeselect={false}
|
|
onValueChange={(next) => {
|
|
value = next;
|
|
onchange?.(next);
|
|
}}
|
|
onOpenChangeComplete={(open) => {
|
|
if (!open) searchValue = '';
|
|
}}
|
|
>
|
|
<div class="relative {className}">
|
|
<Combobox.Input
|
|
class="saas-combobox-input"
|
|
{disabled}
|
|
{placeholder}
|
|
aria-label={searchPlaceholder}
|
|
oninput={(e) => {
|
|
searchValue = e.currentTarget.value;
|
|
}}
|
|
/>
|
|
<Combobox.Trigger
|
|
class="absolute inset-y-0 right-0 flex w-9 items-center justify-center text-surface-600 disabled:cursor-not-allowed disabled:opacity-55"
|
|
{disabled}
|
|
aria-label="展开选项"
|
|
>
|
|
<svg
|
|
class="h-4 w-4 shrink-0"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="1.75"
|
|
aria-hidden="true"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15l3.75 3.75L15.75 15" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 9l3.75-3.75L15.75 9" />
|
|
</svg>
|
|
</Combobox.Trigger>
|
|
</div>
|
|
<Combobox.Portal>
|
|
<Combobox.Content class="saas-select-content" sideOffset={6} collisionPadding={8}>
|
|
<Combobox.Viewport class="max-h-72 overflow-y-auto p-1">
|
|
{#each filteredItems as item (item.value)}
|
|
<Combobox.Item
|
|
class="saas-select-item"
|
|
value={item.value}
|
|
label={item.label}
|
|
disabled={item.disabled}
|
|
>
|
|
{#snippet children({ selected })}
|
|
<span class="min-w-0 flex-1 truncate">{item.label}</span>
|
|
{#if selected}
|
|
<Icon name="check" class="ml-2 h-4 w-4 shrink-0 text-primary-600" />
|
|
{/if}
|
|
{/snippet}
|
|
</Combobox.Item>
|
|
{:else}
|
|
<div class="px-2 py-2 text-sm text-surface-600">{emptyText}</div>
|
|
{/each}
|
|
</Combobox.Viewport>
|
|
</Combobox.Content>
|
|
</Combobox.Portal>
|
|
</Combobox.Root>
|