diff --git a/src/lib/Combobox.svelte b/src/lib/Combobox.svelte index 118008c..20aa1e4 100644 --- a/src/lib/Combobox.svelte +++ b/src/lib/Combobox.svelte @@ -48,6 +48,8 @@ loading?: boolean; /** applies the loading state on first interaction */ lazy?: boolean; + /** uses a compact layout for the search input with less padding and smaller text */ + compact?: boolean; notFoundMessage?: string; class?: ClassValue | null | undefined; use?: () => void; @@ -55,6 +57,10 @@ onchange?: (item: ComboboxOption) => void; onsearch?: (query: string) => boolean | void; onscroll?: (detail: { event: UIEvent; top: boolean; bottom: boolean }) => void; + /** this callback runs only once on the first interaction if lazy is enabled */ + onlazy?: () => void; + onopen?: () => void; + onclose?: () => void; } let { @@ -70,13 +76,17 @@ placeholder, loading = false, lazy = false, + compact = false, notFoundMessage = 'No results found', class: classValue, use, onvalidate, onchange, onsearch, - onscroll + onscroll, + onlazy, + onopen, + onclose }: Props = $props(); let id = $derived(generateIdentifier('combobox', name)); @@ -155,6 +165,32 @@ return getLogicalOption(getHighlightedIndex(), -1); }; + let lazyApplied = false; + /** opens combobox picker and propages any necessary events */ + const openPicker = () => { + open = true; + + // if lazy and not applied, enable loading state once and run callback + if (lazy && !lazyApplied) { + lazyApplied = true; + loading = true; + onlazy?.(); + } + + updatePickerRect(); // update picker position + scrollToHighlighted(); // scroll to highlighted item + onopen?.(); // trigger onopen event if defined + }; + /** closes combobox picker and propages any necessary events */ + const closePicker = () => { + open = false; + + searchValue = ''; // clear search value for next time picker opens + searching = false; // reset searching state + highlighted = value; // reset highlighted item to current value + onclose?.(); // trigger onclose event if defined + }; + /** action to set the minimum width of the combobox based on the number of options */ const minWidth: Action = ( container, @@ -256,7 +292,7 @@ if (item) { value = item; searchValue = ''; - open = false; + closePicker(); onchange?.(item); } else { console.warn(`Combobox: No option found with value "${searchVal}"`); @@ -280,25 +316,10 @@ return; } - open = false; + closePicker(); }); } - // when the picker is opened, update its position and check if lazy - // when the picker is closed, clear any search value (next time it opens, all options will be shown) - // when the picker is closed, reset highlighted to the currently selected value (if any) - $effect(() => { - if (open) { - if (lazy) loading = true; - updatePickerRect(); - scrollToHighlighted(); - } else { - searchValue = ''; - searching = false; - highlighted = value; - } - }); - // when the value (or, in some circumstances, highlighted item) changes, update the search input $effect(() => { if (!searchInput) return; @@ -349,7 +370,7 @@ role="listbox" onkeydown={(e) => { if (e.key === 'Escape') { - open = false; + closePicker(); searchInput?.focus(); } }} @@ -382,7 +403,7 @@ onclick={() => { if (item.disabled) return; value = item; - open = false; + closePicker(); searchInput?.focus(); onchange?.(item); }} @@ -482,6 +503,7 @@ { if (!searchInput) return; @@ -508,15 +530,15 @@ e.preventDefault(); } - open = false; + closePicker(); return; } else if (e.key === 'Escape') { - open = false; + closePicker(); return; } // open the picker - open = true; + openPicker(); if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { searching = false; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index e1fdd69..dfd250a 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -148,6 +148,7 @@ /> +