combobox: add compact mode, open state callbacks
This commit is contained in:
@@ -48,6 +48,8 @@
|
|||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
/** applies the loading state on first interaction */
|
/** applies the loading state on first interaction */
|
||||||
lazy?: boolean;
|
lazy?: boolean;
|
||||||
|
/** uses a compact layout for the search input with less padding and smaller text */
|
||||||
|
compact?: boolean;
|
||||||
notFoundMessage?: string;
|
notFoundMessage?: string;
|
||||||
class?: ClassValue | null | undefined;
|
class?: ClassValue | null | undefined;
|
||||||
use?: () => void;
|
use?: () => void;
|
||||||
@@ -55,6 +57,10 @@
|
|||||||
onchange?: (item: ComboboxOption) => void;
|
onchange?: (item: ComboboxOption) => void;
|
||||||
onsearch?: (query: string) => boolean | void;
|
onsearch?: (query: string) => boolean | void;
|
||||||
onscroll?: (detail: { event: UIEvent; top: boolean; bottom: 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 {
|
let {
|
||||||
@@ -70,13 +76,17 @@
|
|||||||
placeholder,
|
placeholder,
|
||||||
loading = false,
|
loading = false,
|
||||||
lazy = false,
|
lazy = false,
|
||||||
|
compact = false,
|
||||||
notFoundMessage = 'No results found',
|
notFoundMessage = 'No results found',
|
||||||
class: classValue,
|
class: classValue,
|
||||||
use,
|
use,
|
||||||
onvalidate,
|
onvalidate,
|
||||||
onchange,
|
onchange,
|
||||||
onsearch,
|
onsearch,
|
||||||
onscroll
|
onscroll,
|
||||||
|
onlazy,
|
||||||
|
onopen,
|
||||||
|
onclose
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let id = $derived(generateIdentifier('combobox', name));
|
let id = $derived(generateIdentifier('combobox', name));
|
||||||
@@ -155,6 +165,32 @@
|
|||||||
return getLogicalOption(getHighlightedIndex(), -1);
|
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 */
|
/** action to set the minimum width of the combobox based on the number of options */
|
||||||
const minWidth: Action<HTMLDivElement, { options: ComboboxOption[] }> = (
|
const minWidth: Action<HTMLDivElement, { options: ComboboxOption[] }> = (
|
||||||
container,
|
container,
|
||||||
@@ -256,7 +292,7 @@
|
|||||||
if (item) {
|
if (item) {
|
||||||
value = item;
|
value = item;
|
||||||
searchValue = '';
|
searchValue = '';
|
||||||
open = false;
|
closePicker();
|
||||||
onchange?.(item);
|
onchange?.(item);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Combobox: No option found with value "${searchVal}"`);
|
console.warn(`Combobox: No option found with value "${searchVal}"`);
|
||||||
@@ -280,25 +316,10 @@
|
|||||||
return;
|
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
|
// when the value (or, in some circumstances, highlighted item) changes, update the search input
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!searchInput) return;
|
if (!searchInput) return;
|
||||||
@@ -349,7 +370,7 @@
|
|||||||
role="listbox"
|
role="listbox"
|
||||||
onkeydown={(e) => {
|
onkeydown={(e) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
open = false;
|
closePicker();
|
||||||
searchInput?.focus();
|
searchInput?.focus();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -382,7 +403,7 @@
|
|||||||
onclick={() => {
|
onclick={() => {
|
||||||
if (item.disabled) return;
|
if (item.disabled) return;
|
||||||
value = item;
|
value = item;
|
||||||
open = false;
|
closePicker();
|
||||||
searchInput?.focus();
|
searchInput?.focus();
|
||||||
onchange?.(item);
|
onchange?.(item);
|
||||||
}}
|
}}
|
||||||
@@ -482,6 +503,7 @@
|
|||||||
|
|
||||||
<StyledRawInput
|
<StyledRawInput
|
||||||
class={[iconVisible && 'pl-10', caret && 'pr-9', !valid && 'border-red-500!']}
|
class={[iconVisible && 'pl-10', caret && 'pr-9', !valid && 'border-red-500!']}
|
||||||
|
{compact}
|
||||||
type="text"
|
type="text"
|
||||||
name={name + '_search'}
|
name={name + '_search'}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
@@ -494,7 +516,7 @@
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
open = true;
|
openPicker();
|
||||||
}}
|
}}
|
||||||
onkeydown={(e) => {
|
onkeydown={(e) => {
|
||||||
if (!searchInput) return;
|
if (!searchInput) return;
|
||||||
@@ -508,15 +530,15 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
open = false;
|
closePicker();
|
||||||
return;
|
return;
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === 'Escape') {
|
||||||
open = false;
|
closePicker();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// open the picker
|
// open the picker
|
||||||
open = true;
|
openPicker();
|
||||||
|
|
||||||
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||||
searching = false;
|
searching = false;
|
||||||
|
|||||||
@@ -148,6 +148,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Combobox label="Lazy combobox" placeholder="Choose..." options={[]} lazy />
|
<Combobox label="Lazy combobox" placeholder="Choose..." options={[]} lazy />
|
||||||
|
<Combobox label="Compact combobox" placeholder="Choose..." options={[]} lazy compact />
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user