LibreChat/client/src/components/ui/InputCombobox.tsx

106 lines
3.3 KiB
TypeScript
Raw Normal View History

import React from 'react';
import * as Ariakit from '@ariakit/react';
import type { OptionWithIcon } from '~/common';
import { cn } from '~/utils';
type ComboboxProps = {
label?: string;
placeholder?: string;
options: OptionWithIcon[] | string[];
className?: string;
labelClassName?: string;
value: string;
onChange: (value: string) => void;
onBlur: () => void;
};
export const InputCombobox: React.FC<ComboboxProps> = ({
label,
labelClassName,
placeholder = 'Select an option',
options,
className,
value,
onChange,
onBlur,
}) => {
const isOptionObject = (option: unknown): option is OptionWithIcon => {
return option != null && typeof option === 'object' && 'value' in option;
};
const [isOpen, setIsOpen] = React.useState(false);
const [inputValue, setInputValue] = React.useState(value);
const [isKeyboardFocus, setIsKeyboardFocus] = React.useState(false);
React.useEffect(() => {
setInputValue(value);
}, [value]);
const handleChange = (newValue: string) => {
setInputValue(newValue);
onChange(newValue);
};
return (
<Ariakit.ComboboxProvider value={inputValue} setValue={handleChange}>
{label != null && (
<Ariakit.ComboboxLabel
className={cn('mb-2 block text-sm font-medium text-text-primary', labelClassName ?? '')}
>
{label}
</Ariakit.ComboboxLabel>
)}
<div className={cn('relative', isKeyboardFocus ? 'rounded-md ring-2 ring-ring-primary' : '')}>
<Ariakit.Combobox
placeholder={placeholder}
className={cn(
'h-10 w-full rounded-md border border-border-light bg-surface-primary px-3 py-2 text-sm',
'placeholder-text-secondary hover:bg-surface-hover',
'focus:outline-none',
className,
)}
onChange={(event) => handleChange(event.target.value)}
onBlur={() => {
setIsKeyboardFocus(false);
onBlur();
}}
onFocusVisible={() => {
setIsKeyboardFocus(true);
setIsOpen(true);
}}
onMouseDown={() => {
setIsKeyboardFocus(false);
}}
/>
</div>
<Ariakit.ComboboxPopover
gutter={4}
sameWidth
open={isOpen}
onClose={() => setIsOpen(false)}
className={cn(
'z-50 max-h-60 w-full overflow-auto rounded-md bg-surface-primary p-1 shadow-lg',
'animate-in fade-in-0 zoom-in-95',
)}
>
{options.map((option: string | OptionWithIcon, index: number) => (
<Ariakit.ComboboxItem
key={index}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',
'cursor-pointer hover:bg-surface-tertiary hover:text-text-primary',
'data-[active-item]:bg-surface-tertiary data-[active-item]:text-text-primary',
)}
value={isOptionObject(option) ? `${option.value ?? ''}` : option}
>
{isOptionObject(option) && option.icon != null && (
<span className="mr-2 flex-shrink-0">{option.icon}</span>
)}
{isOptionObject(option) ? option.label : option}
</Ariakit.ComboboxItem>
))}
</Ariakit.ComboboxPopover>
</Ariakit.ComboboxProvider>
);
};