frutjam
Version:
A utility-first CSS UI Library for Tailwind CSS
82 lines (74 loc) • 2.44 kB
JavaScript
import { useState, useCallback, useId } from 'react'
export function useCombobox({ items = [], filterFn } = {}) {
const uid = useId()
const listboxId = `${uid}-listbox`
const [open, setOpen] = useState(false)
const [query, setQuery] = useState('')
const [activeIndex, setActiveIndex] = useState(-1)
const [value, setValue] = useState('')
const filtered = filterFn
? filterFn(items, query)
: items.filter((item) =>
String(item.label ?? item).toLowerCase().includes(query.toLowerCase())
)
const activeDescendant = activeIndex >= 0 ? `${uid}-option-${activeIndex}` : undefined
const select = useCallback((item) => {
const label = item.label ?? String(item)
setValue(label)
setQuery(label)
setOpen(false)
setActiveIndex(-1)
}, [])
const handleKeyDown = useCallback((e) => {
if (e.key === 'ArrowDown') {
e.preventDefault()
if (!open) { setOpen(true); return }
setActiveIndex((i) => Math.min(i + 1, filtered.length - 1))
} else if (e.key === 'ArrowUp') {
e.preventDefault()
setActiveIndex((i) => Math.max(i - 1, 0))
} else if (e.key === 'Enter' && activeIndex >= 0) {
e.preventDefault()
select(filtered[activeIndex])
} else if (e.key === 'Escape') {
setOpen(false)
setActiveIndex(-1)
}
}, [open, filtered, activeIndex, select])
return {
open,
value,
query,
filtered,
activeIndex,
select,
containerProps: {
className: open ? 'combobox combobox-open' : 'combobox',
},
inputProps: {
value: query,
onChange: (e) => { setQuery(e.target.value); setOpen(true); setActiveIndex(-1) },
onKeyDown: handleKeyDown,
onFocus: () => setOpen(true),
onBlur: () => setTimeout(() => setOpen(false), 150),
role: 'combobox',
'aria-expanded': open,
'aria-autocomplete': 'list',
'aria-controls': listboxId,
'aria-activedescendant': activeDescendant,
},
listboxProps: {
id: listboxId,
role: 'listbox',
},
optionProps: (item, index) => ({
id: `${uid}-option-${index}`,
className: activeIndex === index ? 'combobox-option combobox-option-active' : 'combobox-option',
role: 'option',
'aria-selected': activeIndex === index,
onMouseDown: (e) => e.preventDefault(),
onClick: () => select(item),
onMouseEnter: () => setActiveIndex(index),
}),
}
}