Skip to content

Commit 6b53653

Browse files
authored
Merge pull request #711 from glints-dev/feature/next-avatar
feat: add avatar component
2 parents 9b5ce8a + 630a933 commit 6b53653

30 files changed

+237
-0
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react';
2+
import { Story, Meta } from '@storybook/react';
3+
import { BaseContainer } from '../../Layout/GlintsContainer/GlintsContainer';
4+
import { Avatar, AvatarProps } from './Avatar';
5+
6+
export default {
7+
title: '@next/Avatar',
8+
component: Avatar,
9+
decorators: [Story => <BaseContainer>{Story()}</BaseContainer>],
10+
argTypes: {},
11+
} as Meta;
12+
13+
const Template: Story<AvatarProps> = args => <Avatar {...args} />;
14+
15+
export const Interactive = Template.bind({});
16+
Interactive.args = {
17+
initials: 'HE',
18+
};

src/@next/Avatar/Avatar.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react';
2+
import { Typography } from '../Typography';
3+
import { Neutral } from '../utilities/colors';
4+
import { AvatarStyle } from './AvatarStyle';
5+
6+
const avatarBackgroundColorVariant = [
7+
'supplementary',
8+
'warning',
9+
'danger',
10+
'success',
11+
'outstanding',
12+
] as const;
13+
14+
const avatarSizeVariant = ['large', 'medium'] as const;
15+
16+
export type AvatarBackgroundColorVariant =
17+
typeof avatarBackgroundColorVariant[number];
18+
19+
export type AvatarSizeVariant = typeof avatarSizeVariant[number];
20+
21+
export interface AvatarProps extends React.HTMLAttributes<HTMLDivElement> {
22+
variant?: AvatarBackgroundColorVariant;
23+
size?: AvatarSizeVariant;
24+
initials?: string;
25+
}
26+
27+
export const Avatar = ({
28+
variant = 'supplementary',
29+
size = 'medium',
30+
initials,
31+
...props
32+
}: AvatarProps) => {
33+
return (
34+
<AvatarStyle variant={variant} size={size} {...props}>
35+
<Typography
36+
variant={size === 'medium' ? 'body1' : 'headline6'}
37+
color={Neutral.B18}
38+
as={'span'}
39+
>
40+
{initials.slice(0, 2)}
41+
</Typography>
42+
</AvatarStyle>
43+
);
44+
};

src/@next/Avatar/AvatarStyle.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import styled from 'styled-components';
2+
import * as Breakpoints from '../utilities/breakpoints';
3+
import { Blue, Green, Neutral, Orange, Red } from '../utilities/colors';
4+
import {
5+
AvatarBackgroundColorVariant,
6+
AvatarProps,
7+
AvatarSizeVariant,
8+
} from './Avatar';
9+
10+
const mediumAvatarSizeStyle = `
11+
width: 40px;
12+
height: 40px;
13+
14+
@media (max-width: ${Breakpoints.large}) {
15+
width: 32px;
16+
height: 32px;
17+
}
18+
`;
19+
20+
const largeAvatarSizeStyle = `
21+
width: 60px;
22+
height: 60px;
23+
24+
@media (max-width: ${Breakpoints.large}) {
25+
width: 40px;
26+
height: 40px;
27+
}
28+
`;
29+
30+
const avatarBackgroundColor: {
31+
[variant in AvatarBackgroundColorVariant]: string;
32+
} = {
33+
['supplementary']: Neutral.B99,
34+
['warning']: Orange.S42,
35+
['danger']: Red.B100,
36+
['success']: Green.B89,
37+
['outstanding']: Blue.S08,
38+
};
39+
40+
const avatarSizeVariant: {
41+
[sizeVariant in AvatarSizeVariant]: string;
42+
} = {
43+
['large']: largeAvatarSizeStyle,
44+
['medium']: mediumAvatarSizeStyle,
45+
};
46+
47+
const getAvatarBackgroundColor = (variant: AvatarBackgroundColorVariant) => {
48+
if (!(variant in avatarBackgroundColor)) {
49+
console.warn(
50+
`${variant} is not a valid variant for the background color, default will be used`
51+
);
52+
return avatarBackgroundColor['supplementary'];
53+
}
54+
55+
return avatarBackgroundColor[variant];
56+
};
57+
58+
const getAvatarSizeStyle = (sizeVariant: AvatarSizeVariant) => {
59+
if (!(sizeVariant in avatarSizeVariant)) {
60+
console.warn(
61+
`${sizeVariant} is not a valid variant for the size, default will be used`
62+
);
63+
64+
return avatarSizeVariant['medium'];
65+
}
66+
67+
return avatarSizeVariant[sizeVariant];
68+
};
69+
70+
export const AvatarStyle = styled.div<AvatarProps>`
71+
${({ size }: AvatarProps) => {
72+
return getAvatarSizeStyle(size);
73+
}}}
74+
75+
${({ variant }: AvatarProps) => ({
76+
'background-color': getAvatarBackgroundColor(variant),
77+
})}}
78+
79+
display: flex;
80+
align-items: center;
81+
justify-content: center;
82+
border-radius: 50%;
83+
`;

