badge-link-with-tooltip.tsx 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. import { OverlayTrigger, Tooltip } from 'react-bootstrap'
  2. import type { MergeAndOverride } from '../../../../../types/utils'
  3. import BadgeLink, { type BadgeLinkProps } from './badge-link'
  4. import { useEffect, useRef, useState } from 'react'
  5. import classNames from 'classnames'
  6. type BadgeLinkWithTooltipProps = MergeAndOverride<
  7. BadgeLinkProps,
  8. {
  9. placement?: 'top' | 'right' | 'bottom' | 'left'
  10. tooltipTitle: string
  11. }
  12. >
  13. function getElementWidth(el: Element) {
  14. const elComputedStyle = window.getComputedStyle(el)
  15. const elPaddingX =
  16. parseFloat(elComputedStyle.paddingLeft) +
  17. parseFloat(elComputedStyle.paddingRight)
  18. const elBorderX =
  19. parseFloat(elComputedStyle.borderLeftWidth) +
  20. parseFloat(elComputedStyle.borderRightWidth)
  21. return el.scrollWidth - elPaddingX - elBorderX
  22. }
  23. function BadgeLinkWithTooltip({
  24. children,
  25. tooltipTitle,
  26. placement,
  27. ...rest
  28. }: BadgeLinkWithTooltipProps) {
  29. const badgeContentRef = useRef<HTMLElement>(null)
  30. const [showTooltip, setShowTooltip] = useState(true)
  31. const [noMaxWidth, setNoMaxWidth] = useState(false)
  32. const badgeLinkClasses = classNames({ 'badge-link-no-max-width': noMaxWidth })
  33. const renderTooltip = (props: any) => {
  34. if (showTooltip) {
  35. return (
  36. <Tooltip
  37. id={`badge-tooltip-${rest.href.replace(/\//g, '-')}`}
  38. {...props}
  39. >
  40. {tooltipTitle}
  41. </Tooltip>
  42. )
  43. } else {
  44. return <></>
  45. }
  46. }
  47. useEffect(() => {
  48. if (badgeContentRef.current) {
  49. // Check if tooltip needed.
  50. // If .badge-content does not extend beyond max-width limit on
  51. // .badge then tooltip is not needed. max-width is always
  52. // removed when withTooltip exists and tooltip is not needed
  53. // to avoid any differences in width calculation after font
  54. // loaded (for example, Noto sans). Othwerise, badge might get
  55. // clipped due to font loaded causing .badge-content to be
  56. // greater than .badge max-width and no tooltip was determined
  57. // to be needed with default font (for example, sans-serif)
  58. const badgeContentWidth = badgeContentRef.current.scrollWidth
  59. if (badgeContentRef.current?.parentElement) {
  60. const badgeWidth = getElementWidth(
  61. badgeContentRef.current?.parentElement
  62. )
  63. if (badgeContentWidth <= badgeWidth) {
  64. // no tooltip and remove max-width
  65. setNoMaxWidth(true)
  66. setShowTooltip(false)
  67. }
  68. }
  69. }
  70. }, [])
  71. return (
  72. <OverlayTrigger placement={placement || 'bottom'} overlay={renderTooltip}>
  73. <span>
  74. <BadgeLink
  75. {...rest}
  76. badgeContentRef={badgeContentRef}
  77. badgeLinkClasses={badgeLinkClasses}
  78. >
  79. {children}
  80. </BadgeLink>
  81. </span>
  82. </OverlayTrigger>
  83. )
  84. }
  85. export default BadgeLinkWithTooltip