tag.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import {useCallback} from 'react';
  2. import type {Theme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {Button} from 'sentry/components/button';
  5. import ExternalLink from 'sentry/components/links/externalLink';
  6. import type {LinkProps} from 'sentry/components/links/link';
  7. import Link from 'sentry/components/links/link';
  8. import type {TooltipProps} from 'sentry/components/tooltip';
  9. import {Tooltip} from 'sentry/components/tooltip';
  10. import {IconClose, IconOpen} from 'sentry/icons';
  11. import type {SVGIconProps} from 'sentry/icons/svgIcon';
  12. import {IconDefaultsProvider} from 'sentry/icons/useIconDefaults';
  13. import {t} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import {trackAnalytics} from 'sentry/utils/analytics';
  16. import type {Color} from 'sentry/utils/theme';
  17. import theme from 'sentry/utils/theme';
  18. interface TagProps extends React.HTMLAttributes<HTMLSpanElement> {
  19. /**
  20. * Makes the tag clickable. Use for external links.
  21. * If no icon is passed, it defaults to IconOpen (can be removed by passing icon={null})
  22. */
  23. href?: string;
  24. /**
  25. * Icon on the left side.
  26. */
  27. icon?: React.ReactNode;
  28. /**
  29. * Triggered when the item is clicked
  30. */
  31. onClick?: (eventKey: any) => void;
  32. /**
  33. * Shows clickable IconClose on the right side.
  34. */
  35. onDismiss?: () => void;
  36. /**
  37. * Max width of the tag's text
  38. */
  39. textMaxWidth?: number;
  40. /**
  41. * Makes the tag clickable. Use for internal links handled by react router.
  42. * If no icon is passed, it defaults to IconOpen (can be removed by passing icon={null})
  43. */
  44. to?: LinkProps['to'];
  45. /**
  46. * Additional properites for the Tooltip when `tooltipText` is set.
  47. */
  48. tooltipProps?: Omit<TooltipProps, 'children' | 'title' | 'skipWrapper'>;
  49. /**
  50. * Text to show up on a hover.
  51. */
  52. tooltipText?: TooltipProps['title'];
  53. /**
  54. * Dictates color scheme of the tag.
  55. */
  56. type?: keyof Theme['tag'];
  57. }
  58. function BaseTag({
  59. type = 'default',
  60. icon,
  61. tooltipText,
  62. tooltipProps,
  63. to,
  64. onClick,
  65. href,
  66. onDismiss,
  67. children,
  68. textMaxWidth = 150,
  69. ...props
  70. }: TagProps) {
  71. const iconsProps: SVGIconProps = {
  72. size: 'xs',
  73. color: theme.tag[type].color as Color,
  74. };
  75. const isLink = href !== undefined || to !== undefined;
  76. // Links use the IconOpen by default
  77. const linkIcon = isLink ? <IconOpen /> : null;
  78. const tagIcon = icon || icon === null ? icon : linkIcon;
  79. const handleDismiss = useCallback<React.MouseEventHandler>(
  80. event => {
  81. event.preventDefault();
  82. onDismiss?.();
  83. },
  84. [onDismiss]
  85. );
  86. const trackClickEvent = useCallback(() => {
  87. trackAnalytics('tag.clicked', {
  88. is_clickable: onClick !== undefined || isLink,
  89. organization: null,
  90. });
  91. }, [isLink, onClick]);
  92. const tag = (
  93. <Tooltip title={tooltipText} containerDisplayMode="inline-flex" {...tooltipProps}>
  94. <Background type={type}>
  95. {tagIcon && (
  96. <IconWrapper>
  97. <IconDefaultsProvider {...iconsProps}>{tagIcon}</IconDefaultsProvider>
  98. </IconWrapper>
  99. )}
  100. <Text type={type} maxWidth={textMaxWidth}>
  101. {children}
  102. </Text>
  103. {onDismiss !== undefined && (
  104. <DismissButton
  105. onClick={handleDismiss}
  106. size="zero"
  107. priority="link"
  108. aria-label={t('Dismiss')}
  109. icon={<IconClose isCircled {...iconsProps} />}
  110. />
  111. )}
  112. </Background>
  113. </Tooltip>
  114. );
  115. const tagWithParent =
  116. href !== undefined ? (
  117. <ExternalLink href={href}>{tag}</ExternalLink>
  118. ) : to !== undefined ? (
  119. <Link to={to} onClick={onClick}>
  120. {tag}
  121. </Link>
  122. ) : (
  123. tag
  124. );
  125. return (
  126. <span {...props} onClick={trackClickEvent}>
  127. {tagWithParent}
  128. </span>
  129. );
  130. }
  131. const Tag = styled(BaseTag)`
  132. font-size: ${p => p.theme.fontSizeSmall};
  133. `;
  134. const TAG_HEIGHT = '20px';
  135. export const Background = styled('div')<{type: keyof Theme['tag']}>`
  136. display: inline-flex;
  137. align-items: center;
  138. height: ${TAG_HEIGHT};
  139. border-radius: ${TAG_HEIGHT};
  140. background-color: ${p => p.theme.tag[p.type].background};
  141. border: solid 1px ${p => p.theme.tag[p.type].border};
  142. padding: 0 ${space(1)};
  143. `;
  144. const IconWrapper = styled('span')`
  145. margin-right: ${space(0.5)};
  146. display: inline-flex;
  147. `;
  148. const Text = styled('span')<{maxWidth: number; type: keyof Theme['tag']}>`
  149. color: ${p => p.theme.tag[p.type].color};
  150. max-width: ${p => p.maxWidth}px;
  151. overflow: hidden;
  152. white-space: nowrap;
  153. text-overflow: ellipsis;
  154. line-height: ${TAG_HEIGHT};
  155. `;
  156. const DismissButton = styled(Button)`
  157. margin-left: ${space(0.5)};
  158. margin-right: -${space(0.5)};
  159. border: none;
  160. `;
  161. export {Tag, type TagProps};
  162. const DO_NOT_USE_TAG = Tag;
  163. export default DO_NOT_USE_TAG;