All files / app/src/shared/ui/form-block-wrapper form-block-wrapper.tsx

95.16% Statements 59/62
66.66% Branches 20/30
66.66% Functions 2/3
95.16% Lines 59/62

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 971x                                           1x 96x 96x 96x 96x 96x 96x 96x 96x 96x 96x 96x 96x 96x   96x   96x 96x 96x   96x   96x 96x   96x 66x     96x   96x 2x 2x 2x 2x 2x   2x 2x     96x 96x 96x 9x 9x 9x 9x   96x 96x 96x 96x 66x     96x 96x 96x 96x 96x 96x 96x 96x 96x   96x 96x 96x 96x   96x  
import { FCWithChildren } from '@/shared/@types'
import { ReactElement, useEffect, useRef, useState } from 'react'
import { useTranslate } from '@/shared/lib'
import { motion } from 'framer-motion'
import { useFormErrors } from '@/shared/hooks'
import { useFormContext } from 'react-hook-form'
import cn from 'classnames'
import { Button } from '@tmk/ui-kit'
 
export interface FormBlockWrapperProps {
  title?: ReactElement
  defaultOpen?: boolean
  fields?: string[]
  childrenClassName?: string
  getDisclosureButtonText?: (isOpen: boolean) => string
  position?: Position
  buttonClassName?: string
  withMarginTop?: boolean
}
 
type Position = 'top' | 'bottom'
 
export const FormBlockWrapper: FCWithChildren<FormBlockWrapperProps> = ({
  title,
  className,
  children,
  defaultOpen = true,
  fields,
  childrenClassName,
  getDisclosureButtonText,
  position = 'top',
  buttonClassName,
  withMarginTop = true,
  ...rest
}) => {
  const { t } = useTranslate(['common'])
 
  const ref = useRef<HTMLDivElement>(null)
 
  const { formState } = useFormContext() || { formState: {} }
  const { errors } = formState
  const { fieldsHasError } = useFormErrors(errors) || {}
 
  const [open, setOpen] = useState(defaultOpen)
 
  const isNeedMarginTop = withMarginTop && (title || fields) && children
  const scrollHeight = ref.current?.scrollHeight
 
  useEffect(() => {
    if (fieldsHasError?.(fields)) {
      setOpen(true)
    }
  }, [fields, fieldsHasError])
 
  const DisclosureButton = () => (
    <Button
      className={cn('flex-shrink-0', buttonClassName)}
      variant='text'
      color='secondary'
      onClick={() => setOpen(prev => !prev)}
    >
      <h3>{getDisclosureButtonText?.(open) || t(open ? 'Rollup' : 'Open')}</h3>
    </Button>
  )
 
  return (
    <div className={cn('bg-gray-tertiary w-full p-4 flex flex-col rounded-base', className)} {...rest}>
      {(title || fields) && (
        <div className={'flex items-center justify-between'}>
          {title && title}
          {fields && position === 'top' && <DisclosureButton />}
        </div>
      )}
      <motion.div
        ref={ref}
        initial={
          defaultOpen
            ? { height: scrollHeight, marginTop: isNeedMarginTop ? '20px' : 0 }
            : { height: 0, overflow: 'hidden', marginTop: 0 }
        }
        animate={{
          height: open ? scrollHeight : 0,
          ...(!open && { overflow: 'hidden' }),
          marginTop: open && isNeedMarginTop ? '20px' : 0,
          transitionEnd: open ? { overflow: 'visible', height: 'max-content' } : { overflow: 'hidden', height: 0 },
        }}
        exit={{ overflow: 'visible' }}
        transition={{ type: 'just' }}
        className={childrenClassName}
      >
        {children}
      </motion.div>
      {fields && position === 'bottom' && <DisclosureButton />}
    </div>
  )
}