tag.tsx 3.9 KB

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