hovercard.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import {Fragment} from 'react';
  2. import {createPortal} from 'react-dom';
  3. import {useTheme} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import classNames from 'classnames';
  6. import {AnimatePresence} from 'framer-motion';
  7. import {Overlay, PositionWrapper} from 'sentry/components/overlay';
  8. import space from 'sentry/styles/space';
  9. import {ColorOrAlias} from 'sentry/utils/theme';
  10. import {useHoverOverlay, UseHoverOverlayProps} from 'sentry/utils/useHoverOverlay';
  11. interface HovercardProps extends Omit<UseHoverOverlayProps, 'isHoverable'> {
  12. /**
  13. * Classname to apply to the hovercard
  14. */
  15. children: React.ReactNode;
  16. /**
  17. * Element to display in the body
  18. */
  19. body?: React.ReactNode;
  20. /**
  21. * Classname to apply to body container
  22. */
  23. bodyClassName?: string;
  24. /**
  25. * Classname to apply to the hovercard container
  26. */
  27. containerClassName?: string;
  28. /**
  29. * Element to display in the header
  30. */
  31. header?: React.ReactNode;
  32. /**
  33. * Color of the arrow tip border
  34. */
  35. tipBorderColor?: ColorOrAlias;
  36. /**
  37. * Color of the arrow tip
  38. */
  39. tipColor?: ColorOrAlias;
  40. }
  41. function Hovercard({
  42. body,
  43. bodyClassName,
  44. children,
  45. className,
  46. containerClassName,
  47. header,
  48. offset = 12,
  49. displayTimeout = 100,
  50. tipBorderColor = 'translucentBorder',
  51. tipColor = 'backgroundElevated',
  52. ...hoverOverlayProps
  53. }: HovercardProps): React.ReactElement {
  54. const theme = useTheme();
  55. const {wrapTrigger, isOpen, overlayProps, placement, arrowData, arrowProps} =
  56. useHoverOverlay('hovercard', {
  57. offset,
  58. displayTimeout,
  59. isHoverable: true,
  60. className: containerClassName,
  61. ...hoverOverlayProps,
  62. });
  63. // Nothing to render if no header or body. Be consistent with wrapping the
  64. // children with the trigger in the case that the body / header is set while
  65. // the trigger is hovered.
  66. if (!body && !header) {
  67. return <Fragment>{wrapTrigger(children)}</Fragment>;
  68. }
  69. const hovercardContent = isOpen && (
  70. <PositionWrapper zIndex={theme.zIndex.hovercard} {...overlayProps}>
  71. <StyledHovercard
  72. animated
  73. arrowProps={{
  74. ...arrowProps,
  75. size: 20,
  76. background: tipColor,
  77. border: tipBorderColor,
  78. }}
  79. originPoint={arrowData}
  80. placement={placement}
  81. className={classNames('hovercard', className)}
  82. >
  83. {header ? <Header>{header}</Header> : null}
  84. {body ? <Body className={bodyClassName}>{body}</Body> : null}
  85. </StyledHovercard>
  86. </PositionWrapper>
  87. );
  88. return (
  89. <Fragment>
  90. {wrapTrigger(children)}
  91. {createPortal(<AnimatePresence>{hovercardContent}</AnimatePresence>, document.body)}
  92. </Fragment>
  93. );
  94. }
  95. const StyledHovercard = styled(Overlay)`
  96. width: 295px;
  97. line-height: 1.2;
  98. `;
  99. const Header = styled('div')`
  100. font-size: ${p => p.theme.fontSizeMedium};
  101. background: ${p => p.theme.backgroundSecondary};
  102. border-bottom: 1px solid ${p => p.theme.border};
  103. border-radius: ${p => p.theme.borderRadiusTop};
  104. font-weight: 600;
  105. word-wrap: break-word;
  106. padding: ${space(1.5)};
  107. `;
  108. const Body = styled('div')`
  109. padding: ${space(2)};
  110. min-height: 30px;
  111. `;
  112. export {Hovercard, Header, Body};