2024-02-05 14:32:50 +00:00
|
|
|
import {
|
|
|
|
Combobox,
|
|
|
|
ComboboxCancel,
|
|
|
|
ComboboxGroup,
|
|
|
|
ComboboxGroupLabel,
|
|
|
|
ComboboxItem,
|
|
|
|
ComboboxItemValue,
|
|
|
|
ComboboxPopover,
|
|
|
|
ComboboxProvider,
|
|
|
|
} from '@ariakit/react'
|
|
|
|
import { ComponentType, ForwardedRef, forwardRef, startTransition, useState } from 'react'
|
|
|
|
import './Autocomplete.css'
|
|
|
|
import { Icon } from './Icon'
|
2024-02-05 20:46:07 +00:00
|
|
|
import { Spinner } from './Spinner'
|
2024-02-05 14:32:50 +00:00
|
|
|
|
|
|
|
export type DropdownOption = {
|
|
|
|
label: string
|
|
|
|
value: string
|
|
|
|
group?: string
|
|
|
|
}
|
|
|
|
|
|
|
|
type AutocompleteProps = {
|
|
|
|
customUI?: React.ReactNode
|
|
|
|
groups?: string[]
|
|
|
|
groupsToIcon?: {
|
|
|
|
[key: string]: ComponentType<{
|
|
|
|
className?: string
|
|
|
|
}>
|
|
|
|
}
|
|
|
|
groupsToLabel?: { [key: string]: string }
|
|
|
|
isLoading: boolean
|
|
|
|
options: DropdownOption[]
|
|
|
|
onChange: (value: string) => void
|
|
|
|
onInputChange: (value: string) => void
|
|
|
|
}
|
|
|
|
|
|
|
|
const DEFAULT_GROUP = 'autocomplete-default'
|
|
|
|
|
|
|
|
const Autocomplete = forwardRef(function Autocomplete(
|
|
|
|
{
|
|
|
|
customUI,
|
|
|
|
groups = [DEFAULT_GROUP],
|
|
|
|
groupsToIcon,
|
|
|
|
groupsToLabel,
|
|
|
|
isLoading,
|
|
|
|
options,
|
|
|
|
onInputChange,
|
|
|
|
onChange,
|
|
|
|
}: AutocompleteProps,
|
|
|
|
ref: ForwardedRef<HTMLInputElement>
|
|
|
|
) {
|
|
|
|
const [open, setOpen] = useState(false)
|
|
|
|
const [value, setValue] = useState('')
|
|
|
|
|
|
|
|
const renderedGroups = groups.map((group) => {
|
|
|
|
const filteredOptions = options.filter(
|
|
|
|
({ group: optionGroup }) => optionGroup === group || group === DEFAULT_GROUP
|
|
|
|
)
|
|
|
|
if (filteredOptions.length === 0) return null
|
|
|
|
|
|
|
|
return (
|
2024-02-05 20:46:07 +00:00
|
|
|
<ComboboxGroup key={group}>
|
2024-02-05 14:32:50 +00:00
|
|
|
{groupsToLabel?.[group] && (
|
2024-02-05 20:46:07 +00:00
|
|
|
<ComboboxGroupLabel key={`${group}-group`} className="autocomplete__group">
|
|
|
|
{groupsToLabel[group]}
|
2024-02-05 14:32:50 +00:00
|
|
|
</ComboboxGroupLabel>
|
|
|
|
)}
|
|
|
|
{filteredOptions.map(({ label, value }) => {
|
|
|
|
const Icon = groupsToIcon?.[group]
|
|
|
|
return (
|
|
|
|
<ComboboxItem key={`${label}-${value}`} className="autocomplete__item" value={value}>
|
|
|
|
{Icon && <Icon className="autocomplete__item__icon" />}
|
|
|
|
<ComboboxItemValue value={label} />
|
|
|
|
</ComboboxItem>
|
|
|
|
)
|
|
|
|
})}
|
|
|
|
</ComboboxGroup>
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
return (
|
|
|
|
<ComboboxProvider<string>
|
|
|
|
defaultSelectedValue=""
|
|
|
|
open={open}
|
|
|
|
setOpen={setOpen}
|
|
|
|
resetValueOnHide
|
|
|
|
includesBaseElement={false}
|
|
|
|
setValue={(newValue) => {
|
|
|
|
startTransition(() => setValue(newValue))
|
|
|
|
onInputChange(newValue)
|
|
|
|
}}
|
|
|
|
setSelectedValue={(newValue) => onChange(newValue)}
|
|
|
|
>
|
|
|
|
<div className="autocomplete__wrapper">
|
|
|
|
{isLoading ? (
|
|
|
|
<Spinner className="autocomplete__icon" />
|
|
|
|
) : (
|
|
|
|
<Icon className="autocomplete__icon" icon="search" small />
|
|
|
|
)}
|
|
|
|
|
|
|
|
<Combobox
|
|
|
|
autoSelect
|
|
|
|
placeholder="Search…"
|
|
|
|
ref={ref}
|
|
|
|
className="autocomplete__input"
|
|
|
|
value={value}
|
|
|
|
/>
|
|
|
|
|
|
|
|
{value && <ComboboxCancel className="autocomplete__cancel" />}
|
|
|
|
|
|
|
|
{value && (
|
|
|
|
<ComboboxPopover className="autocomplete__popover">
|
|
|
|
{customUI}
|
2024-02-05 20:46:07 +00:00
|
|
|
{options.length === 0 && <span className="autocomplete__empty">No results found.</span>}
|
2024-02-05 14:32:50 +00:00
|
|
|
{options.length !== 0 && renderedGroups}
|
|
|
|
</ComboboxPopover>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</ComboboxProvider>
|
|
|
|
)
|
|
|
|
})
|
|
|
|
|
2024-02-05 20:46:07 +00:00
|
|
|
export { Autocomplete }
|