Skip to content

Commit

Permalink
Add AlphaAvatarGroup component (#2177)
Browse files Browse the repository at this point in the history
<!--
  How to write a good PR title:
- Follow [the Conventional Commits
specification](https://www.conventionalcommits.org/en/v1.0.0/).
  - Give as much context as necessary and as little as possible
  - Prefix it with [WIP] while it’s a work in progress
-->

## Self Checklist

- [x] I wrote a PR title in **English** and added an appropriate
**label** to the PR.
- [x] I wrote the commit message in **English** and to follow [**the
Conventional Commits
specification**](https://www.conventionalcommits.org/en/v1.0.0/).
- [x] I [added the
**changeset**](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md)
about the changes that needed to be released. (or didn't have to)
- [x] I wrote or updated **documentation** related to the changes. (or
didn't have to)
- [x] I wrote or updated **tests** related to the changes. (or didn't
have to)
- [x] I tested the changes in various browsers. (or didn't have to)
  - Windows: Chrome, Edge, (Optional) Firefox
  - macOS: Chrome, Edge, Safari, (Optional) Firefox

## Related Issue

<!-- Please link to issue if one exists -->

<!-- Fixes #0000 -->

- Fixes #2163 

## Summary

<!-- Please brief explanation of the changes made -->

- `AlphaAvatarGroup` 컴포넌트를 구현합니다. 

## Details

<!-- Please elaborate description of the changes -->

- AvatarGroup 에서 새로운 디자인 토큰을 사용하고 있지 않아서 토큰을 갈아끼울 필요는 없었습니다. 
- 아바타 사이즈에 따라서 border 두께를 다르게 주도록 디자인이 변경된 부분이 있어서 AvatarGroup에서 사이즈를
컨텍스트를 내려주도록 했습니다.
- 불필요하게 열어주는 속성을 제거했습니다. `ellipsisStyle`, `ellipsisClassName`,
`onMouseEnterEllipsis`, `onMouseLeaveEllipsis`속성이 없어졌습니다.


### Breaking change? (Yes/No)

<!-- If Yes, please describe the impact and migration path for users -->

- No

## References

<!-- Please list any other resources or points the reviewer should be
aware of -->

- [컴포넌트
스펙(internal)](https://www.notion.so/channelio/Avatar-Group-5fa6501554514acb816f3ff8ad0f7c91)
-
[피그마(internal)](https://www.figma.com/file/aJJF4bU82uR0jAsmWp5wlE/Navigation?type=design&node-id=1-19452&mode=dev)
  • Loading branch information
yangwooseong authored Apr 25, 2024
1 parent e82bc25 commit 61752bf
Show file tree
Hide file tree
Showing 11 changed files with 768 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/soft-walls-pretend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@channel.io/bezier-react": patch
---

Add `AlphaAvatarGroup` component
50 changes: 33 additions & 17 deletions packages/bezier-react/src/components/AlphaAvatar/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,46 @@ import classNames from 'classnames'

import { isEmpty } from '~/src/utils/type'

import { useAvatarGroupContext } from '~/src/components/AlphaAvatarGroup/AvatarGroup'
import {
SmoothCornersBox,
type SmoothCornersBoxProps,
} from '~/src/components/SmoothCornersBox'
import { Status, type StatusSize } from '~/src/components/Status'
import { useTokens } from '~/src/components/ThemeProvider'

import type { AvatarProps } from './Avatar.types'
import type { AvatarProps, AvatarSize } from './Avatar.types'
import defaultAvatarUrl from './assets/default-avatar.svg'
import useProgressiveImage from './useProgressiveImage'

import styles from './Avatar.module.scss'

const shadow: SmoothCornersBoxProps['shadow'] = {
spreadRadius: 2,
color: 'bg-white-high',
function getStatusSize(size: AvatarSize): StatusSize {
switch (size) {
case '90':
case '120':
return 'l'
default:
return 'm'
}
}

function getShadow(size: AvatarSize): SmoothCornersBoxProps['shadow'] {
const spreadRadius = (() => {
switch (size) {
case '90':
return 3
case '120':
return 4
default:
return 2
}
})()

return {
spreadRadius,
color: 'bg-white-high',
}
}

export function useAvatarRadiusToken() {
Expand Down Expand Up @@ -49,7 +73,7 @@ export const Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
{
avatarUrl = '',
fallbackUrl = defaultAvatarUrl,
size = '24',
size: sizeProps = '24',
name,
disabled = false,
showBorder = false,
Expand All @@ -61,6 +85,8 @@ export const Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
},
forwardedRef
) {
const avatarGroupContext = useAvatarGroupContext()
const size = avatarGroupContext?.size ?? sizeProps
const loadedAvatarUrl = useProgressiveImage(avatarUrl, fallbackUrl)
const AVATAR_BORDER_RADIUS = useAvatarRadiusToken()

Expand All @@ -72,16 +98,6 @@ export const Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
return null
}

const statusSize: StatusSize = (() => {
switch (size) {
case '90':
case '120':
return 'l'
default:
return 'm'
}
})()

const Contents = (() => {
if (children) {
return children
Expand All @@ -90,7 +106,7 @@ export const Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
return (
<Status
type={status}
size={statusSize}
size={getStatusSize(size)}
/>
)
}
Expand Down Expand Up @@ -131,7 +147,7 @@ export const Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
)}
disabled={!smoothCorners}
borderRadius={AVATAR_BORDER_RADIUS}
shadow={showBorder ? shadow : undefined}
shadow={showBorder ? getShadow(size) : undefined}
backgroundColor="bg-white-normal"
backgroundImage={loadedAvatarUrl}
data-testid={AVATAR_TEST_ID}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react'

import { type Meta, type StoryFn, type StoryObj } from '@storybook/react'

import { AlphaAvatar } from '~/src/components/AlphaAvatar'

import { AvatarGroup } from './AvatarGroup'
import { type AvatarGroupProps } from './AvatarGroup.types'
import MOCK_AVATAR_LIST from './__mocks__/avatarList'

const meta: Meta<typeof AvatarGroup> = {
component: AvatarGroup,
argTypes: {
max: {
control: {
type: 'range',
min: 1,
max: MOCK_AVATAR_LIST.length,
step: 1,
},
},
spacing: {
control: {
type: 'range',
min: -50,
max: 50,
step: 1,
},
},
},
}
export default meta

const Template: StoryFn<AvatarGroupProps> = (args) => (
<AvatarGroup {...args}>
{MOCK_AVATAR_LIST.map(({ id, avatarUrl, name }) => (
<AlphaAvatar
key={id}
avatarUrl={avatarUrl}
name={name}
/>
))}
</AvatarGroup>
)

export const Primary: StoryObj<AvatarGroupProps> = {
render: Template,

args: {
max: 5,
ellipsisType: 'icon',
size: '30',
spacing: 4,
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
@import '../AlphaAvatar/Avatar.module';

.AvatarGroup {
--b-avatar-group-spacing: 0;
--b-avatar-group-size: 0;

position: relative;
z-index: var(--z-index-base);
display: flex;

@each $size in $avatar-sizes {
&:where(.size-#{$size}) {
--b-avatar-group-size: #{$size}px;
}
}

& > * + * {
margin-left: var(--b-avatar-group-spacing);
}
}

.AvatarEllipsisIconWrapper {
position: relative;
}

.AvatarEllipsisIcon {
position: absolute;
z-index: var(--z-index-floating);
top: 0;
right: 0;

display: flex;
align-items: center;
justify-content: center;

width: 100%;
height: 100%;

outline: none;
}

.AvatarEllipsisCountWrapper {
--b-avatar-group-ellipsis-ml: 0;

margin-left: var(--b-avatar-group-ellipsis-ml);
}

.AvatarEllipsisCount {
position: relative;
display: flex;
align-items: center;
height: var(--b-avatar-group-size);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from 'react'

import { render } from '~/src/utils/test'

import { Avatar } from '~/src/components/Avatar'

import { AVATAR_GROUP_ELLIPSIS_ICON_TEST_ID, AvatarGroup } from './AvatarGroup'
import { type AvatarGroupProps } from './AvatarGroup.types'
import MOCK_AVATAR_LIST from './__mocks__/avatarList'

describe('AvatarGroup', () => {
let props: AvatarGroupProps
const mockFallbackUrl = 'https://www.google.com'

beforeEach(() => {
props = {
max: MOCK_AVATAR_LIST.length - 1,
spacing: 4,
ellipsisType: 'icon',
}
})

afterAll(() => {
jest.restoreAllMocks()
})

const renderComponent = (otherProps?: AvatarGroupProps) =>
render(
<AvatarGroup
{...props}
{...otherProps}
>
{MOCK_AVATAR_LIST.map(({ id, avatarUrl, name }) => (
<Avatar
key={id}
avatarUrl={avatarUrl}
fallbackUrl={mockFallbackUrl}
name={name}
/>
))}
</AvatarGroup>
)

describe('Ellipsis type - Icon', () => {
beforeEach(() => {
props.ellipsisType = 'icon'
})

it('Snapshot', () => {
const { getByRole } = renderComponent()
const rendered = getByRole('group')
expect(rendered).toMatchSnapshot()
})

it('should render ellipsis icon when avatar count is more than max', () => {
const { getByTestId } = renderComponent()
const rendered = getByTestId(AVATAR_GROUP_ELLIPSIS_ICON_TEST_ID)
expect(rendered).toBeInTheDocument()
})

it('should not render ellipsis icon when avatar count is less than max', () => {
props.max = MOCK_AVATAR_LIST.length
const { queryByTestId } = renderComponent()
const rendered = queryByTestId(AVATAR_GROUP_ELLIPSIS_ICON_TEST_ID)
expect(rendered).not.toBeInTheDocument()
})
})

describe('Ellipsis type - Count', () => {
beforeEach(() => {
props.ellipsisType = 'count'
})

it('Snapshot', () => {
const { getByRole } = renderComponent()
const rendered = getByRole('group')
expect(rendered).toMatchSnapshot()
})

it('should render ellipsis count when avatar count is more than max', () => {
const { getByText } = renderComponent()
const rendered = getByText('+1')
expect(rendered).toBeInTheDocument()
})

it('should not render ellipsis count when avatar count is less than max', () => {
props.max = MOCK_AVATAR_LIST.length
const { queryByText } = renderComponent()
const rendered = queryByText('+1')
expect(rendered).not.toBeInTheDocument()
})
})
})
Loading

0 comments on commit 61752bf

Please sign in to comment.