tooltip.tsx 2.7 KB

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