-
Notifications
You must be signed in to change notification settings - Fork 6
feat(image): add image component #262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 2 commits
7712c95
3fea105
c426061
ac2eb99
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| declare module '*.module.css'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아마 이거 사용하실일 없을거예요. 이거 Template으로 만드신 패키지같은데, vanilla extract 마이그레이션 이전의 상태인가보네요. 이건 module.css 사용할때 있었던 잔재같아요 ~ |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| { | ||
| "name": "@sipe-team/image", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. side 패키지에도 추가가 되어야할거같아요. |
||
| "description": "Image for Sipe Design System", | ||
| "version": "0.0.0", | ||
| "license": "MIT", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/sipe-team/side" | ||
| }, | ||
| "type": "module", | ||
| "exports": "./src/index.ts", | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "scripts": { | ||
| "build": "tsup", | ||
| "lint": "pnpm exec biome lint", | ||
| "test": "vitest", | ||
| "typecheck": "tsc", | ||
| "prepack": "pnpm run build" | ||
| }, | ||
| "devDependencies": { | ||
| "@testing-library/jest-dom": "catalog:", | ||
| "@testing-library/react": "catalog:", | ||
| "@types/react": "catalog:react", | ||
| "happy-dom": "catalog:", | ||
| "react": "catalog:react", | ||
| "react-dom": "catalog:react", | ||
| "tsup": "catalog:", | ||
| "typescript": "catalog:", | ||
| "vitest": "catalog:" | ||
| }, | ||
| "peerDependencies": { | ||
| "react": ">= 18", | ||
| "react-dom": ">= 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" | ||
| } | ||
| }, | ||
| "./styles.css": "./dist/index.css" | ||
| } | ||
| }, | ||
| "sideEffects": [ | ||
| "**/*.css" | ||
| ] | ||
|
Comment on lines
+56
to
+61
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 지금 따로
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| import type { Meta, StoryObj } from '@storybook/react'; | ||
|
|
||
| import { Image } from './Image'; | ||
|
|
||
| const meta: Meta<typeof Image> = { | ||
| title: 'Components/Image', | ||
| component: Image, | ||
| parameters: { | ||
| backgrounds: { | ||
| default: 'light', | ||
| }, | ||
| }, | ||
| args: { | ||
| src: 'https://picsum.photos/400/300', | ||
| alt: '예시 이미지', | ||
| width: 400, | ||
| height: 300, | ||
| }, | ||
| }; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<typeof Image>; | ||
|
|
||
| export const Basic: Story = { | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: '기본 이미지 렌더링입니다. 기본 크기와 기본 fit 동작을 확인할 수 있습니다.', | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| export const Fallbacks: Story = { | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: 'fallbackSrc가 있을 때와 없을 때 동작을 한 번에 비교합니다.', | ||
| }, | ||
| }, | ||
| }, | ||
| render: (args) => ( | ||
| <div style={{ display: 'flex', gap: 12 }}> | ||
| <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> | ||
| <Image | ||
| {...args} | ||
| alt="fallback with src" | ||
| src="https://invalid-url.com/broken.jpg" | ||
| fallbackSrc="https://dummyimage.com/400x300/e5e7eb/111827&text=FALLBACK" | ||
| /> | ||
| <span style={{ fontSize: 12, color: '#71717a' }}>with fallbackSrc</span> | ||
| </div> | ||
| <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> | ||
| <div | ||
| style={{ | ||
| width: 400, | ||
| height: 300, | ||
| border: '1px dashed #d4d4d8', | ||
| borderRadius: 8, | ||
| background: 'repeating-linear-gradient(45deg, #fafafa, #fafafa 10px, #f4f4f5 10px, #f4f4f5 20px)', | ||
| }} | ||
| > | ||
| <Image {...args} alt="error without fallback" src="https://invalid-url.com/broken.jpg" /> | ||
| </div> | ||
| <span style={{ fontSize: 12, color: '#71717a' }}>without fallbackSrc</span> | ||
| </div> | ||
| </div> | ||
| ), | ||
| args: { | ||
| width: 400, | ||
| height: 300, | ||
| }, | ||
| }; | ||
|
|
||
| export const WithPlaceholder: Story = { | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: '로딩 중 placeholder 확인은 DevTools에서 네트워크를 Slow 3G로 설정 후 새로고침하세요.', | ||
| }, | ||
| }, | ||
| }, | ||
| args: { | ||
| src: 'https://picsum.photos/400/300', | ||
| alt: 'placeholder 예시', | ||
| placeholder: ( | ||
| <div | ||
| style={{ | ||
| width: 400, | ||
| height: 300, | ||
| background: '#e0e0e0', | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| }} | ||
| > | ||
| 로딩 중... | ||
| </div> | ||
| ), | ||
| }, | ||
| }; | ||
|
|
||
| export const WithFill: Story = { | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: 'fill 사용 예시입니다. 부모 컨테이너에 `position: relative`, `width: 400`, `height: 300`이 필요합니다.', | ||
| }, | ||
| }, | ||
| }, | ||
| render: (args) => ( | ||
| <div style={{ position: 'relative', width: 400, height: 300 }}> | ||
| <Image {...args} fill /> | ||
| </div> | ||
| ), | ||
| args: { | ||
| src: 'https://picsum.photos/400/300', | ||
| }, | ||
| }; | ||
|
|
||
| export const Fits: Story = { | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: 'fit 옵션(`contain`, `cover`)을 같은 크기 박스에서 비교하는 예시입니다.', | ||
| }, | ||
| }, | ||
| }, | ||
| render: (args) => ( | ||
| <div style={{ display: 'flex', gap: 12 }}> | ||
| <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> | ||
| <Image {...args} fit="contain" alt="fit contain" /> | ||
| <span style={{ fontSize: 12, color: '#71717a' }}>fit: contain</span> | ||
| </div> | ||
| <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> | ||
| <Image {...args} fit="cover" alt="fit cover" /> | ||
| <span style={{ fontSize: 12, color: '#71717a' }}>fit: cover</span> | ||
| </div> | ||
| </div> | ||
| ), | ||
| args: { | ||
| src: 'https://picsum.photos/600/200', | ||
| width: 300, | ||
| height: 300, | ||
| }, | ||
| }; | ||
|
|
||
| export const ResponsiveWidth: Story = { | ||
| parameters: { | ||
| docs: { | ||
| description: { | ||
| story: 'width를 string(100%)으로 전달했을 때 부모 너비를 따라 반응형으로 동작하는 예시입니다.', | ||
| }, | ||
| }, | ||
| }, | ||
| render: (args) => ( | ||
| <div | ||
| style={{ | ||
| width: '100%', | ||
| maxWidth: 500, | ||
| minWidth: 220, | ||
| resize: 'horizontal', | ||
| overflow: 'auto', | ||
| border: '1px solid #e4e4e7', | ||
| padding: 8, | ||
| }} | ||
| > | ||
| <Image {...args} /> | ||
| </div> | ||
| ), | ||
| args: { | ||
| src: 'https://picsum.photos/400/300', | ||
| width: '100%', | ||
| height: 300, | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import { fireEvent, render, screen } from '@testing-library/react'; | ||
| import { describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| import { Image } from './Image'; | ||
|
|
||
| describe('Image', () => { | ||
| it('renders with required src and alt props', () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 테스트가 꼼꼼하네요 👍🏻 |
||
| render(<Image src="https://picsum.photos/400/300" alt="sample image" />); | ||
|
|
||
| const img = screen.getByRole('img', { name: 'sample image' }); | ||
| expect(img).toBeInTheDocument(); | ||
| expect(img).toHaveAttribute('src', 'https://picsum.photos/400/300'); | ||
| }); | ||
|
|
||
| it('applies width and height styles for number and string values', () => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 컴포넌트를 그래서 컴포넌트 자체보다 |
||
| render(<Image src="https://picsum.photos/400/300" alt="size test" width={320} height="50%" />); | ||
|
|
||
| const img = screen.getByRole('img', { name: 'size test' }); | ||
| expect(img).toHaveStyle({ | ||
| width: '320px', | ||
| height: '50%', | ||
| }); | ||
| }); | ||
|
|
||
| it('switches to fallbackSrc once when image load fails', () => { | ||
| render( | ||
| <Image | ||
| src="https://invalid-url.com/broken.jpg" | ||
| fallbackSrc="https://dummyimage.com/400x300/e5e7eb/111827&text=FALLBACK" | ||
| alt="fallback test" | ||
| />, | ||
| ); | ||
|
|
||
| const img = screen.getByRole('img', { name: 'fallback test' }); | ||
| fireEvent.error(img); | ||
|
|
||
| expect(img).toHaveAttribute('src', 'https://dummyimage.com/400x300/e5e7eb/111827&text=FALLBACK'); | ||
| }); | ||
|
|
||
| it('moves to error state and hides image when fallback is unavailable', () => { | ||
| render(<Image src="https://invalid-url.com/broken.jpg" alt="error test" width={400} height={300} />); | ||
|
|
||
| const img = screen.getByRole('img', { name: 'error test' }); | ||
| fireEvent.error(img); | ||
|
|
||
| expect(img).toHaveStyle({ visibility: 'hidden' }); | ||
| }); | ||
|
|
||
| it('shows placeholder while loading and hides it after load', () => { | ||
| render( | ||
| <Image | ||
| src="https://picsum.photos/400/300" | ||
| alt="placeholder test" | ||
| placeholder={ | ||
| <div data-testid="placeholder" style={{ width: 400, height: 300 }}> | ||
| loading... | ||
| </div> | ||
| } | ||
| />, | ||
| ); | ||
|
|
||
| const img = screen.getByAltText('placeholder test'); | ||
| expect(screen.getByTestId('placeholder')).toBeInTheDocument(); | ||
| expect(img).toHaveStyle({ visibility: 'hidden' }); | ||
|
|
||
| fireEvent.load(img); | ||
|
|
||
| expect(screen.queryByTestId('placeholder')).not.toBeInTheDocument(); | ||
| expect(img.style.visibility).toBe(''); | ||
| }); | ||
|
|
||
| it('applies fill styles when fill is true', () => { | ||
| render(<Image src="https://picsum.photos/400/300" alt="fill test" fill />); | ||
|
|
||
| const img = screen.getByRole('img', { name: 'fill test' }); | ||
| expect(img).toHaveStyle({ | ||
| position: 'absolute', | ||
| width: '100%', | ||
| height: '100%', | ||
| }); | ||
| }); | ||
|
|
||
| it('uses lazy loading by default', () => { | ||
| render(<Image src="https://picsum.photos/400/300" alt="loading attr test" />); | ||
|
|
||
| const img = screen.getByRole('img', { name: 'loading attr test' }); | ||
| expect(img).toHaveAttribute('loading', 'lazy'); | ||
| }); | ||
|
|
||
| it('always calls user onError callback', () => { | ||
| const onError = vi.fn(); | ||
| render(<Image src="https://invalid-url.com/broken.jpg" alt="onError test" onError={onError} />); | ||
|
|
||
| const img = screen.getByRole('img', { name: 'onError test' }); | ||
| fireEvent.error(img); | ||
|
|
||
| expect(onError).toHaveBeenCalledTimes(1); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 file이 혹시 필요한가요...?
필요한 지 검토 부탁드립니다.