-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat(avatar): implement test * feat(avatar): scaffold package * feat(avatar): implement component * feat(avatar): add avatar story
- Loading branch information
1 parent
d476230
commit f527341
Showing
14 changed files
with
452 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import type { StorybookConfig } from '@storybook/react-vite'; | ||
|
||
export default { | ||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], | ||
addons: [ | ||
'@storybook/addon-onboarding', | ||
'@storybook/addon-links', | ||
'@storybook/addon-essentials', | ||
'@chromatic-com/storybook', | ||
'@storybook/addon-interactions', | ||
], | ||
framework: { | ||
name: '@storybook/react-vite', | ||
options: {}, | ||
}, | ||
} satisfies StorybookConfig; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import type { Preview } from '@storybook/react'; | ||
|
||
export default { | ||
tags: ['autodocs'], | ||
} satisfies Preview; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
declare module '*.module.css'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
{ | ||
"name": "@sipe-team/avatar", | ||
"description": "Avatar component for Sipe Design System", | ||
"version": "0.0.0", | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/sipe-team/3-2_side" | ||
}, | ||
"type": "module", | ||
"exports": "./src/index.ts", | ||
"files": [ | ||
"dist" | ||
], | ||
"scripts": { | ||
"build": "tsup", | ||
"build:storybook": "storybook build", | ||
"dev:storybook": "storybook dev -p 6006", | ||
"lint": "biome lint .", | ||
"test": "vitest", | ||
"typecheck": "tsc", | ||
"prepack": "pnpm run build" | ||
}, | ||
"dependencies": { | ||
"@radix-ui/react-slot": "^1.1.0", | ||
"@sipe-team/typography": "workspace:^", | ||
"@sipe-team/tokens": "workspace:*", | ||
"clsx": "^2.1.1" | ||
}, | ||
"devDependencies": { | ||
"@faker-js/faker": "^9.2.0", | ||
"@storybook/addon-essentials": "catalog:", | ||
"@storybook/addon-interactions": "catalog:", | ||
"@storybook/addon-links": "catalog:", | ||
"@storybook/blocks": "catalog:", | ||
"@storybook/react": "catalog:", | ||
"@storybook/react-vite": "catalog:", | ||
"@storybook/test": "catalog:", | ||
"@types/react": "^18.3.12", | ||
"@testing-library/jest-dom": "^6.6.3", | ||
"@testing-library/react": "^16.0.1", | ||
"happy-dom": "catalog:", | ||
"react": "^18.3.1", | ||
"storybook": "catalog:", | ||
"tsup": "catalog:", | ||
"typescript": "catalog:", | ||
"vitest": "catalog:" | ||
}, | ||
"peerDependencies": { | ||
"react": ">= 18" | ||
}, | ||
"publishConfig": { | ||
"access": "public", | ||
"registry": "https://npm.pkg.github.com", | ||
"exports": { | ||
".": { | ||
"import": { | ||
"types": "./dist/index.d.ts", | ||
"default": "./dist/index.js" | ||
}, | ||
"require": { | ||
"types": "./dist/index.d.cts", | ||
"default": "./dist/index.cjs" | ||
} | ||
} | ||
} | ||
}, | ||
"sideEffects": false | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
.avatar { | ||
width: var(--avatar-size); | ||
height: var(--avatar-size); | ||
border-radius: var(--avatar-shape); | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
overflow: hidden; | ||
background-color: #e2e8f0; | ||
} | ||
|
||
.image { | ||
width: 100%; | ||
height: 100%; | ||
object-fit: cover; | ||
} | ||
|
||
.fallback { | ||
font-size: 0.8rem; | ||
color: #2d3748; | ||
text-align: center; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import type { Meta, StoryObj } from "@storybook/react"; | ||
import { Avatar } from "./Avatar"; | ||
import { faker } from "@faker-js/faker"; | ||
|
||
const meta = { | ||
title: "Avatar", | ||
component: Avatar, | ||
parameters: { | ||
layout: "centered", | ||
}, | ||
} satisfies Meta<typeof Avatar>; | ||
export default meta; | ||
|
||
type Story = StoryObj<typeof meta>; | ||
|
||
const testImage = faker.image.avatar(); | ||
|
||
export const Basic: Story = { | ||
args: { | ||
src: "https://randomuser.me/api/portraits/men/1.jpg", | ||
alt: "대체 텍스트", | ||
}, | ||
}; | ||
|
||
export const Sizes: Story = { | ||
render: () => ( | ||
<div style={{ display: "flex", gap: "1rem" }}> | ||
<Avatar size="xs" src={testImage} alt="XSmall" /> | ||
<Avatar size="sm" src={testImage} alt="small" /> | ||
<Avatar size="md" src={testImage} alt="medium" /> | ||
<Avatar size="lg" src={testImage} alt="large" /> | ||
<Avatar size="xl" src={testImage} alt="XLarge" /> | ||
</div> | ||
), | ||
}; | ||
|
||
export const Shapes: Story = { | ||
render: () => ( | ||
<div style={{ display: "flex", gap: "1rem" }}> | ||
<Avatar shape="circle" src={testImage} alt="원형" /> | ||
<Avatar shape="rounded" src={testImage} alt="둥근 사각형" /> | ||
<Avatar shape="square" src={testImage} alt="사각형" /> | ||
</div> | ||
), | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { faker } from "@faker-js/faker"; | ||
import { render, screen } from "@testing-library/react"; | ||
import { expect, test, describe, it } from "vitest"; | ||
import { Avatar } from "./Avatar"; | ||
import type { AvatarShape, AvatarSize } from "./Avatar"; | ||
|
||
const testImage = faker.image.avatar(); | ||
|
||
test("Avatar 컴포넌트가 주입받은 이미지 주소를 src 속성으로 설정한다.", () => { | ||
render(<Avatar src={testImage} alt="대체 텍스트" />); | ||
|
||
const img = screen.getByRole("img"); | ||
expect(img).toHaveAttribute("src", testImage); | ||
}); | ||
|
||
test("이미지가 없을 경우 대체 텍스트를 표시한다.", () => { | ||
render(<Avatar alt="대체 텍스트" />); | ||
|
||
expect(screen.getByText("대체 텍스트")).toBeInTheDocument(); | ||
}); | ||
|
||
test("이미지 로드 실패 시 fallback을 표시한다.", () => { | ||
render( | ||
<Avatar | ||
src="broken-link" | ||
fallback="https://randomuser.me/api/portraits/women/1.jpg" | ||
/> | ||
); | ||
|
||
const img = screen.getByRole("img"); | ||
img.dispatchEvent(new Event("error")); | ||
|
||
expect(img).toHaveAttribute( | ||
"src", | ||
"https://randomuser.me/api/portraits/women/1.jpg" | ||
); | ||
}); | ||
|
||
describe("Avatar 컴포넌트", () => { | ||
const sizes: { size: AvatarSize; expectedSize: string }[] = [ | ||
{ size: "xs", expectedSize: "24px" }, | ||
{ size: "sm", expectedSize: "32px" }, | ||
{ size: "md", expectedSize: "40px" }, | ||
{ size: "lg", expectedSize: "70px" }, | ||
{ size: "xl", expectedSize: "96px" }, | ||
]; | ||
|
||
const shapes: { shape: AvatarShape; expectedRadius: string }[] = [ | ||
{ shape: "circle", expectedRadius: "50%" }, | ||
{ shape: "rounded", expectedRadius: "4px" }, | ||
{ shape: "square", expectedRadius: "0px" }, | ||
]; | ||
|
||
it.each(sizes)( | ||
"size가 $size일때 $expectedSize x $expectedSize 크기로 렌더링 된다.", | ||
({ size, expectedSize }) => { | ||
render(<Avatar src={testImage} size={size} />); | ||
const container = screen.getByRole("img").parentElement; | ||
expect(container).toHaveStyle({ | ||
width: expectedSize, | ||
height: expectedSize, | ||
}); | ||
} | ||
); | ||
|
||
it.each(shapes)( | ||
"shape가 $shape일때 borderRadius는 $expectedRadius로 나타난다.", | ||
({ shape, expectedRadius }) => { | ||
render(<Avatar src={testImage} shape={shape} />); | ||
const container = screen.getByRole("img").parentElement; | ||
expect(container).toHaveStyle({ borderRadius: expectedRadius }); | ||
} | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import { Slot } from "@radix-ui/react-slot"; | ||
import { clsx as cx } from "clsx"; | ||
import { | ||
type CSSProperties, | ||
type ComponentProps, | ||
type ForwardedRef, | ||
forwardRef, | ||
} from "react"; | ||
import styles from "./Avatar.module.css"; | ||
|
||
/** | ||
+ * Avatar 컴포넌트의 크기 옵션 | ||
+ * @type {AvatarSize} | ||
+ * - xs: 24px | ||
+ * - sm: 32px | ||
+ * - md: 40px (기본값) | ||
+ * - lg: 70px | ||
+ * - xl: 96px | ||
+ */ | ||
export type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl"; | ||
|
||
/** | ||
+ * Avatar 컴포넌트의 모양 옵션 | ||
+ * @type {AvatarShape} | ||
+ * - circle: 원형 (50% border-radius) | ||
+ * - rounded: 둥근 모서리 (4px border-radius) | ||
+ * - square: 정사각형 (0px border-radius) | ||
+ */ | ||
export type AvatarShape = "circle" | "rounded" | "square"; | ||
|
||
export interface AvatarProps extends ComponentProps<"div"> { | ||
asChild?: boolean; | ||
src?: string; | ||
alt?: string; | ||
size?: AvatarSize; | ||
shape?: AvatarShape; | ||
fallback?: string; | ||
} | ||
|
||
export const Avatar = forwardRef(function Avatar( | ||
{ | ||
asChild, | ||
className, | ||
src, | ||
alt, | ||
size = "md", | ||
shape = "circle", | ||
fallback, | ||
...props | ||
}: AvatarProps, | ||
ref: ForwardedRef<any> | ||
) { | ||
const Component = asChild ? Slot : "div"; | ||
|
||
const style = { | ||
...props.style, | ||
width: getAvatarSize(size), | ||
height: getAvatarSize(size), | ||
borderRadius: getAvatarShape(shape), | ||
} as CSSProperties; | ||
|
||
return ( | ||
<Component | ||
className={cx(styles.avatar, className)} | ||
ref={ref} | ||
style={style} | ||
{...props} | ||
> | ||
{src ? ( | ||
<img | ||
src={src} | ||
alt={alt} | ||
onError={(e) => { | ||
if (fallback) e.currentTarget.src = fallback; | ||
}} | ||
className={styles.image} | ||
/> | ||
) : ( | ||
<span className={styles.fallback}>{alt || fallback}</span> | ||
)} | ||
</Component> | ||
); | ||
}); | ||
|
||
function getAvatarSize(size: AvatarSize) { | ||
switch (size) { | ||
case "xs": | ||
return "24px"; | ||
case "sm": | ||
return "32px"; | ||
case "md": | ||
return "40px"; | ||
case "lg": | ||
return "70px"; | ||
case "xl": | ||
return "96px"; | ||
default: | ||
return "40px"; | ||
} | ||
} | ||
|
||
function getAvatarShape(shape: AvatarShape) { | ||
switch (shape) { | ||
case "rounded": | ||
return "4px"; | ||
case "square": | ||
return "0px"; | ||
default: | ||
return "50%"; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './Avatar.tsx'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"extends": "../../tsconfig.json" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { defineConfig } from 'tsup'; | ||
|
||
export default defineConfig({ | ||
entry: ['src/index.ts'], | ||
clean: true, | ||
dts: true, | ||
format: ['esm', 'cjs'], | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { defineConfig } from 'vitest/config'; | ||
|
||
export default defineConfig({ | ||
// 테스트와 관련한 설정 | ||
test: { | ||
// 테스트를 실행할 환경 | ||
// default: 'node' | ||
// 브라우저 환경에서 테스트를 희망시 - 'jsdom' 또는 'happy-dom'으로 설정 | ||
environment: 'happy-dom', | ||
|
||
// 글로벌 API를 사용할지 여부를 선택 | ||
// ex) describe, it, expect 등 | ||
globals: true, | ||
|
||
// 테스트 실행 환경에 필요한 스크립트를 불러올 수 있음 | ||
// ex) 모듈 mokcing, matcher extend 등 | ||
setupFiles: './vitest.setup.ts', | ||
passWithNoTests: true, | ||
watch: false, | ||
css: true, | ||
}, | ||
|
||
// 환경별로 설정해주어야하는 추가 기능을 플러그인으로 주입 가능 | ||
// ex) vite-tsconfig-paths | ||
plugins: [], | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// vitest에서 기본적으로 제공하는 matcher 외에도 DOM 환경에서 유용하게 사용가능한 다양한 matcher를 제공 | ||
// ex) expect(foo).toBeInTheDocument(); | ||
// 얘가 없으면 Avatar.test.tsx에서 error가 발생함 | ||
import '@testing-library/jest-dom'; |
Oops, something went wrong.