forked from EduCraft/curriculum-project-hub
feat(hub): add SearchableSelectField component and update RoleCard to use it
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user