avatarList.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. import {css} from '@emotion/react';
  2. import styled from '@emotion/styled';
  3. import TeamAvatar from 'sentry/components/avatar/teamAvatar';
  4. import UserAvatar from 'sentry/components/avatar/userAvatar';
  5. import {Tooltip} from 'sentry/components/tooltip';
  6. import {AvatarUser, Team} from 'sentry/types';
  7. type UserAvatarProps = React.ComponentProps<typeof UserAvatar>;
  8. type Props = {
  9. avatarSize?: number;
  10. className?: string;
  11. maxVisibleAvatars?: number;
  12. renderTooltip?: UserAvatarProps['renderTooltip'];
  13. teams?: Team[];
  14. tooltipOptions?: UserAvatarProps['tooltipOptions'];
  15. typeAvatars?: string;
  16. users?: AvatarUser[];
  17. };
  18. function AvatarList({
  19. avatarSize = 28,
  20. maxVisibleAvatars = 5,
  21. typeAvatars = 'users',
  22. tooltipOptions = {},
  23. className,
  24. users = [],
  25. teams = [],
  26. renderTooltip,
  27. }: Props) {
  28. const numTeams = teams.length;
  29. const numVisibleTeams = maxVisibleAvatars - numTeams > 0 ? numTeams : maxVisibleAvatars;
  30. const maxVisibleUsers =
  31. maxVisibleAvatars - numVisibleTeams > 0 ? maxVisibleAvatars - numVisibleTeams : 0;
  32. // Reverse the order since css flex-reverse is used to display the avatars
  33. const visibleTeamAvatars = teams.slice(0, numVisibleTeams).reverse();
  34. const visibleUserAvatars = users.slice(0, maxVisibleUsers).reverse();
  35. const numCollapsedAvatars = users.length - visibleUserAvatars.length;
  36. if (!tooltipOptions.position) {
  37. tooltipOptions.position = 'top';
  38. }
  39. return (
  40. <AvatarListWrapper className={className}>
  41. {!!numCollapsedAvatars && (
  42. <Tooltip title={`${numCollapsedAvatars} other ${typeAvatars}`}>
  43. <CollapsedAvatars size={avatarSize} data-test-id="avatarList-collapsedavatars">
  44. {numCollapsedAvatars < 99 && <Plus>+</Plus>}
  45. {numCollapsedAvatars}
  46. </CollapsedAvatars>
  47. </Tooltip>
  48. )}
  49. {visibleUserAvatars.map(user => (
  50. <StyledUserAvatar
  51. key={`${user.id}-${user.email}`}
  52. user={user}
  53. size={avatarSize}
  54. renderTooltip={renderTooltip}
  55. tooltipOptions={tooltipOptions}
  56. hasTooltip
  57. />
  58. ))}
  59. {visibleTeamAvatars.map(team => (
  60. <StyledTeamAvatar
  61. key={`${team.id}-${team.name}`}
  62. team={team}
  63. size={avatarSize}
  64. tooltipOptions={tooltipOptions}
  65. hasTooltip
  66. />
  67. ))}
  68. </AvatarListWrapper>
  69. );
  70. }
  71. export default AvatarList;
  72. // used in releases list page to do some alignment
  73. export const AvatarListWrapper = styled('div')`
  74. display: flex;
  75. flex-direction: row-reverse;
  76. `;
  77. const AvatarStyle = p => css`
  78. border: 2px solid ${p.theme.background};
  79. margin-left: -8px;
  80. cursor: default;
  81. &:hover {
  82. z-index: 1;
  83. }
  84. `;
  85. const StyledUserAvatar = styled(UserAvatar)`
  86. overflow: hidden;
  87. border-radius: 50%;
  88. ${AvatarStyle};
  89. `;
  90. const StyledTeamAvatar = styled(TeamAvatar)`
  91. overflow: hidden;
  92. ${AvatarStyle}
  93. `;
  94. const CollapsedAvatars = styled('div')<{size: number}>`
  95. display: flex;
  96. align-items: center;
  97. justify-content: center;
  98. position: relative;
  99. text-align: center;
  100. font-weight: 600;
  101. background-color: ${p => p.theme.gray200};
  102. color: ${p => p.theme.gray300};
  103. font-size: ${p => Math.floor(p.size / 2.3)}px;
  104. width: ${p => p.size}px;
  105. height: ${p => p.size}px;
  106. border-radius: 50%;
  107. ${AvatarStyle};
  108. `;
  109. const Plus = styled('span')`
  110. font-size: 10px;
  111. margin-left: 1px;
  112. margin-right: -1px;
  113. `;