platformList.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import {css, Theme} from '@emotion/react';
  2. import styled from '@emotion/styled';
  3. import {PlatformIcon} from 'platformicons';
  4. import {Tooltip} from 'sentry/components/tooltip';
  5. import {tn} from 'sentry/locale';
  6. import type {PlatformKey} from 'sentry/types';
  7. import getPlatformName from 'sentry/utils/getPlatformName';
  8. type Props = {
  9. className?: string;
  10. /**
  11. * Will set container width to be size of having `this.props.max` icons
  12. * This is good for lists where the project name is displayed
  13. */
  14. consistentWidth?: boolean;
  15. direction?: 'right' | 'left';
  16. /**
  17. * Maximum number of platform icons to display
  18. */
  19. max?: number;
  20. platforms?: PlatformKey[];
  21. /**
  22. * If true and if the number of children is greater than the max prop,
  23. * a counter will be displayed at the end of the stack
  24. */
  25. showCounter?: boolean;
  26. /**
  27. * Platform icon size in pixels
  28. */
  29. size?: number;
  30. };
  31. type WrapperProps = Required<
  32. Pick<Props, 'showCounter' | 'size' | 'direction' | 'consistentWidth' | 'max'>
  33. >;
  34. function PlatformList({
  35. platforms = [],
  36. direction = 'right',
  37. max = 3,
  38. size = 16,
  39. consistentWidth = false,
  40. showCounter = false,
  41. className,
  42. }: Props) {
  43. const visiblePlatforms = platforms.slice(0, max);
  44. const numNotVisiblePlatforms = platforms.length - visiblePlatforms.length;
  45. const displayCounter = showCounter && !!numNotVisiblePlatforms;
  46. function renderContent() {
  47. if (!platforms.length) {
  48. return <StyledPlatformIcon size={size} platform="default" />;
  49. }
  50. const platformIcons = visiblePlatforms.slice().reverse();
  51. if (displayCounter) {
  52. return (
  53. <InnerWrapper>
  54. <PlatformIcons>
  55. {platformIcons.map((visiblePlatform, index) => (
  56. <Tooltip
  57. key={visiblePlatform + index}
  58. title={getPlatformName(visiblePlatform)}
  59. containerDisplayMode="inline-flex"
  60. >
  61. <StyledPlatformIcon platform={visiblePlatform} size={size} />
  62. </Tooltip>
  63. ))}
  64. </PlatformIcons>
  65. <Tooltip
  66. title={tn('%s other platform', '%s other platforms', numNotVisiblePlatforms)}
  67. containerDisplayMode="inline-flex"
  68. >
  69. <Counter>
  70. {numNotVisiblePlatforms}
  71. <Plus>{'\u002B'}</Plus>
  72. </Counter>
  73. </Tooltip>
  74. </InnerWrapper>
  75. );
  76. }
  77. return (
  78. <PlatformIcons>
  79. {platformIcons.map((visiblePlatform, index) => (
  80. <StyledPlatformIcon
  81. data-test-id={`platform-icon-${visiblePlatform}`}
  82. key={visiblePlatform + index}
  83. platform={visiblePlatform}
  84. size={size}
  85. />
  86. ))}
  87. </PlatformIcons>
  88. );
  89. }
  90. return (
  91. <Wrapper
  92. consistentWidth={consistentWidth}
  93. className={className}
  94. size={size}
  95. showCounter={displayCounter}
  96. direction={direction}
  97. max={max}
  98. >
  99. {renderContent()}
  100. </Wrapper>
  101. );
  102. }
  103. export default PlatformList;
  104. function getOverlapWidth(size: number) {
  105. return Math.round(size / 4);
  106. }
  107. const commonStyles = ({theme}: {theme: Theme}) => `
  108. cursor: default;
  109. border-radius: ${theme.borderRadius};
  110. box-shadow: 0 0 0 1px ${theme.background};
  111. :hover {
  112. z-index: 1;
  113. }
  114. `;
  115. const PlatformIcons = styled('div')`
  116. display: flex;
  117. `;
  118. const InnerWrapper = styled('div')`
  119. display: flex;
  120. position: relative;
  121. `;
  122. const Plus = styled('span')`
  123. font-size: 10px;
  124. `;
  125. const StyledPlatformIcon = styled(PlatformIcon)`
  126. ${p => commonStyles(p)};
  127. `;
  128. const Counter = styled('div')`
  129. ${p => commonStyles(p)};
  130. display: flex;
  131. align-items: center;
  132. justify-content: center;
  133. text-align: center;
  134. font-weight: 600;
  135. font-size: ${p => p.theme.fontSizeExtraSmall};
  136. background-color: ${p => p.theme.gray200};
  137. color: ${p => p.theme.gray300};
  138. padding: 0 1px;
  139. position: absolute;
  140. right: -1px;
  141. `;
  142. const Wrapper = styled('div')<WrapperProps>`
  143. display: flex;
  144. flex-shrink: 0;
  145. justify-content: ${p => (p.direction === 'right' ? 'flex-end' : 'flex-start')};
  146. ${p =>
  147. p.consistentWidth && `width: ${p.size + (p.max - 1) * getOverlapWidth(p.size)}px;`};
  148. ${PlatformIcons} {
  149. ${p =>
  150. p.showCounter
  151. ? css`
  152. z-index: 1;
  153. flex-direction: row-reverse;
  154. > * :not(:first-child) {
  155. margin-right: ${p.size * -1 + getOverlapWidth(p.size)}px;
  156. }
  157. `
  158. : css`
  159. > * :not(:first-child) {
  160. margin-left: ${p.size * -1 + getOverlapWidth(p.size)}px;
  161. }
  162. `}
  163. }
  164. ${InnerWrapper} {
  165. padding-right: ${p => p.size / 2 + 1}px;
  166. }
  167. ${Counter} {
  168. height: ${p => p.size}px;
  169. min-width: ${p => p.size}px;
  170. }
  171. `;