Skip to content

Instantly share code, notes, and snippets.

@andreguerrerosilvera
Last active April 16, 2024 10:18
Show Gist options
  • Select an option

  • Save andreguerrerosilvera/1e8d5007ee21c806f0ce938a31c21a2a to your computer and use it in GitHub Desktop.

Select an option

Save andreguerrerosilvera/1e8d5007ee21c806f0ce938a31c21a2a to your computer and use it in GitHub Desktop.
Creating a custom reusable Combobox using headless-ui for react
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;
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;
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;
@tuffstuff9
Copy link

Great work! This is really helpful.

Two points:

  1. In your SelectAutocomplete.tsx, you have 'OrSelectAutocompleteProps'. Is that a typo? Is it supposed to be 'SelectAutocompleteProps'.

  2. There appears to be some z index issues:

Both are the same component
image

image

Once again, thanks for your contribution!

@andreguerrerosilvera
Copy link
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.

  1. Yes, it's a typo, I'll correct it right away.
  2. 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