src/@next/Avatar/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './Avatar';

src/@next/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export { ButtonGroup } from './ButtonGroup';
1010
export { Icon } from './Icon';
1111
export { Tab, TabProps, Tabs, TabsProps } from './Tabs';
1212
export { Typography } from './Typography';
13+
export { Avatar } from './Avatar';
1314

1415
// Utilities
1516
import * as BorderRadius from './utilities/borderRadius';

test/e2e/avatar/avatar.spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { expect, Page, test } from '@playwright/test';
2+
import { StoryBookPage } from '../storybookPage';
3+
4+
const getPage = (page: Page) =>
5+
new StoryBookPage(page, '?path=/story/next-avatar--interactive');
6+
7+
const testData = [
8+
{
9+
testName: 'Avatar',
10+
args: '',
11+
screenshotResult: 'avatar',
12+
},
13+
{
14+
testName: 'Avatar - supplementary',
15+
args: 'variant:supplementary',
16+
screenshotResult: 'avatar-supplementary',
17+
},
18+
{
19+
testName: 'Avatar - warning',
20+
args: 'variant:warning',
21+
screenshotResult: 'avatar-warning',
22+
},
23+
{
24+
testName: 'Avatar - danger',
25+
args: 'variant:danger',
26+
screenshotResult: 'avatar-danger',
27+
},
28+
{
29+
testName: 'Avatar - success',
30+
args: 'variant:success',
31+
screenshotResult: 'avatar-success',
32+
},
33+
{
34+
testName: 'Avatar - outstanding',
35+
args: 'variant:outstanding',
36+
screenshotResult: 'avatar-outstanding',
37+
},
38+
];
39+
40+
for (const [, testRecord] of testData.entries()) {
41+
test(`${testRecord.testName} - large`, async ({ page }) => {
42+
const avatarPage = getPage(page);
43+
await avatarPage.goto(
44+
`args=${testRecord.args};size:large` as `args=${string}`
45+
);
46+
await expect(avatarPage.container).toHaveScreenshot(
47+
`${testRecord.screenshotResult}-large.png`
48+
);
49+
});
50+
}
51+
52+
for (const [, testRecord] of testData.entries()) {
53+
test(`${testRecord.testName} - medium`, async ({ page }) => {
54+
const avatarPage = getPage(page);
55+
await avatarPage.goto(
56+
`args=${testRecord.args};size:medium` as `args=${string}`
57+
);
58+
await expect(avatarPage.container).toHaveScreenshot(
59+
`${testRecord.screenshotResult}-medium.png`
60+
);
61+
});
62+
}
63+
64+
test.describe('small viewport', () => {
65+
for (const [, testRecord] of testData.entries()) {
66+
test(`${testRecord.testName} - large`, async ({ page }) => {
67+
page.setViewportSize({ width: 768, height: 600 });
68+
const avatarPage = getPage(page);
69+
await avatarPage.goto(
70+
`args=${testRecord.args};size:large` as `args=${string}`
71+
);
72+
await expect(avatarPage.container).toHaveScreenshot(
73+
`${testRecord.screenshotResult}-small-viewport-large.png`
74+
);
75+
});
76+
}
77+
78+
for (const [, testRecord] of testData.entries()) {
79+
test(`${testRecord.testName} - medium`, async ({ page }) => {
80+
page.setViewportSize({ width: 768, height: 600 });
81+
const avatarPage = getPage(page);
82+
await avatarPage.goto(
83+
`args=${testRecord.args};size:medium` as `args=${string}`
84+
);
85+
await expect(avatarPage.container).toHaveScreenshot(
86+
`${testRecord.screenshotResult}-small-viewport-medium.png`
87+
);
88+
});
89+
}
90+
});
1.96 KB
Loading
1.54 KB
Loading
1.35 KB
Loading
1.11 KB
Loading

0 commit comments

Comments
 (0)