Last active
April 16, 2024 10:18
-
-
Save andreguerrerosilvera/1e8d5007ee21c806f0ce938a31c21a2a to your computer and use it in GitHub Desktop.
Creating a custom reusable Combobox using headless-ui for react
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Combobox } from '@headlessui/react'; | |
| interface InputAutocompleteProps<T> { | |
| multiple?: boolean; | |
| labelExtractor: (item: T) => string; | |
| handleChange: (query: string) => void; | |
| } | |
| export function InputAutocomplete<T>({ | |
| multiple, | |
| labelExtractor, | |
| handleChange, | |
| }: InputAutocompleteProps<T>) { | |
| function transformDisplayValue(items: T[] | T) { | |
| if (multiple) { | |
| return (items as T[]).map((item: T) => labelExtractor(item)).join(', '); | |
| } | |
| return labelExtractor(items as T); | |
| } | |
| return ( | |
| <Combobox.Input | |
| className="w-full border-none py-2 pl-3 pr-10 sm:text-sm leading-5 truncate text-slate-700 focus:ring-0" | |
| displayValue={(items) => transformDisplayValue(items as T[] | T)} | |
| onChange={(event) => handleChange(event.target.value)} | |
| /> | |
| ); | |
| } | |
| export default InputAutocomplete; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Fragment, useEffect, useState } from 'react'; | |
| import { Combobox, Transition } from '@headlessui/react'; | |
| import { BsCheck2 } from 'react-icons/bs'; | |
| import { AiOutlineCaretDown } from 'react-icons/ai'; | |
| import Wrapper from './selector-wrapper'; | |
| import InputAutocomplete from './input-autocomplete'; | |
| interface SelectAutocompleteProps<T> { | |
| items: T[]; | |
| noResultsMessage: string; | |
| multiple?: boolean; | |
| labelExtractor: (item: T) => string; | |
| renderItem: (item: T) => JSX.Element; | |
| filterItems: (query: string) => void; | |
| } | |
| export function SelectAutocomplete<T>({ | |
| items, | |
| noResultsMessage, | |
| multiple = false, | |
| renderItem, | |
| filterItems, | |
| labelExtractor, | |
| }: SelectAutocompleteProps<T>) { | |
| const [query, setQuery] = useState(''); | |
| useEffect(() => { | |
| filterItems(query); | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, [query]); | |
| return ( | |
| <Wrapper multiple={multiple}> | |
| <div className="relative mt-1"> | |
| <div className="relative w-full cursor-default overflow-hidden rounded-lg bg-white text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-teal-300 sm:text-sm border border-neutral-200 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"> | |
| <InputAutocomplete | |
| multiple={multiple} | |
| labelExtractor={labelExtractor} | |
| handleChange={setQuery} | |
| /> | |
| <Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2"> | |
| <AiOutlineCaretDown | |
| className="h-5 w-5 text-gray-400" | |
| aria-hidden="true" | |
| /> | |
| </Combobox.Button> | |
| </div> | |
| <Transition | |
| as={Fragment} | |
| leave="transition ease-in duration-100" | |
| leaveFrom="opacity-100" | |
| leaveTo="opacity-0" | |
| afterLeave={() => setQuery('')} | |
| > | |
| <Combobox.Options className="absolute mt-1 max-h-60 w-full overflow-auto rounded-lg bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"> | |
| {items.length === 0 && query !== '' ? ( | |
| <div className="relative cursor-default select-none py-2 px-4 text-slate-700"> | |
| {noResultsMessage} | |
| </div> | |
| ) : ( | |
| items.map((item) => ( | |
| <Combobox.Option | |
| key={labelExtractor(item)} | |
| className={({ active }) => | |
| `relative cursor-default select-none py-2 pl-10 pr-4 ${ | |
| active ? 'bg-indigo-500 text-white' : 'text-slate-700' | |
| }` | |
| } | |
| value={item} | |
| > | |
| {({ selected, active }) => ( | |
| <> | |
| {renderItem(item)} | |
| {selected ? ( | |
| <span | |
| className={`absolute inset-y-0 left-0 flex items-center pl-3 ${ | |
| active ? 'text-white' : 'text-indigo-800' | |
| }`} | |
| > | |
| <BsCheck2 className="h-5 w-5" aria-hidden="true" /> | |
| </span> | |
| ) : null} | |
| </> | |
| )} | |
| </Combobox.Option> | |
| )) | |
| )} | |
| </Combobox.Options> | |
| </Transition> | |
| </div> | |
| </Wrapper> | |
| ); | |
| } | |
| export default SelectAutocomplete; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { ReactNode, useState } from 'react'; | |
| import { Combobox } from '@headlessui/react'; | |
| interface SelectorWrapperProps { | |
| multiple: boolean; | |
| children: ReactNode; | |
| } | |
| export function SelectorWrapper<T>({ | |
| multiple, | |
| children, | |
| }: SelectorWrapperProps) { | |
| const [selectedItems, setItems] = useState<T[]>([]); | |
| const [selectedItem, setItem] = useState(''); | |
| function handleChangeItems(items: T[]) { | |
| setItems([...items]); | |
| } | |
| if (multiple) { | |
| return ( | |
| <Combobox value={selectedItems} onChange={handleChangeItems} multiple> | |
| {children} | |
| </Combobox> | |
| ); | |
| } | |
| return ( | |
| <Combobox value={selectedItem} onChange={setItem}> | |
| {children} | |
| </Combobox> | |
| ); | |
| } | |
| export default SelectorWrapper; |
Author
I'm very glad that this piece of code is useful to you in some way! This gave me a headache when I implemented it lol.
- Yes, it's a typo, I'll correct it right away.
- Oops, you're right, there's a mess with the z-index, I'll check it when I get home.
Thank you very much for the feedback * - *
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Great work! This is really helpful.
Two points:
In your SelectAutocomplete.tsx, you have 'OrSelectAutocompleteProps'. Is that a typo? Is it supposed to be 'SelectAutocompleteProps'.
There appears to be some z index issues:
Both are the same component

Once again, thanks for your contribution!