combobox: add compact mode, open state callbacks
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -148,6 +148,7 @@
|
||||
/>
|
||||
|
||||
<Combobox label="Lazy combobox" placeholder="Choose..." options={[]} lazy />
|
||||
<Combobox label="Compact combobox" placeholder="Choose..." options={[]} lazy compact />
|
||||
</InputGroup>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user