| 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> | |
| ); | |
| }; | |