All files / app/src/shared/ui/select select.tsx

82.6% Statements 76/92
55.55% Branches 5/9
40% Functions 2/5
82.6% Lines 76/92

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 1281x                                                 1x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x   2x 2x 2x 2x 2x 2x 2x 2x   2x 1x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x   1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x   1x 1x       1x 1x 1x 5x 5x                             5x 1x 1x       1x 1x 1x   2x   2x  
import { useState } from 'react'
import { Listbox, Transition } from '@headlessui/react'
import { Nullable, SelectOption } from '@/shared/@types'
import { useTranslate } from '@/shared/lib'
import { Input, InputProps } from '../input'
import { Button } from '../button'
import Loading from '@/shared/assets/icons/common/loading.svg'
import ArrowIcon from '@/shared/assets/icons/common/select-arrow.svg'
import cn from 'classnames'
 
export interface SelectProps<T> {
  name: string
  label?: string
  value?: Nullable<T>
  defaultValue?: T
  disabled?: boolean
  options?: SelectOption[]
  isSaved?: boolean
  inputProps?: Omit<InputProps, 'name'>
  className?: string
  optionClassName?: string
  isLoading?: boolean
  onChange?: (selected: Nullable<T>) => void
}
 
export const Select = <T extends string | number>({
  name,
  label,
  value = null,
  defaultValue,
  disabled,
  options,
  isSaved,
  inputProps,
  className,
  optionClassName,
  isLoading,
  onChange,
}: SelectProps<T>) => {
  const { t } = useTranslate(['common'])
  const [optionsListRef, setOptionsListRef] = useState<Nullable<HTMLDivElement>>(null)
  const isScrollable = optionsListRef && optionsListRef.scrollHeight > optionsListRef.clientHeight
 
  return (
    <Listbox
      as='div'
      defaultValue={defaultValue}
      className={cn('relative w-full outline-none', className)}
      value={value}
      disabled={disabled || isSaved}
      onChange={e => onChange?.(e)}
    >
      {({ open }) => (
        <>
          <Listbox.Button
            className={cn('w-full', {
              'cursor-text': isSaved,
            })}
          >
            <Input
              name={name}
              label={label}
              type='select'
              readOnly
              isSaved={isSaved}
              value={options?.find(option => option.id === value)?.label || ''}
              disabled={disabled}
              isDropdownOpen={open}
              extraContent={
                <Button variant='icon' disabled={disabled}>
                  <ArrowIcon className={cn('stroke-currentColor transition-transform', { 'rotate-180': open })} />
                </Button>
              }
              reset={() => onChange?.(null)}
              {...inputProps}
            />
          </Listbox.Button>
          <Transition
            show={open}
            enterFrom='scale-y-95 opacity-0'
            enterTo='scale-y-100 opacity-100'
            leaveFrom='scale-y-100 opacity-100'
            leaveTo='scale-y-95 opacity-0'
            className={cn(
              'absolute w-full bg-white border border-border p-small mt-1 rounded-xl z-10 overflow-hidden',
              {
                'pr-1': isScrollable,
              }
            )}
          >
            <div ref={setOptionsListRef} className='max-h-[210px] scrollbar-list overflow-auto'>
              {isLoading ? (
                <div className='px-5 py-[10.5px]'>
                  <Loading className='h-[29px] fill-main mx-auto animate-spin' />
                </div>
              ) : options?.length ? (
                <Listbox.Options static className={cn('flex flex-col gap-1 outline-none', { 'pr-1': isScrollable })}>
                  {options?.map(option => (
                    <Listbox.Option key={`${option.id}`} value={option.id} disabled={option.disabled}>
                      {({ active, selected, disabled }) => (
                        <h4
                          className={cn(
                            'px-5 py-[10.5px] cursor-pointer rounded-base',
                            {
                              'bg-background-tertiary': active || selected,
                              'text-black': !disabled,
                              'text-background-primary': disabled,
                            },
                            optionClassName
                          )}
                        >
                          {option.label}
                        </h4>
                      )}
                    </Listbox.Option>
                  ))}
                </Listbox.Options>
              ) : (
                <h4 className='px-5 py-[10.5px] text-text-secondary'>{t('EmptyRequest')}</h4>
              )}
            </div>
          </Transition>
        </>
      )}
    </Listbox>
  )
}