boundTooltip.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import {useCallback, useRef} from 'react';
  2. import styled from '@emotion/styled';
  3. import {vec2} from 'gl-matrix';
  4. import space from 'sentry/styles/space';
  5. import {useFlamegraphTheme} from 'sentry/utils/profiling/flamegraph/useFlamegraphTheme';
  6. import {FlamegraphCanvas} from 'sentry/utils/profiling/flamegraphCanvas';
  7. import {FlamegraphView} from 'sentry/utils/profiling/flamegraphView';
  8. import {Rect} from 'sentry/utils/profiling/gl/utils';
  9. import theme from 'sentry/utils/theme';
  10. function computeBestTooltipPlacement(
  11. cursor: vec2,
  12. container: Rect,
  13. tooltip: DOMRect
  14. ): string {
  15. // This is because the cursor's origin is in the top left corner of the arrow, so we want
  16. // to offset it just enough so that the tooltip does not overlap with the arrow's tail.
  17. // When the tooltip placed to the left of the cursor, we do not have that issue and hence
  18. // no offset is applied.
  19. const OFFSET_PX = 6;
  20. let left = cursor[0] + OFFSET_PX;
  21. const top = cursor[1] + OFFSET_PX;
  22. if (cursor[0] > container.width / 2) {
  23. left = cursor[0] - tooltip.width; // No offset is applied here as tooltip is placed to the left
  24. }
  25. return `translate(${left || 0}px, ${top || 0}px)`;
  26. }
  27. interface BoundTooltipProps {
  28. bounds: Rect;
  29. cursor: vec2;
  30. flamegraphCanvas: FlamegraphCanvas;
  31. flamegraphView: FlamegraphView;
  32. children?: React.ReactNode;
  33. }
  34. function BoundTooltip({
  35. bounds,
  36. flamegraphCanvas,
  37. cursor,
  38. flamegraphView,
  39. children,
  40. }: BoundTooltipProps): React.ReactElement | null {
  41. const flamegraphTheme = useFlamegraphTheme();
  42. const physicalSpaceCursor = vec2.transformMat3(
  43. vec2.create(),
  44. cursor,
  45. flamegraphView.fromConfigView(flamegraphCanvas.physicalSpace)
  46. );
  47. const logicalSpaceCursor = vec2.transformMat3(
  48. vec2.create(),
  49. physicalSpaceCursor,
  50. flamegraphCanvas.physicalToLogicalSpace
  51. );
  52. const rafIdRef = useRef<number | undefined>();
  53. const onRef = useCallback(
  54. node => {
  55. if (node === null) {
  56. return;
  57. }
  58. if (rafIdRef.current) {
  59. window.cancelAnimationFrame(rafIdRef.current);
  60. rafIdRef.current = undefined;
  61. }
  62. rafIdRef.current = window.requestAnimationFrame(() => {
  63. node.style.transform = computeBestTooltipPlacement(
  64. logicalSpaceCursor,
  65. bounds,
  66. node.getBoundingClientRect()
  67. );
  68. });
  69. },
  70. [bounds, logicalSpaceCursor]
  71. );
  72. return (
  73. <Tooltip
  74. ref={onRef}
  75. style={{
  76. willChange: 'transform',
  77. fontSize: flamegraphTheme.SIZES.TOOLTIP_FONT_SIZE,
  78. fontFamily: flamegraphTheme.FONTS.FONT,
  79. zIndex: theme.zIndex.tooltip,
  80. maxWidth: bounds.width,
  81. }}
  82. >
  83. {children}
  84. </Tooltip>
  85. );
  86. }
  87. const Tooltip = styled('div')`
  88. background: ${p => p.theme.background};
  89. position: absolute;
  90. white-space: nowrap;
  91. text-overflow: ellipsis;
  92. overflow: hidden;
  93. pointer-events: none;
  94. user-select: none;
  95. border-radius: ${p => p.theme.borderRadius};
  96. padding: ${space(0.25)} ${space(1)};
  97. border: 1px solid ${p => p.theme.border};
  98. font-size: ${p => p.theme.fontSizeSmall};
  99. line-height: 24px;
  100. `;
  101. export {BoundTooltip};