platformList.tsx 4.7 KB

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