combobox: default icon support, smart icon width

This commit is contained in:
Elijah Duffy
2025-07-15 18:58:25 -07:00
parent 6825154f8f
commit 8d12c1b05d
2 changed files with 22 additions and 8 deletions

View File

@@ -24,7 +24,7 @@
import { Portal } from '@jsrob/svelte-portal';
import { browser } from '$app/environment';
import { scale } from 'svelte/transition';
import { generateIdentifier } from './util';
import { generateIdentifier, type IconDef } from './util';
import type { ClassValue } from 'svelte/elements';
import { matchSorter } from 'match-sorter';
import Spinner from './Spinner.svelte';
@@ -40,6 +40,8 @@
invalidMessage?: string | null;
label?: string;
placeholder?: string;
/** displayed by default if no item is selected that has its own icon */
icon?: IconDef;
/**
* enables loading spinner and fallback text if no items are found. may be changed
* internally if lazy is enabled. loading is, however, not bindable, so don't expect
@@ -76,6 +78,7 @@
invalidMessage = 'Please select an option',
label,
placeholder,
icon,
loading = false,
lazy = false,
compact = false,
@@ -98,6 +101,7 @@
let searchValue = $state('');
let pickerPosition = $state<'top' | 'bottom'>('bottom');
let searching = $state(false);
let iconWidth = $state<number | undefined>(undefined);
let searchInput = $state<HTMLInputElement | null>(null);
let searchContainer = $state<HTMLDivElement | null>(null);
@@ -121,13 +125,12 @@
});
/** controls whether an icon should be displayed */
let iconVisible = $derived.by(() => {
return (
(open && highlighted && highlighted.icon) ||
let iconVisible = $derived(
(open && highlighted && highlighted.icon) ||
(value && value.icon && searchValue === '') ||
loading
);
});
loading ||
icon !== undefined
);
/** controls whether the highlighted option should be used in the selection preview */
let useHighlighted = $derived.by(() => {
@@ -494,9 +497,11 @@
{#if iconVisible}
<div
class={[
(iconWidth === undefined || iconWidth === 0) && 'opacity-0',
'pointer-events-none absolute top-1/2 left-3.5 -translate-y-1/2 transform select-none'
]}
transition:scale
bind:clientWidth={iconWidth}
>
{#if loading}
<Spinner class="stroke-sui-accent! -mt-0.5" size="1em" />
@@ -504,6 +509,8 @@
{@render highlighted.icon(highlighted)}
{:else if value?.icon}
{@render value.icon(value)}
{:else if icon}
<icon.component {...icon.props} />
{:else}
{/if}
@@ -511,7 +518,8 @@
{/if}
<StyledRawInput
class={[iconVisible && 'pl-10', caret && 'pr-9', !valid && 'border-red-500!']}
style={iconWidth && iconVisible ? `padding-left: ${iconWidth + 14 + 10}px` : undefined}
class={[caret && 'pr-9', !valid && 'border-red-500!']}
{compact}
type="text"
name={name + '_search'}

View File

@@ -151,6 +151,12 @@
/>
<Combobox
icon={{
component: Plus,
props: {
size: '1.2em'
}
}}
label="Lazy combobox"
placeholder="Choose..."
options={lazyOptions}