tag.tsx 4.4 KB

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