mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-23 03:40:14 +01:00
106 lines
3.3 KiB
TypeScript
106 lines
3.3 KiB
TypeScript
|
|
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>
|
||
|
|
);
|
||
|
|
};
|