combobox: add compact mode, open state callbacks

This commit is contained in:
Elijah Duffy
2025-07-15 15:24:09 -07:00
parent bb44a946e1
commit 1ae9b3a34f
2 changed files with 47 additions and 24 deletions

View File

@@ -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<HTMLDivElement, { options: ComboboxOption[] }> = (
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 @@
<StyledRawInput
class={[iconVisible && 'pl-10', caret && 'pr-9', !valid && 'border-red-500!']}
{compact}
type="text"
name={name + '_search'}
{placeholder}
@@ -494,7 +516,7 @@
}, 100);
}
open = true;
openPicker();
}}
onkeydown={(e) => {
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;

View File

@@ -148,6 +148,7 @@
/>
<Combobox label="Lazy combobox" placeholder="Choose..." options={[]} lazy />
<Combobox label="Compact combobox" placeholder="Choose..." options={[]} lazy compact />
</InputGroup>
</div>