fix(app): update combobox options behavior and tests

This commit is contained in:
Mohamed Boudra
2026-02-17 10:38:31 +07:00
parent 459af22670
commit 50b99584a1
7 changed files with 515 additions and 103 deletions
@@ -0,0 +1,95 @@
export interface ComboboxOptionModel {
id: string;
label: string;
description?: string;
kind?: "directory";
}
export interface BuildVisibleComboboxOptionsInput {
options: ComboboxOptionModel[];
searchQuery: string;
searchable: boolean;
allowCustomValue: boolean;
customValuePrefix: string;
customValueDescription?: string;
customValueKind?: "directory";
}
export function shouldShowCustomComboboxOption(input: {
options: ComboboxOptionModel[];
searchQuery: string;
searchable: boolean;
allowCustomValue: boolean;
}): boolean {
const sanitizedSearchValue = input.searchQuery.trim();
if (!input.searchable || !input.allowCustomValue || sanitizedSearchValue.length === 0) {
return false;
}
return !input.options.some(
(opt) =>
opt.id.toLowerCase() === sanitizedSearchValue.toLowerCase() ||
opt.label.toLowerCase() === sanitizedSearchValue.toLowerCase()
);
}
export function buildVisibleComboboxOptions(
input: BuildVisibleComboboxOptionsInput
): ComboboxOptionModel[] {
const normalizedSearch = input.searchable ? input.searchQuery.trim().toLowerCase() : "";
const filteredOptions = normalizedSearch
? input.options.filter(
(opt) =>
opt.label.toLowerCase().includes(normalizedSearch) ||
opt.id.toLowerCase().includes(normalizedSearch) ||
opt.description?.toLowerCase().includes(normalizedSearch)
)
: input.options;
const sanitizedSearchValue = input.searchQuery.trim();
const showCustomOption = shouldShowCustomComboboxOption({
options: input.options,
searchQuery: input.searchQuery,
searchable: input.searchable,
allowCustomValue: input.allowCustomValue,
});
const visibleOptions: ComboboxOptionModel[] = [];
if (showCustomOption) {
const trimmedPrefix = input.customValuePrefix.trim();
const customLabel =
trimmedPrefix.length > 0
? `${trimmedPrefix} "${sanitizedSearchValue}"`
: sanitizedSearchValue;
visibleOptions.push({
id: sanitizedSearchValue,
label: customLabel,
description: input.customValueDescription,
kind: input.customValueKind,
});
}
visibleOptions.push(...filteredOptions);
return visibleOptions;
}
export function orderVisibleComboboxOptions(
visibleOptions: ComboboxOptionModel[],
optionsPosition: "below-search" | "above-search"
): ComboboxOptionModel[] {
if (optionsPosition !== "above-search") {
return visibleOptions;
}
return [...visibleOptions].reverse();
}
export function getComboboxFallbackIndex(
itemCount: number,
optionsPosition: "below-search" | "above-search"
): number {
if (itemCount <= 0) {
return -1;
}
return optionsPosition === "above-search" ? itemCount - 1 : 0;
}