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 = []; }} />