hovercard.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  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 {AnimatePresence} from 'framer-motion';
  6. import {Overlay, PositionWrapper} from 'sentry/components/overlay';
  7. import space from 'sentry/styles/space';
  8. import {ColorOrAlias} from 'sentry/utils/theme';
  9. import {useHoverOverlay, UseHoverOverlayProps} from 'sentry/utils/useHoverOverlay';
  10. interface HovercardProps extends Omit<UseHoverOverlayProps, 'isHoverable'> {
  11. /**
  12. * Classname to apply to the hovercard
  13. */
  14. children: React.ReactNode;
  15. /**
  16. * Element to display in the body
  17. */
  18. body?: React.ReactNode;
  19. /**
  20. * Classname to apply to body container
  21. */
  22. bodyClassName?: string;
  23. /**
  24. * Classname to apply to the hovercard container
  25. */
  26. containerClassName?: string;
  27. /**
  28. * Element to display in the header
  29. */
  30. header?: React.ReactNode;
  31. /**
  32. * Color of the arrow tip border
  33. */
  34. tipBorderColor?: ColorOrAlias;
  35. /**
  36. * Color of the arrow tip
  37. */
  38. tipColor?: ColorOrAlias;
  39. }
  40. function Hovercard({
  41. body,
  42. bodyClassName,
  43. children,
  44. className,
  45. containerClassName,
  46. header,
  47. offset = 12,
  48. displayTimeout = 100,
  49. tipBorderColor = 'translucentBorder',
  50. tipColor = 'backgroundElevated',
  51. ...hoverOverlayProps
  52. }: HovercardProps): React.ReactElement {
  53. const theme = useTheme();
  54. const {wrapTrigger, isOpen, overlayProps, placement, arrowData, arrowProps} =
  55. useHoverOverlay('hovercard', {
  56. offset,
  57. displayTimeout,
  58. isHoverable: true,
  59. className: containerClassName,
  60. ...hoverOverlayProps,
  61. });
  62. // Nothing to render if no header or body. Be consistent with wrapping the
  63. // children with the trigger in the case that the body / header is set while
  64. // the trigger is hovered.
  65. if (!body && !header) {
  66. return <Fragment>{wrapTrigger(children)}</Fragment>;
  67. }
  68. const hovercardContent = isOpen && (
  69. <PositionWrapper zIndex={theme.zIndex.hovercard} {...overlayProps}>
  70. <StyledHovercard
  71. animated
  72. arrowProps={{
  73. ...arrowProps,
  74. size: 20,
  75. background: tipColor,
  76. border: tipBorderColor,
  77. }}
  78. originPoint={arrowData}
  79. placement={placement}
  80. className={className}
  81. >
  82. {header ? <Header>{header}</Header> : null}
  83. {body ? <Body className={bodyClassName}>{body}</Body> : null}
  84. </StyledHovercard>
  85. </PositionWrapper>
  86. );
  87. return (
  88. <Fragment>
  89. {wrapTrigger(children)}
  90. {createPortal(<AnimatePresence>{hovercardContent}</AnimatePresence>, document.body)}
  91. </Fragment>
  92. );
  93. }
  94. const StyledHovercard = styled(Overlay)`
  95. width: 295px;
  96. line-height: 1.2;
  97. h6 {
  98. color: ${p => p.theme.subText};
  99. font-size: ${p => p.theme.fontSizeExtraSmall};
  100. margin-bottom: ${space(1)};
  101. text-transform: uppercase;
  102. }
  103. `;
  104. const Header = styled('div')`
  105. font-size: ${p => p.theme.fontSizeMedium};
  106. background: ${p => p.theme.backgroundSecondary};
  107. border-bottom: 1px solid ${p => p.theme.border};
  108. border-radius: ${p => p.theme.borderRadiusTop};
  109. font-weight: 600;
  110. word-wrap: break-word;
  111. padding: ${space(1.5)};
  112. `;
  113. const Body = styled('div')`
  114. padding: ${space(2)};
  115. min-height: 30px;
  116. `;
  117. const Divider = styled('div')`
  118. position: relative;
  119. margin-top: ${space(1.5)};
  120. margin-bottom: ${space(1)};
  121. &:before {
  122. display: block;
  123. position: absolute;
  124. content: '';
  125. height: 1px;
  126. top: 50%;
  127. left: ${space(2)};
  128. right: ${space(2)};
  129. background: ${p => p.theme.innerBorder};
  130. z-index: -1;
  131. }
  132. h6 {
  133. display: inline;
  134. padding-right: ${space(1)};
  135. background: ${p => p.theme.background};
  136. }
  137. `;
  138. export {Hovercard, Header, Body, Divider};