diff --git a/src/lib/Combobox.svelte b/src/lib/Combobox.svelte
index 86a0a46..314b6db 100644
--- a/src/lib/Combobox.svelte
+++ b/src/lib/Combobox.svelte
@@ -4,50 +4,35 @@
value: string;
/**
* Label of the option displayed in the picker and, if the option is
- * selected, in the main input field (unless preview overrides it during
- * combobox open state).
+ * selected, in the main input field. Preview overrides label if provided
+ * and the combobox is open (see usePreview prop for exceptions).
*/
label?: string;
/**
- * An optional custom render function for the option, receives the
- * option as an argument and is used in the picker instead of the
- * label if provided. Note that label should still be provided even
- * if render is used, as it's used for search and accessibility.
+ * Preview text for the option, displayed in the main input field when
+ * the option is selected. See usePreview prop for controlling when
+ * preview takes precedence over label.
*/
- render?: Snippet<[item: ComboboxOption]>;
+ preview?: string;
/**
* Additional information text for the option, displayed beside the
* label in the picker and, if the option is selected, in the main
* input field.
*/
infotext?: string;
- /**
- * Snippet override for infotext, receives the option as an argument.
- * Note that infotext should still be provided even if infotextRender
- * is used, as it's used for search and accessibility.
+ /** An optional icon for the option, displayed in the picker and,
+ * if the option is selected, in the main input field.
*/
- infotextRender?: Snippet<[item: ComboboxOption]>;
- /**
- * Preview text for the option, displayed in the picker and, if the
- * option is selected and the combobox is open, in the main input field.
- */
- preview?: string;
- /**
- * Snippet override for the preview text, receives the option as an
- * argument. Note that preview should still be provided even if
- * previewRender is used, as it's used for search and accessibility.
- */
- previewRender?: Snippet<[item: ComboboxOption]>;
+ icon?: IconDef;
/** Whether the option is disabled */
disabled?: boolean;
- /** An optional icon for the option, displayed in the picker and,
- * if the option is selected, in the closed combobox.
- */
- icon?: Snippet<[item: ComboboxOption]> | IconDef;
};
- const getLabel = (item: ComboboxOption | undefined): string => item?.label ?? item?.value ?? '';
- const getPreview = (item: ComboboxOption | undefined): string => item?.preview ?? getLabel(item);
+ /** returns option label, falling back to value or 'Undefined Option' if no option provided */
+ const getLabel = (opt: ComboboxOption | undefined): string =>
+ opt ? (opt.label ?? opt.value) : 'Undefined Option';
+ /** returns option preview, falling back to getLabel if missing */
+ const getPreview = (opt: ComboboxOption | undefined): string => opt?.preview ?? getLabel(opt);
+
+
{#if open}
@@ -442,67 +521,19 @@
const margin = 10; // 10px margin for top & bottom
const atTop = target.scrollTop < margin;
const atBottom = target.scrollTop + target.clientHeight > target.scrollHeight - margin;
- onscroll({ event: e, top: atTop, bottom: atBottom });
+ onscroll({ event: e, top: atTop, bottom: atBottom, searchInput: searchInput?.value ?? '' });
}}
tabindex="0"
>
- {#each filteredItems as item, i (item.value)}
-
- {
- if (item.disabled) return;
- updateValue(item);
- searchInput?.focus();
- }}
- onkeydown={() => {}}
- tabindex="-1"
- >
-
- {#if item.icon}
- {@render optionIconOrIconDef(item)}
- {/if}
-
-
-
- {@render snippetOrString(item, item.render || getLabel(item))}
-
-
-
- {#if item.infotext || item.infotextRender}
-
- {@render snippetOrString(item, item.infotextRender || item.infotext)}
-
- {/if}
-
-
- {#if value?.value === item.value}
-
-
-
- {/if}
-
+ {#each filteredItems as opt (opt.value)}
+ {@render option(opt)}
{:else}
{#if loading}
- Loading...
+ {@render option(loadingOption, true)}
{:else}
- {notFoundMessage}
+ {@render option(notFoundOption, true)}
{/if}
{/each}
@@ -510,7 +541,7 @@
{/if}
-
+
{#if label}
@@ -543,6 +574,64 @@
{/if}
+{#snippet optionIcon(opt: ComboboxOption)}
+ {#if iconRender}
+ {@render iconRender(opt)}
+ {:else if opt.icon}
+
+ {/if}
+{/snippet}
+
+
+{#snippet option(opt: ComboboxOption, forceDisabled?: boolean)}
+ {@const optDisabled = opt.disabled || forceDisabled}
+
+ {
+ if (optDisabled) return;
+ updateValue(opt);
+ searchInput?.focus();
+ }}
+ onkeydown={() => {}}
+ tabindex="-1"
+ >
+
+ {@render optionIcon(opt)}
+
+
+
+ {@render snippetOrString(opt, labelRender || getLabel(opt))}
+
+
+
+ {#if opt.infotext || infotextRender}
+
+ {@render snippetOrString(opt, infotextRender || opt.infotext)}
+
+ {/if}
+
+
+ {#if value?.value === opt.value}
+
+
+
+ {/if}
+
+{/snippet}
+
{#snippet searchInputBox(caret: boolean = true)}
@@ -558,12 +647,16 @@
>
{#if loading}
- {:else if useHighlighted && highlighted?.icon}
- {@render optionIconOrIconDef(highlighted)}
- {:else if value?.icon}
- {@render optionIconOrIconDef(value)}
+ {:else if useHighlighted && highlighted}
+ {@render optionIcon(highlighted)}
+ {:else if value}
+ {@render optionIcon(value)}
{:else if icon}
-
+ {#if typeof icon === 'function'}
+ {@render icon()}
+ {:else}
+
+ {/if}
{:else}
❌
{/if}
@@ -634,7 +727,7 @@
/>
- {#if (value && (value.infotext || value.infotextRender)) || (highlighted && useHighlighted && (highlighted.infotext || highlighted.infotextRender))}
+ {#if (value && value.infotext) || (highlighted && useHighlighted && highlighted.infotext) || infotextRender}
- {useHighlighted && highlighted?.infotext ? highlighted.infotext : value?.infotext}
- {#if useHighlighted && (highlighted?.infotext || highlighted?.infotextRender)}
- {@render snippetOrString(highlighted!, highlighted.infotextRender)}
- {:else if value?.infotext}
- {@render snippetOrString(value, value.infotextRender)}
+ {#if useHighlighted && highlighted}
+ {@render snippetOrString(highlighted, infotextRender || highlighted.infotext)}
+ {:else if value}
+ {@render snippetOrString(value, infotextRender || value.infotext)}
{/if}
{/if}
@@ -663,14 +755,6 @@
{/snippet}
-{#snippet optionIconOrIconDef(opt: ComboboxOption | undefined)}
- {#if typeof opt?.icon === 'function'}
- {@render opt.icon(opt)}
- {:else if opt?.icon}
-
- {/if}
-{/snippet}
-
{#snippet snippetOrString(
opt: ComboboxOption,
value: string | Snippet<[item: ComboboxOption]> | undefined
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 3a3810e..00a3f58 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -164,16 +164,6 @@
-{#snippet comboRender(item: ComboboxOption)}
- Opt 1
-{/snippet}
-{#snippet comboInfotext(item: ComboboxOption)}
- User
-{/snippet}
-{#snippet comboPreview(item: ComboboxOption)}
- Preview {item.label}
-{/snippet}
-
Combobox
@@ -187,17 +177,21 @@
value: 'option1',
label: 'Option 1',
preview: 'Prvw',
- infotext: 'Info',
- render: comboRender,
- infotextRender: comboInfotext,
- previewRender: comboPreview
+ infotext: 'Info'
},
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3', disabled: true }
]}
onchange={(e) => console.log('Selected:', e.value)}
onvalidate={(e) => console.log('Validation:', e.detail)}
- />
+ >
+ {#snippet labelRender(opt: ComboboxOption)}
+ Processed {opt.label}
+ {/snippet}
+ {#snippet infotextRender(opt: ComboboxOption)}
+ Processed {opt.infotext}
+ {/snippet}
+
{
- setTimeout(() => {
- lazyOptions = [
- { value: 'option1', label: 'Option 1' },
- { value: 'option2', label: 'Option 2' },
- { value: 'option3', label: 'Option 3' }
- ];
- }, 2500);
+ lazy={'always'}
+ onlazy={async () => {
+ await new Promise((resolve) => setTimeout(resolve, 2500));
+ lazyOptions = [
+ { value: 'option1', label: 'Option 1' },
+ { value: 'option2', label: 'Option 2' },
+ { value: 'option3', label: 'Option 3' }
+ ];
+ }}
+ onopenchange={(open) => {
+ if (!open) lazyOptions = [];
}}
/>