index.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import { Popover, Transition } from '@headlessui/react'
  2. import { Fragment, cloneElement, useRef } from 'react'
  3. import s from './style.module.css'
  4. import cn from '@/utils/classnames'
  5. export type HtmlContentProps = {
  6. onClose?: () => void
  7. onClick?: () => void
  8. }
  9. type IPopover = {
  10. className?: string
  11. htmlContent: React.ReactElement<HtmlContentProps>
  12. popupClassName?: string
  13. trigger?: 'click' | 'hover'
  14. position?: 'bottom' | 'br' | 'bl'
  15. btnElement?: string | React.ReactNode
  16. btnClassName?: string | ((open: boolean) => string)
  17. manualClose?: boolean
  18. disabled?: boolean
  19. }
  20. const timeoutDuration = 100
  21. export default function CustomPopover({
  22. trigger = 'hover',
  23. position = 'bottom',
  24. htmlContent,
  25. popupClassName,
  26. btnElement,
  27. className,
  28. btnClassName,
  29. manualClose,
  30. disabled = false,
  31. }: IPopover) {
  32. const buttonRef = useRef<HTMLButtonElement>(null)
  33. const timeOutRef = useRef<NodeJS.Timeout | null>(null)
  34. const onMouseEnter = (isOpen: boolean) => {
  35. timeOutRef.current && clearTimeout(timeOutRef.current)
  36. !isOpen && buttonRef.current?.click()
  37. }
  38. const onMouseLeave = (isOpen: boolean) => {
  39. timeOutRef.current = setTimeout(() => {
  40. isOpen && buttonRef.current?.click()
  41. }, timeoutDuration)
  42. }
  43. return (
  44. <Popover className="relative">
  45. {({ open }: { open: boolean }) => {
  46. return (
  47. <>
  48. <div
  49. {...(trigger !== 'hover'
  50. ? {}
  51. : {
  52. onMouseLeave: () => onMouseLeave(open),
  53. onMouseEnter: () => onMouseEnter(open),
  54. })}
  55. >
  56. <Popover.Button
  57. ref={buttonRef}
  58. disabled={disabled}
  59. className={`group ${s.popupBtn} ${open ? '' : 'bg-gray-100'} ${!btnClassName
  60. ? ''
  61. : typeof btnClassName === 'string'
  62. ? btnClassName
  63. : btnClassName?.(open)
  64. }`}
  65. >
  66. {btnElement}
  67. </Popover.Button>
  68. <Transition as={Fragment}>
  69. <Popover.Panel
  70. className={cn(
  71. s.popupPanel,
  72. position === 'bottom' && '-translate-x-1/2 left-1/2',
  73. position === 'bl' && 'left-0',
  74. position === 'br' && 'right-0',
  75. className,
  76. )}
  77. {...(trigger !== 'hover'
  78. ? {}
  79. : {
  80. onMouseLeave: () => onMouseLeave(open),
  81. onMouseEnter: () => onMouseEnter(open),
  82. })
  83. }
  84. >
  85. {({ close }) => (
  86. <div
  87. className={cn(s.panelContainer, popupClassName)}
  88. {...(trigger !== 'hover'
  89. ? {}
  90. : {
  91. onMouseLeave: () => onMouseLeave(open),
  92. onMouseEnter: () => onMouseEnter(open),
  93. })
  94. }
  95. >
  96. {cloneElement(htmlContent as React.ReactElement<HtmlContentProps>, {
  97. onClose: () => onMouseLeave(open),
  98. ...(manualClose
  99. ? {
  100. onClick: close,
  101. }
  102. : {}),
  103. })}
  104. </div>
  105. )}
  106. </Popover.Panel>
  107. </Transition>
  108. </div>
  109. </>
  110. )
  111. }}
  112. </Popover>
  113. )
  114. }