overlayArrow.tsx 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import {forwardRef, useMemo} from 'react';
  2. import {PopperProps} from 'react-popper';
  3. import styled from '@emotion/styled';
  4. import domId from 'sentry/utils/domId';
  5. import {ColorOrAlias} from 'sentry/utils/theme';
  6. type Props = React.HTMLAttributes<HTMLDivElement> & {
  7. background?: ColorOrAlias;
  8. border?: ColorOrAlias;
  9. placement?: PopperProps<any>['placement'];
  10. size?: number;
  11. strokeWidth?: number;
  12. };
  13. const BaseOverlayArrow: React.ForwardRefRenderFunction<HTMLDivElement, Props> = (
  14. {
  15. size = 16,
  16. strokeWidth = 1,
  17. placement,
  18. background = 'backgroundElevated',
  19. border = 'translucentBorder',
  20. ...props
  21. },
  22. ref
  23. ) => {
  24. /**
  25. * SVG height
  26. */
  27. const h = Math.round(size * 0.4);
  28. /**
  29. * SVG width
  30. */
  31. const w = size;
  32. /**
  33. * SVG stroke width
  34. */
  35. const s = strokeWidth;
  36. const arrowPath = [
  37. `M 0 ${h - s / 2}`,
  38. `C ${w * 0.25} ${h - s / 2} ${w * 0.45} ${s / 2} ${w / 2} ${s / 2}`,
  39. `C ${w * 0.55} ${s / 2} ${w * 0.75} ${h - s / 2} ${w} ${h - s / 2}`,
  40. ].join('');
  41. const strokeMaskId = useMemo(() => domId('stroke-mask'), []);
  42. const fillMaskId = useMemo(() => domId('fill-mask'), []);
  43. return (
  44. <Wrap ref={ref} placement={placement} size={size} {...props}>
  45. <SVG
  46. overflow="visible"
  47. width={w}
  48. height={h}
  49. viewBox={`0 0 ${w} ${h}`}
  50. background={background}
  51. border={border}
  52. >
  53. <defs>
  54. <mask id={strokeMaskId}>
  55. <rect x="0" y={-strokeWidth} width="100%" height="100%" fill="white" />
  56. </mask>
  57. <mask id={fillMaskId}>
  58. <rect x="0" y="0" width="100%" height="100%" fill="white" />
  59. <path d={arrowPath} vectorEffect="non-scaling-stroke" stroke="black" />
  60. </mask>
  61. </defs>
  62. <path
  63. d={`${arrowPath} V ${h} H 0 Z`}
  64. mask={`url(#${fillMaskId})`}
  65. className="fill"
  66. />
  67. <path d={arrowPath} mask={`url(#${strokeMaskId})`} className="stroke" />
  68. </SVG>
  69. </Wrap>
  70. );
  71. };
  72. const OverlayArrow = forwardRef(BaseOverlayArrow);
  73. export default OverlayArrow;
  74. const Wrap = styled('div')<{size: number; placement?: PopperProps<any>['placement']}>`
  75. position: relative;
  76. display: flex;
  77. width: ${p => p.size}px;
  78. height: ${p => p.size}px;
  79. ${p =>
  80. p.placement?.startsWith('top') &&
  81. `bottom: 0; transform: translateY(50%) rotate(180deg);`}
  82. ${p => p.placement?.startsWith('bottom') && `top: 0; transform: translateY(-50%) ;`}
  83. ${p =>
  84. p.placement?.startsWith('left') &&
  85. `right: 0; transform: translateX(50%) rotate(90deg);`}
  86. ${p =>
  87. p.placement?.startsWith('right') &&
  88. `left: 0; transform: translateX(-50%) rotate(-90deg);`}
  89. `;
  90. const SVG = styled('svg')<{background: ColorOrAlias; border: ColorOrAlias}>`
  91. position: absolute;
  92. bottom: 50%;
  93. fill: none;
  94. stroke: none;
  95. path.stroke {
  96. stroke: ${p => p.theme[p.border]};
  97. }
  98. path.fill {
  99. fill: ${p => p.theme[p.background]};
  100. }
  101. `;