Skip to content

Instantly share code, notes, and snippets.

@mxdi9i7
Created September 27, 2021 17:31
Show Gist options
  • Save mxdi9i7/bf8652a6666232c658a90cc832dc8c01 to your computer and use it in GitHub Desktop.
Save mxdi9i7/bf8652a6666232c658a90cc832dc8c01 to your computer and use it in GitHub Desktop.
Menu Multiselect for Headless+TailwindUI
import { IDishVariant, IModifierGroup } from '../../types';
import React, { useState, useRef, useEffect, forwardRef } from 'react';
import { Listbox, Transition } from '@headlessui/react';
import { CheckIcon, SelectorIcon } from '@heroicons/react/solid';
import classNames from 'classnames';
interface Props {
selectedList: any[];
data: any[];
label: string;
onSelect: (value: Props['selectedList']) => void;
isRequired?: boolean;
}
const MenuMultiSelect: React.FC<Props> = ({
selectedList,
data,
label,
onSelect,
isRequired,
}) => {
const node = useRef();
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
document.addEventListener('mousedown', handleClick);
return () => {
document.removeEventListener('mousedown', handleClick);
};
}, []);
function isSelected(value: string) {
return !!selectedList.find((el) => el === value);
}
function handleSelect(value: string) {
if (!isSelected(value)) {
const selectedUpdated = [...selectedList];
const found = data.find((el) => el._id === value)?._id;
if (found) {
selectedUpdated.push(found);
}
onSelect(selectedUpdated);
} else {
handleDeselect(value);
}
setIsOpen(true);
}
function handleDeselect(value: string) {
const selectedUpdated = selectedList.filter((el) => el !== value);
onSelect(selectedUpdated);
setIsOpen(true);
}
const handleClick = (e: MouseEvent) => {
if (node.current?.contains(e.target)) {
return;
}
setIsOpen(false);
};
const Btn = forwardRef((_props, ref) => {
return (
<button
className={`relative w-full bg-white border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-pointer focus:outline-none focus:ring-1 focus:ring-yellow-500 focus:border-yellow-500 sm:text-sm ${
isOpen ? 'border-red-500 ring-red-500 ring-1' : 'border-grey-300'
}`}
onClick={() => setIsOpen(!isOpen)}
ref={ref}
>
<span className='block truncate'>
{selectedList.length < 1
? 'Nothing is selected'
: `Selected ${selectedList.length} options`}
</span>
<span className='absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none'>
<svg
className='h-5 w-5 text-grey-400'
viewBox='0 0 20 20'
fill='none'
stroke='currentColor'
>
<path
d='M7 7l3-3 3 3m0 6l-3 3-3-3'
strokeWidth='1.5'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
</span>
</button>
);
});
return (
<div className='w-full mx-auto' ref={node}>
<Listbox
as='div'
value={selectedList}
onChange={(value: any) => {
handleSelect(value._id);
}}
open={isOpen}
>
{() => (
<>
<Listbox.Label className='block flex justify-between text-left text-sm text-gray-700'>
<span className='font-semibold'>{label}</span>
<span
className={classNames('text-sm text-gray-500', {
['text-yellow-500']: isRequired,
})}
>
{isRequired ? 'Required' : 'Optional'}
</span>
</Listbox.Label>
<div className='relative mt-1'>
<span className='inline-block w-full rounded-md shadow-sm'>
<Listbox.Button open={isOpen} as={Btn} />
</span>
<Transition
show={isOpen}
leave='transition ease-in duration-100'
leaveFrom='opacity-100'
leaveTo='opacity-0'
>
<Listbox.Options
static
className='absolute z-10 mt-1 w-full bg-white shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm'
>
{data.map(
(
v: IDishVariant | IModifierGroup,
i: React.Key | null | undefined,
) => {
const selected = isSelected(v._id || '');
return (
<Listbox.Option
key={i}
value={v}
className={({ active }) =>
classNames(
active
? 'text-white bg-yellow-600'
: 'text-gray-900',
'cursor-pointer select-none relative py-2 pl-3 pr-9',
)
}
>
{({ active }) => (
<>
<div className='flex items-center'>
<span
className={classNames(
selected ? 'font-semibold' : 'font-normal',
'block truncate',
)}
>
{v.title}
</span>
</div>
{selected ? (
<span
className={classNames(
active ? 'text-white' : 'text-yellow-600',
'absolute inset-y-0 right-0 flex items-center pr-4',
)}
>
<CheckIcon
className='h-5 w-5'
aria-hidden='true'
/>
</span>
) : null}
</>
)}
</Listbox.Option>
);
},
)}
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
</div>
);
};
export default MenuMultiSelect;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment