Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
Svelte
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140<script lang="ts"> import ChevronDown from "$icon/chevron-down"; import type { Component, Snippet } from "svelte"; import type { SvelteHTMLElements } from "svelte/elements"; import Combobox from "$lib/components/ui/Combobox.svelte"; import { selectField, type SelectAlign, type SelectOption } from "$lib/components/ui/selectField";
interface Props { options: SelectOption[]; value?: string; rich?: boolean; onSelect?: (value: string) => void; placeholder?: string; label?: string; icon?: Component<SvelteHTMLElements["svg"]>; error?: boolean; disabled?: boolean; id?: string; class?: string; searchPlaceholder?: string; emptyLabel?: string; /** take filtering over, e.g. to query a server: `options` is then rendered as given */ onSearch?: (query: string) => void; loading?: boolean; align?: SelectAlign; panelClass?: string; footer?: Snippet; }
let { options, value = $bindable(""), rich = false, onSelect, placeholder, label, icon: Icon, error = false, disabled = false, id, class: className, searchPlaceholder, emptyLabel, onSearch, loading, align, panelClass, footer }: Props = $props();
const groups = $derived( options.reduce<{ label?: string; options: SelectOption[] }[]>((groups, option) => { const last = groups.at(-1); if (last && last.label === option.group) last.options.push(option); else groups.push({ label: option.group, options: [option] }); return groups; }, []) );
const field = $derived(selectField({ error, disabled }));
// `pl-7` has to be able to beat this `px-2`, so it is merged after rather than interpolated // into the same string — prettier's class sorter would hoist an inline conditional to the // front, where tailwind-merge resolves it the other way and the label lands under the icon const nativeBox = "min-h-9 appearance-none px-2 py-1 pr-8";
const onchange = (event: Event) => { onSelect?.((event.currentTarget as HTMLSelectElement).value); };</script>
{#snippet nativeOptions(items: SelectOption[])} {#each items as option (option.value)} <option value={option.value} disabled={option.disabled} >{option.label ?? option.value}</option > {/each}{/snippet}
{#if rich} <Combobox {options} bind:value {onSelect} {placeholder} {label} icon={Icon} {error} {disabled} {id} {searchPlaceholder} {emptyLabel} {onSearch} {loading} {align} {panelClass} {footer} class={className} />{:else} <div class="relative {className ?? ''}"> {#if Icon} <Icon class="pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2 text-foreground-default" aria-hidden="true" /> {/if} <select bind:value {id} {disabled} {onchange} aria-label={label} aria-invalid={error} class={field.box({ class: [nativeBox, Icon && "pl-7"] })} > {#if placeholder} <option value="">{placeholder}</option> {/if} {#each groups as group (group.label ?? "")} {#if group.label} <optgroup label={group.label}>{@render nativeOptions(group.options)}</optgroup> {:else} {@render nativeOptions(group.options)} {/if} {/each} </select> <ChevronDown class={field.chevron({ class: "pointer-events-none absolute top-1/2 right-2 size-3.5 -translate-y-1/2" })} aria-hidden="true" /> </div>{/if}