index.tsx 3.5 KB

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