tooltip.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import {
  2. cloneElement,
  3. useEffect,
  4. forwardRef,
  5. useState,
  6. useMemo,
  7. useCallback,
  8. } from 'react'
  9. import {
  10. OverlayTrigger,
  11. OverlayTriggerProps,
  12. Tooltip as BSTooltip,
  13. TooltipProps as BSTooltipProps,
  14. } from 'react-bootstrap'
  15. import { callFnsInSequence } from '@/utils/functions'
  16. import { useTooltipContext } from '@/shared/context/tooltip-provider'
  17. const DEFAULT_DELAY_SHOW = 300
  18. // Slightly lower value avoids flickering when an adjacent tooltip is shown before the previous one hides
  19. const DEFAULT_DELAY_HIDE = 290
  20. type OverlayProps = Omit<OverlayTriggerProps, 'overlay' | 'children'>
  21. const UpdatingTooltip = forwardRef<HTMLDivElement, BSTooltipProps>(
  22. ({ popper, children, show: _, ...props }, ref) => {
  23. useEffect(() => {
  24. popper?.scheduleUpdate?.()
  25. }, [children, popper])
  26. return (
  27. <BSTooltip ref={ref} {...props}>
  28. {children}
  29. </BSTooltip>
  30. )
  31. }
  32. )
  33. UpdatingTooltip.displayName = 'UpdatingTooltip'
  34. const chooseDelayOptions = (
  35. delay?: number | { show: number; hide: number }
  36. ): { show: number; hide: number } => {
  37. if (typeof delay === 'object') {
  38. return delay
  39. }
  40. if (typeof delay === 'number') {
  41. return {
  42. show: delay,
  43. hide: Math.max(delay - 10, 0),
  44. }
  45. }
  46. return {
  47. show: DEFAULT_DELAY_SHOW,
  48. hide: DEFAULT_DELAY_HIDE,
  49. }
  50. }
  51. export type TooltipProps = {
  52. description: React.ReactNode
  53. id: string
  54. overlayProps?: OverlayProps
  55. tooltipProps?: BSTooltipProps
  56. hidden?: boolean
  57. children: React.ReactElement
  58. }
  59. function Tooltip({
  60. id,
  61. description,
  62. children,
  63. tooltipProps,
  64. overlayProps,
  65. hidden,
  66. }: TooltipProps) {
  67. const [show, setShow] = useState(false)
  68. const tooltipContext = useTooltipContext()
  69. useEffect(() => {
  70. const listener = (e: KeyboardEvent) => {
  71. if (show && e.key === 'Escape') {
  72. setShow(false)
  73. e.stopPropagation()
  74. }
  75. }
  76. document.addEventListener('keydown', listener, true)
  77. return () => {
  78. document.removeEventListener('keydown', listener, true)
  79. }
  80. }, [show])
  81. const hideTooltip = (e: React.MouseEvent) => {
  82. if (e.currentTarget instanceof HTMLElement) {
  83. e.currentTarget.blur()
  84. }
  85. setShow(false)
  86. }
  87. const delayProps = useMemo(() => {
  88. const delayOptions = chooseDelayOptions(overlayProps?.delay)
  89. if (tooltipContext?.isTooltipOpen) {
  90. delayOptions.show = 0
  91. delayOptions.hide = 0
  92. }
  93. return delayOptions
  94. }, [overlayProps?.delay, tooltipContext])
  95. const handleToggle = useCallback(
  96. (value: boolean) => {
  97. tooltipContext?.setIsTooltipOpen(value)
  98. setShow(value)
  99. },
  100. [tooltipContext]
  101. )
  102. return (
  103. <OverlayTrigger
  104. overlay={
  105. <UpdatingTooltip
  106. id={`${id}-tooltip`}
  107. {...tooltipProps}
  108. style={{ display: hidden ? 'none' : 'block' }}
  109. >
  110. {description}
  111. </UpdatingTooltip>
  112. }
  113. {...overlayProps}
  114. delay={delayProps}
  115. placement={overlayProps?.placement || 'top'}
  116. show={show}
  117. onToggle={handleToggle}
  118. transition={!tooltipContext?.isTooltipOpen}
  119. >
  120. {overlayProps?.trigger === 'click'
  121. ? children
  122. : cloneElement(children, {
  123. onClick: callFnsInSequence(children.props.onClick, hideTooltip),
  124. })}
  125. </OverlayTrigger>
  126. )
  127. }
  128. export default Tooltip