Advertisement
metalni

autocomplete

Sep 12th, 2023
952
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /* eslint-disable @typescript-eslint/no-unsafe-return */
  2. import {
  3.   type ChangeEvent,
  4.   type FC,
  5.   type ForwardRefExoticComponent,
  6.   Fragment,
  7.   type SVGProps,
  8.   useMemo,
  9.   useState,
  10. } from 'react'
  11. import { Combobox, Transition } from '@headlessui/react'
  12. import { ChevronDownIcon } from '@heroicons/react/24/outline'
  13. import { mergeClassNames } from '@utils'
  14. import { type ControllerRenderProps, type FieldValues } from 'react-hook-form'
  15. import isEmpty from 'lodash.isempty'
  16. import { useUpdate } from '@rounik/react-custom-hooks'
  17.  
  18. export type TOption = {
  19.   value: string
  20. }
  21.  
  22. export interface IAutoCompleteProps {
  23.   options: TOption[]
  24.   placeholder?: string
  25.   containerClassName?: string
  26.   label?: string
  27.   Icon?: ForwardRefExoticComponent<SVGProps<SVGSVGElement>>
  28.   isAsync?: boolean
  29.   onAsyncSearch?: (event: ChangeEvent<HTMLInputElement>) => void
  30.   asyncOnSelect?: (option: TOption) => void
  31.   asyncValue?: string
  32.   asyncSelected?: TOption
  33.   field?: ControllerRenderProps<FieldValues, string>
  34. }
  35.  
  36. // TODO: Refactor this component, it's too polluted
  37. export const AutoComplete: FC<IAutoCompleteProps> = ({
  38.   options,
  39.   placeholder = 'Select',
  40.   containerClassName = '',
  41.   label,
  42.   Icon,
  43.   isAsync = false,
  44.   onAsyncSearch,
  45.   asyncValue,
  46.   asyncOnSelect,
  47.   asyncSelected,
  48.   field,
  49. }) => {
  50.   const [selected, setSelected] = useState<TOption | null>(null)
  51.   const [query, setQuery] = useState('')
  52.   const [showPlaceholder, setShowPlaceholder] = useState(!selected?.value)
  53.  
  54.   useUpdate(() => {
  55.     // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  56.     if (isEmpty(selected?.value) && !isEmpty(field?.value?.value)) {
  57.       setShowPlaceholder(false)
  58.     }
  59.   }, [field?.value, selected])
  60.  
  61.   const onSearch = (event: ChangeEvent<HTMLInputElement>) => {
  62.     if (isEmpty(event.target.value)) field?.onChange({ value: '' })
  63.  
  64.     if (isAsync) {
  65.       onAsyncSearch && onAsyncSearch(event)
  66.     } else {
  67.       setQuery(event.target.value)
  68.     }
  69.   }
  70.   const filterQuery = isAsync ? asyncValue : query
  71.  
  72.   const filteredOptions = useMemo(() => {
  73.     return filterQuery === ''
  74.       ? options
  75.       : options.filter((option) =>
  76.           option.value
  77.             .toLowerCase()
  78.             .replace(/\s+/g, '')
  79.             .includes(query.toLowerCase().replace(/\s+/g, ''))
  80.         )
  81.   }, [query, options, filterQuery])
  82.  
  83.   const defaultContainerClassName = 'relative w-full'
  84.  
  85.   const handleOnChange = (value: TOption) => {
  86.     isAsync ? asyncOnSelect && asyncOnSelect(value) : setSelected(value)
  87.     field?.onChange(value)
  88.   }
  89.  
  90.   const handleOnInputBlur = () => {
  91.     if (!field?.value && !selected)
  92.       setShowPlaceholder(isAsync ? !asyncSelected?.value : !selected)
  93.   }
  94.  
  95.   return (
  96.     <div
  97.       className={mergeClassNames([
  98.         defaultContainerClassName,
  99.         containerClassName,
  100.       ])}
  101.     >
  102.       <Combobox
  103.         value={isAsync ? asyncSelected : selected}
  104.         onChange={handleOnChange}
  105.       >
  106.         <p className="absolute top-[-3px] left-2 z-10 rounded-lg bg-white px-1 text-[10px] text-[#919EAB] group-hover:bg-gray-50">
  107.           {label}
  108.         </p>
  109.         <div className="relative mt-1">
  110.           <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">
  111.             <Combobox.Input
  112.               onBlur={handleOnInputBlur}
  113.               onFocus={() => setShowPlaceholder(false)}
  114.               className={`w-full rounded-lg border-[1px] border-[#919EAB52] py-4 pl-3 pr-10 text-sm leading-5 ${
  115.                 showPlaceholder ? 'text-[#919EAB]' : ''
  116.               } focus:outline-0 focus:ring-0`}
  117.               displayValue={(option: TOption | null) =>
  118.                 showPlaceholder
  119.                   ? placeholder
  120.                   : // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  121.                     option?.value ?? field?.value?.value
  122.               }
  123.               onChange={onSearch}
  124.             />
  125.             <Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
  126.               {!!Icon ? (
  127.                 <Icon
  128.                   className="h-5 w-5 text-gray-400"
  129.                   width={24}
  130.                   height={24}
  131.                 />
  132.               ) : (
  133.                 <ChevronDownIcon
  134.                   className="h-5 w-5 text-gray-400"
  135.                   aria-hidden="true"
  136.                 />
  137.               )}
  138.             </Combobox.Button>
  139.           </div>
  140.           <Transition
  141.             as={Fragment}
  142.             leave="transition ease-in duration-100"
  143.             leaveFrom="opacity-100"
  144.             leaveTo="opacity-0"
  145.             afterLeave={() => setQuery('')}
  146.           >
  147.             <Combobox.Options className="absolute z-20 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
  148.               {filteredOptions.length === 0 && filterQuery !== '' ? (
  149.                 <div className="relative cursor-default select-none py-2 px-4 text-gray-700">
  150.                   Nothing found.
  151.                 </div>
  152.               ) : (
  153.                 filteredOptions.map((option, idx) => (
  154.                   <Combobox.Option
  155.                     key={idx}
  156.                     className={({ active }) =>
  157.                       `relative cursor-default select-none py-2 pl-4 pr-4 ${
  158.                         active ? 'bg-primaryTransparent-16' : ''
  159.                       }`
  160.                     }
  161.                     value={option}
  162.                   >
  163.                     {({ selected, active }) => (
  164.                       <>
  165.                         <span
  166.                           className={`block truncate ${
  167.                             selected ? 'font-medium' : 'font-normal'
  168.                           }`}
  169.                         >
  170.                           {option.value}
  171.                         </span>
  172.                         {selected ? (
  173.                           <span
  174.                             className={`absolute inset-y-0 left-0 flex items-center pl-3 ${
  175.                               active ? 'text-white' : 'text-teal-600'
  176.                             }`}
  177.                           ></span>
  178.                         ) : null}
  179.                       </>
  180.                     )}
  181.                   </Combobox.Option>
  182.                 ))
  183.               )}
  184.             </Combobox.Options>
  185.           </Transition>
  186.         </div>
  187.       </Combobox>
  188.     </div>
  189.   )
  190. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement