@channel.io/bezier-react 2.0.2 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/components/AlphaAvatar/Avatar.js +121 -0
- package/dist/cjs/components/AlphaAvatar/Avatar.js.map +1 -0
- package/dist/cjs/components/AlphaAvatar/Avatar.module.scss.js +8 -0
- package/dist/cjs/components/AlphaAvatar/Avatar.module.scss.js.map +1 -0
- package/dist/cjs/components/AlphaAvatar/assets/default-avatar.svg.js +8 -0
- package/dist/cjs/components/AlphaAvatar/assets/default-avatar.svg.js.map +1 -0
- package/dist/cjs/components/AlphaAvatar/useProgressiveImage.js +48 -0
- package/dist/cjs/components/AlphaAvatar/useProgressiveImage.js.map +1 -0
- package/dist/cjs/components/AlphaAvatarGroup/AvatarGroup.js +153 -0
- package/dist/cjs/components/AlphaAvatarGroup/AvatarGroup.js.map +1 -0
- package/dist/cjs/components/AlphaAvatarGroup/AvatarGroup.module.scss.js +8 -0
- package/dist/cjs/components/AlphaAvatarGroup/AvatarGroup.module.scss.js.map +1 -0
- package/dist/cjs/index.js +10 -5
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/styles.css +1 -1
- package/dist/esm/components/AlphaAvatar/Avatar.mjs +115 -0
- package/dist/esm/components/AlphaAvatar/Avatar.mjs.map +1 -0
- package/dist/esm/components/AlphaAvatar/Avatar.module.scss.mjs +4 -0
- package/dist/esm/components/AlphaAvatar/Avatar.module.scss.mjs.map +1 -0
- package/dist/esm/components/AlphaAvatar/assets/default-avatar.svg.mjs +4 -0
- package/dist/esm/components/AlphaAvatar/assets/default-avatar.svg.mjs.map +1 -0
- package/dist/esm/components/AlphaAvatar/useProgressiveImage.mjs +44 -0
- package/dist/esm/components/AlphaAvatar/useProgressiveImage.mjs.map +1 -0
- package/dist/esm/components/AlphaAvatarGroup/AvatarGroup.mjs +149 -0
- package/dist/esm/components/AlphaAvatarGroup/AvatarGroup.mjs.map +1 -0
- package/dist/esm/components/AlphaAvatarGroup/AvatarGroup.module.scss.mjs +4 -0
- package/dist/esm/components/AlphaAvatarGroup/AvatarGroup.module.scss.mjs.map +1 -0
- package/dist/esm/index.mjs +2 -0
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/styles.css +1 -1
- package/dist/types/components/AlphaAvatar/Avatar.d.ts +22 -0
- package/dist/types/components/AlphaAvatar/Avatar.d.ts.map +1 -0
- package/dist/types/components/AlphaAvatar/Avatar.types.d.ts +42 -0
- package/dist/types/components/AlphaAvatar/Avatar.types.d.ts.map +1 -0
- package/dist/types/components/AlphaAvatar/index.d.ts +3 -0
- package/dist/types/components/AlphaAvatar/index.d.ts.map +1 -0
- package/dist/types/components/AlphaAvatar/useProgressiveImage.d.ts +2 -0
- package/dist/types/components/AlphaAvatar/useProgressiveImage.d.ts.map +1 -0
- package/dist/types/components/AlphaAvatarGroup/AvatarGroup.d.ts +24 -0
- package/dist/types/components/AlphaAvatarGroup/AvatarGroup.d.ts.map +1 -0
- package/dist/types/components/AlphaAvatarGroup/AvatarGroup.types.d.ts +28 -0
- package/dist/types/components/AlphaAvatarGroup/AvatarGroup.types.d.ts.map +1 -0
- package/dist/types/components/AlphaAvatarGroup/index.d.ts +3 -0
- package/dist/types/components/AlphaAvatarGroup/index.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/components/AlphaAvatar/AlphaAvatar.stories.tsx +63 -0
- package/src/components/AlphaAvatar/Avatar.module.scss +54 -0
- package/src/components/AlphaAvatar/Avatar.test.tsx +111 -0
- package/src/components/AlphaAvatar/Avatar.tsx +159 -0
- package/src/components/AlphaAvatar/Avatar.types.ts +64 -0
- package/src/components/AlphaAvatar/__snapshots__/Avatar.test.tsx.snap +93 -0
- package/src/components/AlphaAvatar/assets/default-avatar.svg +11 -0
- package/src/components/AlphaAvatar/index.ts +8 -0
- package/src/components/AlphaAvatar/useProgressiveImage.test.ts +96 -0
- package/src/components/AlphaAvatar/useProgressiveImage.ts +60 -0
- package/src/components/AlphaAvatarGroup/AlphaAvatarGroup.stories.tsx +55 -0
- package/src/components/AlphaAvatarGroup/AvatarGroup.module.scss +53 -0
- package/src/components/AlphaAvatarGroup/AvatarGroup.test.tsx +93 -0
- package/src/components/AlphaAvatarGroup/AvatarGroup.tsx +229 -0
- package/src/components/AlphaAvatarGroup/AvatarGroup.types.ts +43 -0
- package/src/components/AlphaAvatarGroup/__mocks__/avatarList.ts +39 -0
- package/src/components/AlphaAvatarGroup/__snapshots__/AvatarGroup.test.tsx.snap +215 -0
- package/src/components/AlphaAvatarGroup/index.ts +2 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`Avatar > Snapshot 1`] = `
|
|
4
|
+
<div
|
|
5
|
+
aria-label="Name"
|
|
6
|
+
class="SmoothCornersBox AvatarImage"
|
|
7
|
+
data-state="disabled"
|
|
8
|
+
data-testid="bezier-avatar"
|
|
9
|
+
style="--b-smooth-corners-box-border-radius: 42%; --b-smooth-corners-box-shadow-blur-radius: 0px; --b-smooth-corners-box-shadow-spread-radius: 0px; --b-smooth-corners-box-padding: 0px; --b-smooth-corners-box-margin: 0px; --b-smooth-corners-box-background-color: var(--bg-white-normal); --b-smooth-corners-box-background-image: url(https://www.google.com);"
|
|
10
|
+
/>
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
exports[`Avatar > renders border style 1`] = `
|
|
14
|
+
<div
|
|
15
|
+
aria-label="Name"
|
|
16
|
+
class="SmoothCornersBox AvatarImage bordered"
|
|
17
|
+
data-state="disabled"
|
|
18
|
+
data-testid="bezier-avatar"
|
|
19
|
+
style="--b-smooth-corners-box-border-radius: 42%; --b-smooth-corners-box-shadow-blur-radius: 0px; --b-smooth-corners-box-shadow-spread-radius: 2px; --b-smooth-corners-box-shadow-color: var(--bg-white-high); --b-smooth-corners-box-padding: 4px; --b-smooth-corners-box-margin: 0px; --b-smooth-corners-box-background-color: var(--bg-white-normal); --b-smooth-corners-box-background-image: url(https://www.google.com);"
|
|
20
|
+
/>
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
exports[`Avatar > should have right -2px, bottom -2px style on StatusWrapper 1`] = `
|
|
24
|
+
<div
|
|
25
|
+
class="StatusWrapper"
|
|
26
|
+
data-testid="bezier-status-wrapper"
|
|
27
|
+
>
|
|
28
|
+
<div
|
|
29
|
+
class="Status size-m"
|
|
30
|
+
style="--b-status-bg-color: var(--bgtxt-green-normal);"
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
exports[`Avatar > should have right 2px, bottom 2px style on StatusWrapper when show border 1`] = `
|
|
36
|
+
<div
|
|
37
|
+
class="StatusWrapper"
|
|
38
|
+
data-testid="bezier-status-wrapper"
|
|
39
|
+
>
|
|
40
|
+
<div
|
|
41
|
+
class="Status size-m"
|
|
42
|
+
style="--b-status-bg-color: var(--bgtxt-green-normal);"
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
exports[`Avatar > should have right 4px, bottom 4px style on StatusWrapper when size grater then 72 1`] = `
|
|
48
|
+
<div
|
|
49
|
+
class="StatusWrapper"
|
|
50
|
+
data-testid="bezier-status-wrapper"
|
|
51
|
+
>
|
|
52
|
+
<div
|
|
53
|
+
class="Status size-m"
|
|
54
|
+
style="--b-status-bg-color: var(--bgtxt-green-normal);"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
exports[`Avatar > should have right 4px, bottom 4px style on StatusWrapper when size grater then 90 1`] = `
|
|
60
|
+
<div
|
|
61
|
+
class="StatusWrapper"
|
|
62
|
+
data-testid="bezier-status-wrapper"
|
|
63
|
+
>
|
|
64
|
+
<div
|
|
65
|
+
class="Status size-l"
|
|
66
|
+
style="--b-status-bg-color: var(--bgtxt-green-normal);"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
exports[`Avatar > should have right 8px, bottom 8px style on StatusWrapper when size grater then 72 and show border 1`] = `
|
|
72
|
+
<div
|
|
73
|
+
class="StatusWrapper"
|
|
74
|
+
data-testid="bezier-status-wrapper"
|
|
75
|
+
>
|
|
76
|
+
<div
|
|
77
|
+
class="Status size-m"
|
|
78
|
+
style="--b-status-bg-color: var(--bgtxt-green-normal);"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
exports[`Avatar > should have right 8px, bottom 8px style on StatusWrapper when size grater then 90 and show border 1`] = `
|
|
84
|
+
<div
|
|
85
|
+
class="StatusWrapper"
|
|
86
|
+
data-testid="bezier-status-wrapper"
|
|
87
|
+
>
|
|
88
|
+
<div
|
|
89
|
+
class="Status size-l"
|
|
90
|
+
style="--b-status-bg-color: var(--bgtxt-green-normal);"
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<g clip-path="url(#clip0)">
|
|
3
|
+
<rect width="60" height="60" fill="#cfccfb"/>
|
|
4
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M41 23C41 29.0744 36.0744 34 30 34C23.9256 34 19 29.0744 19 23C19 16.9256 23.9256 12 30 12C36.0744 12 41 16.9256 41 23ZM30 37C16.1929 37 5 48.1929 5 62V90H55V62C55 48.1929 43.8071 37 30 37Z" fill="white" fill-opacity="0.4"/>
|
|
5
|
+
</g>
|
|
6
|
+
<defs>
|
|
7
|
+
<clipPath id="clip0">
|
|
8
|
+
<rect width="60" height="60" fill="white"/>
|
|
9
|
+
</clipPath>
|
|
10
|
+
</defs>
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { type RenderHookResult, waitFor } from '@testing-library/react'
|
|
2
|
+
|
|
3
|
+
import { renderHook } from '~/src/utils/test'
|
|
4
|
+
|
|
5
|
+
import useProgressiveImage from './useProgressiveImage'
|
|
6
|
+
|
|
7
|
+
const DELAY = 300
|
|
8
|
+
const TIME_OUT = DELAY + 1000
|
|
9
|
+
const IMAGE_URL_1 = 'image-url-1'
|
|
10
|
+
const IMAGE_URL_2 = 'image-url-2'
|
|
11
|
+
const IMAGE_URL_3 = 'image-url-3'
|
|
12
|
+
const FALLBACK_URL = 'fallback-url'
|
|
13
|
+
|
|
14
|
+
describe('useProgressiveImage ', () => {
|
|
15
|
+
let rendered: RenderHookResult<
|
|
16
|
+
string,
|
|
17
|
+
{ imageUrl: string; fallbackUrl: string }
|
|
18
|
+
>
|
|
19
|
+
const originalGlobalImage = window.Image
|
|
20
|
+
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
;(window.Image as any) = class MockImage {
|
|
23
|
+
onload = () => {}
|
|
24
|
+
src = ''
|
|
25
|
+
constructor() {
|
|
26
|
+
setTimeout(() => {
|
|
27
|
+
this.onload()
|
|
28
|
+
}, DELAY)
|
|
29
|
+
return this
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
afterAll(() => {
|
|
35
|
+
window.Image = originalGlobalImage
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
rendered = renderHook(
|
|
40
|
+
({ imageUrl, fallbackUrl }) => useProgressiveImage(imageUrl, fallbackUrl),
|
|
41
|
+
{
|
|
42
|
+
initialProps: {
|
|
43
|
+
imageUrl: IMAGE_URL_1,
|
|
44
|
+
fallbackUrl: FALLBACK_URL,
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should return fallback url at first', () => {
|
|
51
|
+
const { result } = rendered
|
|
52
|
+
|
|
53
|
+
expect(result.current).toStrictEqual(FALLBACK_URL)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should return image url after it loads', async () => {
|
|
57
|
+
const { result } = rendered
|
|
58
|
+
|
|
59
|
+
await waitFor(() => expect(result.current).toStrictEqual(IMAGE_URL_1), {
|
|
60
|
+
timeout: TIME_OUT,
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should update image url when hook is rendered with different url', async () => {
|
|
65
|
+
const { result, rerender } = rendered
|
|
66
|
+
|
|
67
|
+
rerender({
|
|
68
|
+
imageUrl: IMAGE_URL_2,
|
|
69
|
+
fallbackUrl: FALLBACK_URL,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
await waitFor(() => expect(result.current).toStrictEqual(IMAGE_URL_2), {
|
|
73
|
+
timeout: TIME_OUT,
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should loads image url immediately when rerendered', async () => {
|
|
78
|
+
const { rerender, result } = rendered
|
|
79
|
+
|
|
80
|
+
rerender({
|
|
81
|
+
imageUrl: IMAGE_URL_3,
|
|
82
|
+
fallbackUrl: FALLBACK_URL,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
await waitFor(() => expect(result.current).toStrictEqual(IMAGE_URL_3), {
|
|
86
|
+
timeout: TIME_OUT,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
rerender({
|
|
90
|
+
imageUrl: IMAGE_URL_3,
|
|
91
|
+
fallbackUrl: FALLBACK_URL,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
expect(result.current).toStrictEqual(IMAGE_URL_3)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
interface CachedImage {
|
|
4
|
+
src: string
|
|
5
|
+
isLoaded: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const imageCache = new Map<string, CachedImage>()
|
|
9
|
+
|
|
10
|
+
function getCachedImage(src: string) {
|
|
11
|
+
const cachedImage = imageCache.get(src)
|
|
12
|
+
if (!cachedImage) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
return cachedImage
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function useProgressiveImage(src: string, defaultSrc: string) {
|
|
19
|
+
const [source, setSource] = useState<CachedImage | null>(() =>
|
|
20
|
+
getCachedImage(src)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
useEffect(
|
|
24
|
+
function updateSource() {
|
|
25
|
+
if (source?.src === src) {
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const cachedImage = getCachedImage(src)
|
|
30
|
+
|
|
31
|
+
if (cachedImage?.isLoaded) {
|
|
32
|
+
setSource(cachedImage)
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const image = new Image()
|
|
37
|
+
image.src = src
|
|
38
|
+
image.onload = loadImage(true)
|
|
39
|
+
image.onerror = loadImage(false)
|
|
40
|
+
|
|
41
|
+
function loadImage(isLoaded: boolean) {
|
|
42
|
+
return () => {
|
|
43
|
+
const loadedImage = {
|
|
44
|
+
src,
|
|
45
|
+
isLoaded,
|
|
46
|
+
}
|
|
47
|
+
setSource(loadedImage)
|
|
48
|
+
imageCache.set(src, loadedImage)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
[src, source]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if (!source || !source.isLoaded) {
|
|
56
|
+
return defaultSrc
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return source.src
|
|
60
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { type Meta, type StoryFn, type StoryObj } from '@storybook/react'
|
|
4
|
+
|
|
5
|
+
import { AlphaAvatar } from '~/src/components/AlphaAvatar'
|
|
6
|
+
|
|
7
|
+
import { AvatarGroup } from './AvatarGroup'
|
|
8
|
+
import { type AvatarGroupProps } from './AvatarGroup.types'
|
|
9
|
+
import MOCK_AVATAR_LIST from './__mocks__/avatarList'
|
|
10
|
+
|
|
11
|
+
const meta: Meta<typeof AvatarGroup> = {
|
|
12
|
+
component: AvatarGroup,
|
|
13
|
+
argTypes: {
|
|
14
|
+
max: {
|
|
15
|
+
control: {
|
|
16
|
+
type: 'range',
|
|
17
|
+
min: 1,
|
|
18
|
+
max: MOCK_AVATAR_LIST.length,
|
|
19
|
+
step: 1,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
spacing: {
|
|
23
|
+
control: {
|
|
24
|
+
type: 'range',
|
|
25
|
+
min: -50,
|
|
26
|
+
max: 50,
|
|
27
|
+
step: 1,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
export default meta
|
|
33
|
+
|
|
34
|
+
const Template: StoryFn<AvatarGroupProps> = (args) => (
|
|
35
|
+
<AvatarGroup {...args}>
|
|
36
|
+
{MOCK_AVATAR_LIST.map(({ id, avatarUrl, name }) => (
|
|
37
|
+
<AlphaAvatar
|
|
38
|
+
key={id}
|
|
39
|
+
avatarUrl={avatarUrl}
|
|
40
|
+
name={name}
|
|
41
|
+
/>
|
|
42
|
+
))}
|
|
43
|
+
</AvatarGroup>
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
export const Primary: StoryObj<AvatarGroupProps> = {
|
|
47
|
+
render: Template,
|
|
48
|
+
|
|
49
|
+
args: {
|
|
50
|
+
max: 5,
|
|
51
|
+
ellipsisType: 'icon',
|
|
52
|
+
size: '30',
|
|
53
|
+
spacing: 4,
|
|
54
|
+
},
|
|
55
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
@import '../AlphaAvatar/Avatar.module';
|
|
2
|
+
|
|
3
|
+
.AvatarGroup {
|
|
4
|
+
--b-avatar-group-spacing: 0;
|
|
5
|
+
--b-avatar-group-size: 0;
|
|
6
|
+
|
|
7
|
+
position: relative;
|
|
8
|
+
z-index: var(--z-index-base);
|
|
9
|
+
display: flex;
|
|
10
|
+
|
|
11
|
+
@each $size in $avatar-sizes {
|
|
12
|
+
&:where(.size-#{$size}) {
|
|
13
|
+
--b-avatar-group-size: #{$size}px;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
& > * + * {
|
|
18
|
+
margin-left: var(--b-avatar-group-spacing);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.AvatarEllipsisIconWrapper {
|
|
23
|
+
position: relative;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.AvatarEllipsisIcon {
|
|
27
|
+
position: absolute;
|
|
28
|
+
z-index: var(--z-index-floating);
|
|
29
|
+
top: 0;
|
|
30
|
+
right: 0;
|
|
31
|
+
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
justify-content: center;
|
|
35
|
+
|
|
36
|
+
width: 100%;
|
|
37
|
+
height: 100%;
|
|
38
|
+
|
|
39
|
+
outline: none;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.AvatarEllipsisCountWrapper {
|
|
43
|
+
--b-avatar-group-ellipsis-ml: 0;
|
|
44
|
+
|
|
45
|
+
margin-left: var(--b-avatar-group-ellipsis-ml);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.AvatarEllipsisCount {
|
|
49
|
+
position: relative;
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
height: var(--b-avatar-group-size);
|
|
53
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { render } from '~/src/utils/test'
|
|
4
|
+
|
|
5
|
+
import { Avatar } from '~/src/components/Avatar'
|
|
6
|
+
|
|
7
|
+
import { AVATAR_GROUP_ELLIPSIS_ICON_TEST_ID, AvatarGroup } from './AvatarGroup'
|
|
8
|
+
import { type AvatarGroupProps } from './AvatarGroup.types'
|
|
9
|
+
import MOCK_AVATAR_LIST from './__mocks__/avatarList'
|
|
10
|
+
|
|
11
|
+
describe('AvatarGroup', () => {
|
|
12
|
+
let props: AvatarGroupProps
|
|
13
|
+
const mockFallbackUrl = 'https://www.google.com'
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
props = {
|
|
17
|
+
max: MOCK_AVATAR_LIST.length - 1,
|
|
18
|
+
spacing: 4,
|
|
19
|
+
ellipsisType: 'icon',
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
afterAll(() => {
|
|
24
|
+
jest.restoreAllMocks()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const renderComponent = (otherProps?: AvatarGroupProps) =>
|
|
28
|
+
render(
|
|
29
|
+
<AvatarGroup
|
|
30
|
+
{...props}
|
|
31
|
+
{...otherProps}
|
|
32
|
+
>
|
|
33
|
+
{MOCK_AVATAR_LIST.map(({ id, avatarUrl, name }) => (
|
|
34
|
+
<Avatar
|
|
35
|
+
key={id}
|
|
36
|
+
avatarUrl={avatarUrl}
|
|
37
|
+
fallbackUrl={mockFallbackUrl}
|
|
38
|
+
name={name}
|
|
39
|
+
/>
|
|
40
|
+
))}
|
|
41
|
+
</AvatarGroup>
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
describe('Ellipsis type - Icon', () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
props.ellipsisType = 'icon'
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('Snapshot', () => {
|
|
50
|
+
const { getByRole } = renderComponent()
|
|
51
|
+
const rendered = getByRole('group')
|
|
52
|
+
expect(rendered).toMatchSnapshot()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('should render ellipsis icon when avatar count is more than max', () => {
|
|
56
|
+
const { getByTestId } = renderComponent()
|
|
57
|
+
const rendered = getByTestId(AVATAR_GROUP_ELLIPSIS_ICON_TEST_ID)
|
|
58
|
+
expect(rendered).toBeInTheDocument()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('should not render ellipsis icon when avatar count is less than max', () => {
|
|
62
|
+
props.max = MOCK_AVATAR_LIST.length
|
|
63
|
+
const { queryByTestId } = renderComponent()
|
|
64
|
+
const rendered = queryByTestId(AVATAR_GROUP_ELLIPSIS_ICON_TEST_ID)
|
|
65
|
+
expect(rendered).not.toBeInTheDocument()
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('Ellipsis type - Count', () => {
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
props.ellipsisType = 'count'
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('Snapshot', () => {
|
|
75
|
+
const { getByRole } = renderComponent()
|
|
76
|
+
const rendered = getByRole('group')
|
|
77
|
+
expect(rendered).toMatchSnapshot()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should render ellipsis count when avatar count is more than max', () => {
|
|
81
|
+
const { getByText } = renderComponent()
|
|
82
|
+
const rendered = getByText('+1')
|
|
83
|
+
expect(rendered).toBeInTheDocument()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should not render ellipsis count when avatar count is less than max', () => {
|
|
87
|
+
props.max = MOCK_AVATAR_LIST.length
|
|
88
|
+
const { queryByText } = renderComponent()
|
|
89
|
+
const rendered = queryByText('+1')
|
|
90
|
+
expect(rendered).not.toBeInTheDocument()
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
})
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import React, { forwardRef, useCallback, useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
import { MoreIcon } from '@channel.io/bezier-icons'
|
|
4
|
+
import classNames from 'classnames'
|
|
5
|
+
|
|
6
|
+
import { isLastIndex } from '~/src/utils/array'
|
|
7
|
+
import { createContext } from '~/src/utils/react'
|
|
8
|
+
import { px } from '~/src/utils/style'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
type AlphaAvatarProps,
|
|
12
|
+
type AlphaAvatarSize,
|
|
13
|
+
useAlphaAvatarRadiusToken,
|
|
14
|
+
} from '~/src/components/AlphaAvatar'
|
|
15
|
+
import { Icon } from '~/src/components/Icon'
|
|
16
|
+
import { SmoothCornersBox } from '~/src/components/SmoothCornersBox'
|
|
17
|
+
import { Text } from '~/src/components/Text'
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
type AvatarGroupContextValue,
|
|
21
|
+
type AvatarGroupProps,
|
|
22
|
+
} from './AvatarGroup.types'
|
|
23
|
+
|
|
24
|
+
import styles from './AvatarGroup.module.scss'
|
|
25
|
+
|
|
26
|
+
const [AvatarGroupContextProvider, useAvatarGroupContext] = createContext<
|
|
27
|
+
AvatarGroupContextValue | undefined
|
|
28
|
+
>(undefined)
|
|
29
|
+
|
|
30
|
+
export { useAvatarGroupContext }
|
|
31
|
+
|
|
32
|
+
const MAX_AVATAR_LIST_COUNT = 99
|
|
33
|
+
const AVATAR_GROUP_DEFAULT_SPACING = 4
|
|
34
|
+
export const AVATAR_GROUP_ELLIPSIS_ICON_TEST_ID =
|
|
35
|
+
'bezier-avatar-group-ellipsis-icon'
|
|
36
|
+
|
|
37
|
+
function getRestAvatarListCountText(count: number, max: number) {
|
|
38
|
+
const restCount = count - max
|
|
39
|
+
return `+${restCount > MAX_AVATAR_LIST_COUNT ? MAX_AVATAR_LIST_COUNT : restCount}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// TODO: Not specified
|
|
43
|
+
function getProperIconSize(size: AlphaAvatarSize) {
|
|
44
|
+
return (
|
|
45
|
+
{
|
|
46
|
+
16: 'xxs',
|
|
47
|
+
20: 'xxs',
|
|
48
|
+
24: 'xs',
|
|
49
|
+
30: 's',
|
|
50
|
+
36: 'm',
|
|
51
|
+
42: 'm',
|
|
52
|
+
48: 'l',
|
|
53
|
+
72: 'l',
|
|
54
|
+
90: 'l',
|
|
55
|
+
120: 'l',
|
|
56
|
+
} as const
|
|
57
|
+
)[size]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// TODO: Not specified
|
|
61
|
+
function getProperTypoSize(size: AlphaAvatarSize) {
|
|
62
|
+
return (
|
|
63
|
+
{
|
|
64
|
+
16: '12',
|
|
65
|
+
20: '12',
|
|
66
|
+
24: '13',
|
|
67
|
+
30: '15',
|
|
68
|
+
36: '16',
|
|
69
|
+
42: '18',
|
|
70
|
+
48: '24',
|
|
71
|
+
72: '24',
|
|
72
|
+
90: '24',
|
|
73
|
+
120: '24',
|
|
74
|
+
} as const
|
|
75
|
+
)[size]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `AvatarGroup` is a component for grouping `Avatar` components
|
|
80
|
+
* @example
|
|
81
|
+
*
|
|
82
|
+
* ```tsx
|
|
83
|
+
* <AvatarGroup
|
|
84
|
+
* max={2}
|
|
85
|
+
* size="24"
|
|
86
|
+
* spacing={4}
|
|
87
|
+
* ellipsisType="icon"
|
|
88
|
+
* >
|
|
89
|
+
* <Avatar />
|
|
90
|
+
* <Avatar />
|
|
91
|
+
* <Avatar />
|
|
92
|
+
* </AvatarGroup>
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export const AvatarGroup = forwardRef<HTMLDivElement, AvatarGroupProps>(
|
|
96
|
+
function AvatarGroup(
|
|
97
|
+
{
|
|
98
|
+
max = 5,
|
|
99
|
+
size = '24',
|
|
100
|
+
spacing = AVATAR_GROUP_DEFAULT_SPACING,
|
|
101
|
+
ellipsisType = 'icon',
|
|
102
|
+
style,
|
|
103
|
+
className,
|
|
104
|
+
children,
|
|
105
|
+
...rest
|
|
106
|
+
},
|
|
107
|
+
forwardedRef
|
|
108
|
+
) {
|
|
109
|
+
const AVATAR_BORDER_RADIUS = useAlphaAvatarRadiusToken()
|
|
110
|
+
const avatarListCount = React.Children.count(children)
|
|
111
|
+
|
|
112
|
+
const renderAvatarElement = useCallback(
|
|
113
|
+
(avatar: React.ReactElement<AlphaAvatarProps>) => {
|
|
114
|
+
const key =
|
|
115
|
+
avatar.key ?? `${avatar.props.name}-${avatar.props.avatarUrl}`
|
|
116
|
+
const shouldShowBorder = max > 1 && avatarListCount > 1 && spacing < 0
|
|
117
|
+
const showBorder = avatar.props.showBorder || shouldShowBorder
|
|
118
|
+
return React.cloneElement(avatar, { key, showBorder })
|
|
119
|
+
},
|
|
120
|
+
[avatarListCount, max, spacing]
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const AvatarListComponent = useMemo(() => {
|
|
124
|
+
return React.Children.toArray(children)
|
|
125
|
+
.slice(0, max)
|
|
126
|
+
.map((avatar, index, arr) => {
|
|
127
|
+
if (!React.isValidElement<AlphaAvatarProps>(avatar)) {
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const AvatarElement = renderAvatarElement(avatar)
|
|
132
|
+
|
|
133
|
+
if (!isLastIndex(arr, index) || avatarListCount <= max) {
|
|
134
|
+
return AvatarElement
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (ellipsisType === 'icon') {
|
|
138
|
+
return (
|
|
139
|
+
<div
|
|
140
|
+
key="ellipsis"
|
|
141
|
+
className={styles.AvatarEllipsisIconWrapper}
|
|
142
|
+
data-testid={AVATAR_GROUP_ELLIPSIS_ICON_TEST_ID}
|
|
143
|
+
>
|
|
144
|
+
<SmoothCornersBox
|
|
145
|
+
borderRadius={AVATAR_BORDER_RADIUS}
|
|
146
|
+
backgroundColor="bgtxt-absolute-black-lightest"
|
|
147
|
+
className={styles.AvatarEllipsisIcon}
|
|
148
|
+
>
|
|
149
|
+
<Icon
|
|
150
|
+
source={MoreIcon}
|
|
151
|
+
size={getProperIconSize(size)}
|
|
152
|
+
color="bgtxt-absolute-white-dark"
|
|
153
|
+
/>
|
|
154
|
+
</SmoothCornersBox>
|
|
155
|
+
{AvatarElement}
|
|
156
|
+
</div>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (ellipsisType === 'count') {
|
|
161
|
+
return (
|
|
162
|
+
<React.Fragment key="ellipsis">
|
|
163
|
+
{AvatarElement}
|
|
164
|
+
<div
|
|
165
|
+
style={
|
|
166
|
+
{
|
|
167
|
+
'--b-avatar-group-ellipsis-ml': px(
|
|
168
|
+
Math.max(spacing, AVATAR_GROUP_DEFAULT_SPACING)
|
|
169
|
+
),
|
|
170
|
+
} as React.CSSProperties
|
|
171
|
+
}
|
|
172
|
+
className={classNames(styles.AvatarEllipsisCountWrapper)}
|
|
173
|
+
>
|
|
174
|
+
<Text
|
|
175
|
+
typo={getProperTypoSize(size)}
|
|
176
|
+
color="txt-black-dark"
|
|
177
|
+
className={styles.AvatarEllipsisCount}
|
|
178
|
+
>
|
|
179
|
+
{getRestAvatarListCountText(avatarListCount, max)}
|
|
180
|
+
</Text>
|
|
181
|
+
</div>
|
|
182
|
+
</React.Fragment>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return null
|
|
187
|
+
})
|
|
188
|
+
}, [
|
|
189
|
+
avatarListCount,
|
|
190
|
+
max,
|
|
191
|
+
children,
|
|
192
|
+
renderAvatarElement,
|
|
193
|
+
ellipsisType,
|
|
194
|
+
AVATAR_BORDER_RADIUS,
|
|
195
|
+
size,
|
|
196
|
+
spacing,
|
|
197
|
+
])
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<AvatarGroupContextProvider
|
|
201
|
+
value={useMemo(
|
|
202
|
+
() => ({
|
|
203
|
+
size,
|
|
204
|
+
}),
|
|
205
|
+
[size]
|
|
206
|
+
)}
|
|
207
|
+
>
|
|
208
|
+
<div
|
|
209
|
+
role="group"
|
|
210
|
+
ref={forwardedRef}
|
|
211
|
+
className={classNames(
|
|
212
|
+
styles.AvatarGroup,
|
|
213
|
+
styles[`size-${size}`],
|
|
214
|
+
className
|
|
215
|
+
)}
|
|
216
|
+
style={
|
|
217
|
+
{
|
|
218
|
+
'--b-avatar-group-spacing': px(spacing),
|
|
219
|
+
...style,
|
|
220
|
+
} as React.CSSProperties
|
|
221
|
+
}
|
|
222
|
+
{...rest}
|
|
223
|
+
>
|
|
224
|
+
{AvatarListComponent}
|
|
225
|
+
</div>
|
|
226
|
+
</AvatarGroupContextProvider>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
)
|