-
Notifications
You must be signed in to change notification settings - Fork 1
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
feat: Snackbar 컴포넌트 구현 #162
Changes from 14 commits
7526c4d
a2e1a2c
bd7b682
71263ed
7be5e65
a22180b
586128f
62814f1
40f2c8f
3a8416d
3a2c4e5
ff4e890
8cdfc01
7139cec
43d50c4
bb5de21
01a5ad2
08c7f4c
3ea81f0
27ba6ad
bb4859e
6a732d2
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,78 @@ | ||
import { Canvas, Meta, Controls } from '@storybook/blocks'; | ||
import * as SnackbarStories from './Snackbar.stories.tsx'; | ||
import { Snackbar } from './Snackbar'; | ||
|
||
<Meta of={SnackbarStories} /> | ||
|
||
# Snackbar | ||
|
||
UI의 최하단에서 유저의 이용을 방해하지 않으면서 유저가 수행했거나 수행해야 할 작업에 대해 일시적으로 피드백을 제공합니다. | ||
|
||
<Canvas of={SnackbarStories.Test} /> | ||
<Controls /> | ||
|
||
<br /> | ||
<br /> | ||
|
||
## 사용법 | ||
|
||
Snackbar 컴포넌트의 기본 사용법입니다. | ||
|
||
1. Snackbar를 노출할 영역을 `SnackbarProvider`로 감싸줍니다. | ||
|
||
```tsx | ||
<YDSWrapper> | ||
<SnackbarProvider> | ||
<App /> | ||
</SnackbarProvider> | ||
</YDSWrapper> | ||
``` | ||
|
||
2. `useSnackbar` 훅을 사용하여 snackbar를 가져옵니다. | ||
|
||
```tsx | ||
const { snackbar } = useSnackbar(); | ||
``` | ||
|
||
3. Snackbar를 호출하는 함수를 만들어서 Snackbar를 노출합니다. | ||
|
||
필수 프로퍼티인 `message`를 꼭 설정해주세요. | ||
|
||
`type` - Snackbar의 종류 (info 또는 error). 기본 값은 `info`입니다. <br /> | ||
`width` - Snackbar의 가로 길이 (px, %, 등). 기본적으로 글자 길이에 맞게 가로 길이가 정해집니다. <br /> | ||
`margin` - 왼쪽 오른쪽의 margin 값. 기본 값은 `16px`입니다. <br /> | ||
`message` - Snackbar의 내용 (메시지). 필수 프로퍼티입니다. <br /> | ||
`duration` - Snackbar가 자동으로 닫히기 전까지의 시간(ms). 기본 값은 `5000`입니다. <br /> | ||
`position` - Snackbar의 위치 (left, center, right, full-width). 기본 값은 `center`입니다. | ||
|
||
```tsx | ||
function App() { | ||
const { snackbar } = useSnackbar(); | ||
|
||
const handleShowSnackbar = () => { | ||
snackbar({ | ||
type: 'info', // Snackbar의 종류 (info 또는 error) | ||
width: '350px', // Snackbar의 가로 길이 (px, %, 등) | ||
margin: '16px', // 왼쪽 오른쪽의 margin 값 | ||
message: '테스트용 스낵바입니다.', // Snackbar의 내용 (메시지) | ||
duration: 3000, // Snackbar가 자동으로 닫히기 전까지의 시간(ms) | ||
position: 'center', // Snackbar의 위치 (left, center, right, full-width) | ||
}); | ||
}; | ||
return ( | ||
<> | ||
<BoxButton size="small" hierarchy="primary" onClick={handleShowSnackbar}> | ||
Show Snackbar | ||
</BoxButton> | ||
</> | ||
); | ||
} | ||
``` | ||
|
||
4. Snackbar를 닫을 시, info 타입일 때는 `드래그`, error 타입일 때는 `X 버튼`을 클릭하여 Snackbar를 닫을 수 있습니다. | ||
|
||
<Canvas of={SnackbarStories.CloseTest} /> | ||
|
||
5. Snackbar는 `최대 두 줄까지` 입력되며, 두 줄을 넘어설 시, `ellipsis` 처리됩니다. | ||
|
||
<Canvas of={SnackbarStories.OverflowTest} /> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import { Meta, StoryObj } from '@storybook/react'; | ||
|
||
import { BoxButton } from '../BoxButton'; | ||
import { Snackbar } from './Snackbar'; | ||
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. 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. 보니 저 코드가 상대경로로 되어 있어서 나는 린트 에러 같네여 절대경로로 다음처럼 바꿔놓을테니 확인 부탁드려요!!! import { BoxButton } from '@/components/BoxButton'; |
||
import { SnackbarProps } from './Snackbar.type'; | ||
import { SnackbarProvider } from './SnackbarProvider'; | ||
import { useSnackbar } from './useSnackbar'; | ||
|
||
const meta: Meta<SnackbarProps> = { | ||
title: 'Components/Snackbar', | ||
component: Snackbar, | ||
args: { | ||
type: 'info', | ||
duration: 5000, | ||
margin: '16px', | ||
position: 'center', | ||
}, | ||
argTypes: { | ||
type: { | ||
description: 'Snackbar의 종류 (info 또는 error)', | ||
control: { type: 'radio', options: ['info', 'error'] }, | ||
}, | ||
width: { | ||
control: 'text', | ||
description: 'Snackbar의 가로 길이 (px, %, 등)', | ||
}, | ||
duration: { | ||
description: 'Snackbar가 자동으로 닫히기 전까지의 시간 (ms)', | ||
control: 'number', | ||
}, | ||
position: { | ||
description: 'Snackbar의 위치 (left, center, right, full-width)', | ||
control: { type: 'radio', options: ['left', 'center', 'right', 'full-width'] }, | ||
}, | ||
message: { | ||
control: 'text', | ||
description: 'Snackbar의 내용 (메시지)', | ||
}, | ||
onClose: { table: { disable: true } }, | ||
isClosing: { table: { disable: true } }, | ||
$heightType: { table: { disable: true } }, | ||
}, | ||
}; | ||
|
||
export default meta; | ||
type Story = StoryObj<SnackbarProps>; | ||
|
||
const SnackbarComponent = (args: SnackbarProps) => { | ||
const { snackbar } = useSnackbar(); | ||
|
||
const addSnackbar = () => { | ||
snackbar({ | ||
...args, | ||
message: args.message || '기본 메시지입니다.', | ||
}); | ||
}; | ||
|
||
const buttonLabel = args.type === 'error' ? 'Error Snackbar' : 'Info Snackbar'; | ||
|
||
return ( | ||
<div> | ||
<BoxButton size="small" hierarchy="primary" onClick={addSnackbar}> | ||
{buttonLabel} | ||
</BoxButton> | ||
</div> | ||
); | ||
}; | ||
|
||
export const Test: Story = { | ||
render: (args) => ( | ||
<SnackbarProvider> | ||
<SnackbarComponent {...args} /> | ||
</SnackbarProvider> | ||
), | ||
args: { | ||
type: 'info', | ||
position: 'center', | ||
width: '350px', | ||
message: '테스트용 스낵바입니다.', | ||
}, | ||
}; | ||
|
||
export const Info: Story = { | ||
render: (args) => ( | ||
<SnackbarProvider> | ||
<SnackbarComponent {...args} /> | ||
</SnackbarProvider> | ||
), | ||
args: { | ||
type: 'info', | ||
position: 'center', | ||
width: '350px', | ||
message: '정보성 메시지가 들어갑니다. 최대 2줄 입력 가능합니다.', | ||
}, | ||
}; | ||
|
||
export const Error: Story = { | ||
render: (args) => ( | ||
<SnackbarProvider> | ||
<SnackbarComponent {...args} /> | ||
</SnackbarProvider> | ||
), | ||
args: { | ||
type: 'error', | ||
message: '에러 메시지가 들어갑니다. 최대 2줄 입력 가능합니다.', | ||
width: '350px', | ||
position: 'center', | ||
}, | ||
}; | ||
|
||
export const CloseTest: Story = { | ||
render: (args) => ( | ||
<SnackbarProvider> | ||
<div style={{ display: 'flex', gap: '20px' }}> | ||
<SnackbarComponent {...args} type="info" message="정보성 메시지가 들어갑니다." /> | ||
<SnackbarComponent {...args} type="error" message="에러 메시지가 들어갑니다." /> | ||
</div> | ||
</SnackbarProvider> | ||
), | ||
args: { | ||
width: '350px', | ||
position: 'center', | ||
}, | ||
}; | ||
|
||
export const OverflowTest: Story = { | ||
render: (args) => ( | ||
<SnackbarProvider> | ||
<h3>두 줄 이상 입력 시</h3> | ||
<br /> | ||
<div style={{ display: 'flex', gap: '20px' }}> | ||
<SnackbarComponent {...args} type="info" /> | ||
<SnackbarComponent {...args} type="error" /> | ||
</div> | ||
</SnackbarProvider> | ||
), | ||
args: { | ||
message: | ||
'최대 2줄 입력 가능합니다. 입력 값이 넘칠 시, ellipsis 처리됩니다. 최대 2줄 입력 가능합니다. 입력 값이 넘칠 시, ellipsis 처리됩니다.', | ||
width: '350px', | ||
position: 'center', | ||
}, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import styled, { css } from 'styled-components'; | ||
import { DefaultTheme } from 'styled-components/dist/types'; | ||
import { match } from 'ts-pattern'; | ||
|
||
import { SnackbarHeightType, SnackbarPosition, SnackbarProps, SnackbarType } from './Snackbar.type'; | ||
|
||
interface StyledSnackbarProps { | ||
$type: 'info' | 'error'; | ||
$width: SnackbarProps['width']; | ||
$margin?: SnackbarProps['margin']; | ||
isClosing?: boolean; | ||
position: SnackbarPosition; | ||
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. styled component prop 타입의 필드명엔 참고로
이 맥락에서 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. 저것은..... 이유가 있습니다...
오 예리하네요 보리 반영했습니다ㅎㅎㅎㅎ -> bb5de21 |
||
$heightType?: SnackbarHeightType; | ||
} | ||
|
||
const getBackgroundStyle = (arg: { $type: SnackbarType; theme: DefaultTheme }) => { | ||
return match(arg) | ||
.with({ $type: 'error' }, ({ theme }) => theme.semantic.color.snackbarError) | ||
.otherwise(({ theme }) => theme.semantic.color.snackbarInfo); | ||
}; | ||
|
||
const getFontColorStyle = (arg: { $type: SnackbarType; theme: DefaultTheme }) => { | ||
return match(arg) | ||
.with({ $type: 'error' }, ({ theme }) => theme.semantic.color.textStatusNegative) | ||
.otherwise(() => arg.theme.semantic.color.textBasicWhite); | ||
}; | ||
|
||
const getPositionStyle = (position: SnackbarPosition, margin?: string) => { | ||
return match(position) | ||
.with( | ||
'left', | ||
() => css` | ||
left: 0; | ||
margin-left: ${margin || '16px'}; | ||
` | ||
) | ||
.with( | ||
'right', | ||
() => css` | ||
right: 0px; | ||
margin-right: ${margin || '16px'}; | ||
` | ||
) | ||
.otherwise( | ||
() => css` | ||
left: 0; | ||
right: 0; | ||
margin: 0 auto; | ||
align-items: center; | ||
` | ||
); | ||
}; | ||
|
||
export const StyledSnackbarContainer = styled.div< | ||
Omit<StyledSnackbarProps, '$type' | '$width' | 'visible'> | ||
>` | ||
position: fixed; | ||
bottom: 0; | ||
width: ${({ position }) => (position === 'full-width' ? '100%' : 'fit-content')}; | ||
height: fit-content; | ||
display: flex; | ||
flex-direction: column-reverse; | ||
margin: 0 auto; | ||
${({ position, $margin }) => getPositionStyle(position, $margin)} | ||
`; | ||
|
||
export const StyledSnackbar = styled.div.withConfig({ | ||
shouldForwardProp: (prop) => prop !== 'isClosing', | ||
})<StyledSnackbarProps>` | ||
position: relative; | ||
padding: 24px; | ||
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. |
||
margin-bottom: 16px; | ||
width: ${({ position, $width }) => (position === 'full-width' ? 'calc(100% - 32px)' : $width)}; | ||
height: ${({ $heightType }) => ($heightType === 2 ? '72px' : '52px')}; | ||
border-radius: ${({ theme }) => theme.semantic.radius.m}px; | ||
|
||
${({ theme }) => theme.typo.B3_Rg_14} | ||
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. 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. ㅋㅋㅋㅋㅋㅋㅋㅋㅋ아 대박 진정한 꼼꼼이는 보리 당신 꼼보...👍🏻 커밋 suggestion 할게영 -> 27ba6ad |
||
color: ${({ $type, theme }) => getFontColorStyle({ $type, theme })}; | ||
background-color: ${({ $type, theme }) => `${getBackgroundStyle({ $type, theme })}`}; | ||
|
||
display: flex; | ||
gap: 12px; | ||
align-items: center; | ||
justify-content: space-between; | ||
|
||
${({ isClosing }) => css` | ||
opacity: ${isClosing ? 0 : 1}; | ||
transform: ${isClosing ? 'translateY(100%)' : 'translateY(0)'}; | ||
transition: | ||
opacity 300ms ease-out, | ||
transform 300ms ease-out; | ||
animation: ${isClosing ? 'none' : 'slideIn 500ms ease-out'}; | ||
`} | ||
|
||
@keyframes slideIn { | ||
from { | ||
transform: translateY(100%); | ||
opacity: 0; | ||
} | ||
to { | ||
transform: translateY(0); | ||
opacity: 1; | ||
} | ||
} | ||
`; | ||
|
||
export const StyledIcMessage = styled.div` | ||
width: 100%; | ||
justify-content: space-between; | ||
align-items: flex-start; | ||
display: flex; | ||
gap: 8px; | ||
`; | ||
|
||
export const StyledMessage = styled.span` | ||
width: 100%; | ||
display: -webkit-box; | ||
-webkit-line-clamp: 2; | ||
-webkit-box-orient: vertical; | ||
overflow: hidden; | ||
text-overflow: ellipsis; | ||
`; | ||
|
||
export const StyledErrorIc = styled.div` | ||
height: 20px; | ||
cursor: pointer; | ||
color: ${({ theme }) => theme.semantic.color.iconBasicTertiary}; | ||
`; |
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.
모든 prop에 대한 설명이
사용법
섹션에 리스트로 들어가 있는데,아래처럼 문서 내용을 좀 더 추가하는 건 어떨까요?
사용법
섹션필수 프로퍼티인
message
에 대한 내용만 남긴다(두 줄 초과 시 말줄임표로 처리한다는 부분)
예시
섹션 (별도로 생성)각 prop에 대한 구체적인 설명을 해당 위치로 분리
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.
이 부분 참고해서 수정 사항 반영 다 끝나면 수정해보겠습니다~
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.
수정 완료했습니다~ prop 예시를 밑에 몇개 둘까했는데 스낵바 컴포넌트 특성상 일일이 예시 테스트 주기가 번거로울 것 같아서
위에 Control 있으니 설명만 넣었습니당 -> 3ea81f0