-
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: SnackbarProps 타입 생성 * feat: Snackbar.style.ts 생성 * feat: 마우스 드래그 및 터치 드래그 기능 훅인 useTouchMouseDrag 작업 * feat: Snackbar, SnackbarProvider 작업 * feat: useSnackbar 작업 * feat: export 작업 * docs: Snackbar 문서 작성 * fix: SnackbarWithoutClosingProps 타입 생성 * fix: 스낵바 하나만 생성하도록 수정 * docs: Overflow 시 테스트 추가, height 타입 테이블에서 제거 * refactor: 불필요한 스타일 코드 제거 * fix: snackbar 배경 컬러 snackbar semantic color로 변경 * refactor: 관련 훅들 hooks로 이동 * fix: style type transient prop 처리, SnackbarProps 속 불필요한 $ 제외 * fix: import/order lint에러 -> 절대경로로 변경 * fix: full-width 관련 타입 변경 및 관련 스타일 수정 * docs: prop 관련 스토리북 문서 수정 * fix: 스낵바 스타일 관련 수정 * docs: type prop 예시 삭제 * docs: 불필요한 Story 삭제 및 대소문자 관련 수정
- Loading branch information
1 parent
6bba43b
commit 767e68a
Showing
10 changed files
with
576 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,106 @@ | ||
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`를 꼭 설정해주세요. | ||
|
||
이외의 프로퍼티들은 하단의 예시에서 확인할 수 있습니다. | ||
|
||
```tsx | ||
function App() { | ||
const { snackbar } = useSnackbar(); | ||
|
||
const handleShowSnackbar = () => { | ||
snackbar({ | ||
type: 'info', // Snackbar의 종류 | ||
width: '350px', // Snackbar의 가로 길 | ||
margin: '16px', // 왼쪽 오른쪽의 margin 값 | ||
message: '테스트용 스낵바입니다.', // Snackbar의 내용 | ||
duration: 3000, // Snackbar가 자동으로 닫히기 전까지의 시간(ms) | ||
position: 'center', // Snackbar의 위치 | ||
}); | ||
}; | ||
return ( | ||
<> | ||
<BoxButton size="small" hierarchy="primary" onClick={handleShowSnackbar}> | ||
Show Snackbar | ||
</BoxButton> | ||
</> | ||
); | ||
} | ||
``` | ||
|
||
4. Snackbar를 닫을 시, info 타입일 때는 `드래그`, error 타입일 때는 `X 버튼`을 클릭하여 Snackbar를 닫을 수 있습니다. | ||
|
||
<Canvas of={SnackbarStories.Type} /> | ||
|
||
5. Snackbar는 `최대 두 줄까지` 입력되며, 두 줄을 넘어설 시, `ellipsis` 처리됩니다. | ||
|
||
<Canvas of={SnackbarStories.OverflowTest} /> | ||
|
||
## 예시 | ||
|
||
### type | ||
|
||
`type` prop으로 Snackbar의 종류를 설정합니다. (info 또는 error) <br /> | ||
기본 값은 `info`입니다. | ||
|
||
### width | ||
|
||
`width` prop으로 원하는 Snackbar의 가로 길이를 설정합니다. (full-width, px, rem, em, %, vh, calc()) <br /> | ||
기본 값으로 글자 길이에 맞게 가로 길이가 정해집니다. | ||
|
||
`full-width`인 경우, 기본적으로 `양쪽 margin 16px`이 설정됩니다. 이 때, position 설정은 적용되지 않습니다. | ||
|
||
### margin | ||
|
||
`margin` prop으로 Snackbar의 왼쪽 오른쪽의 margin 값을 정해줍니다. <br /> | ||
기본 값은 `16px`입니다. | ||
|
||
### message | ||
|
||
Snackbar의 필수 프로퍼티로, Snackbar의 내용을 설정합니다. <br/ > | ||
|
||
### duration | ||
|
||
`duration` prop으로 Snackbar가 자동으로 닫히기까지의 시간을 설정합니다. (단위: `ms`) <br /> | ||
기본 값은 `5000`입니다. | ||
|
||
### position | ||
|
||
`position` prop으로 Snackbar의 위치를 설정합니다. (left, center, right) <br /> | ||
기본 값은 `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,116 @@ | ||
import { Meta, StoryObj } from '@storybook/react'; | ||
|
||
import { BoxButton } from '@/components/BoxButton'; | ||
|
||
import { Snackbar } from './Snackbar'; | ||
import { SnackbarProps } from './Snackbar.type'; | ||
import { SnackbarProvider } from './SnackbarProvider'; | ||
import { useSnackbar } from './hooks/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, rem, em, %, vh, calc())`, | ||
}, | ||
duration: { | ||
description: 'Snackbar가 자동으로 닫히기 전까지의 시간 (ms)', | ||
control: 'number', | ||
}, | ||
position: { | ||
description: 'Snackbar의 위치 (left, center, right)', | ||
control: { type: 'radio', options: ['left', 'center', 'right'] }, | ||
}, | ||
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 Type: 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', | ||
}, | ||
}; |
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,130 @@ | ||
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; | ||
$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; | ||
align-items: start; | ||
margin-left: ${margin || '16px'}; | ||
` | ||
) | ||
.with( | ||
'right', | ||
() => css` | ||
right: 0; | ||
align-items: end; | ||
margin-right: ${margin || '16px'}; | ||
` | ||
) | ||
.otherwise( | ||
() => css` | ||
left: 0; | ||
right: 0; | ||
margin: 0 auto; | ||
align-items: center; | ||
` | ||
); | ||
}; | ||
|
||
export const StyledSnackbarContainer = styled.div<Omit<StyledSnackbarProps, '$type' | 'visible'>>` | ||
position: fixed; | ||
bottom: 0; | ||
width: ${({ $width }) => ($width === 'full-width' ? '100%' : $width)}; | ||
height: fit-content; | ||
display: flex; | ||
flex-direction: column-reverse; | ||
margin: 0 auto; | ||
align-items: center; | ||
${({ $width, $position, $margin }) => | ||
$width !== 'full-width' && getPositionStyle($position, $margin)} | ||
`; | ||
|
||
export const StyledSnackbar = styled.div.withConfig({ | ||
shouldForwardProp: (prop) => prop !== 'isClosing', | ||
})<StyledSnackbarProps>` | ||
position: relative; | ||
padding: 16px; | ||
margin-bottom: 16px; | ||
width: ${({ $width }) => ($width === 'full-width' ? 'calc(100% - 32px)' : $width)}; | ||
height: ${({ $heightType }) => ($heightType === 2 ? '72px' : '52px')}; | ||
border-radius: ${({ theme }) => theme.semantic.radius.m}px; | ||
${({ $type, theme }) => ($type === 'info' ? theme.typo.B3_Rg_14 : theme.typo.B3_Sb_14)} | ||
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}; | ||
`; |
Oops, something went wrong.