Skip to content
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

Merged
merged 22 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7526c4d
feat: SnackbarProps 타입 생성
seocylucky Oct 30, 2024
a2e1a2c
feat: Snackbar.style.ts 생성
seocylucky Oct 30, 2024
bd7b682
feat: 마우스 드래그 및 터치 드래그 기능 훅인 useTouchMouseDrag 작업
seocylucky Oct 30, 2024
71263ed
feat: Snackbar, SnackbarProvider 작업
seocylucky Oct 30, 2024
7be5e65
feat: useSnackbar 작업
seocylucky Oct 30, 2024
a22180b
feat: export 작업
seocylucky Oct 30, 2024
586128f
docs: Snackbar 문서 작성
seocylucky Oct 30, 2024
62814f1
Merge remote-tracking branch 'origin/develop' into feat/#153-snackbar
seocylucky Oct 30, 2024
40f2c8f
fix: SnackbarWithoutClosingProps 타입 생성
seocylucky Oct 30, 2024
3a8416d
fix: 스낵바 하나만 생성하도록 수정
seocylucky Oct 30, 2024
3a2c4e5
docs: Overflow 시 테스트 추가, height 타입 테이블에서 제거
seocylucky Oct 30, 2024
ff4e890
refactor: 불필요한 스타일 코드 제거
seocylucky Oct 30, 2024
8cdfc01
Merge branch 'develop' into feat/#153-snackbar
seocylucky Oct 30, 2024
7139cec
fix: snackbar 배경 컬러 snackbar semantic color로 변경
seocylucky Oct 30, 2024
43d50c4
refactor: 관련 훅들 hooks로 이동
seocylucky Oct 31, 2024
bb5de21
fix: style type transient prop 처리, SnackbarProps 속 불필요한 $ 제외
seocylucky Oct 31, 2024
01a5ad2
fix: import/order lint에러 -> 절대경로로 변경
seocylucky Oct 31, 2024
08c7f4c
fix: full-width 관련 타입 변경 및 관련 스타일 수정
seocylucky Nov 1, 2024
3ea81f0
docs: prop 관련 스토리북 문서 수정
seocylucky Nov 1, 2024
27ba6ad
fix: 스낵바 스타일 관련 수정
seocylucky Nov 1, 2024
bb4859e
docs: type prop 예시 삭제
seocylucky Nov 2, 2024
6a732d2
docs: 불필요한 Story 삭제 및 대소문자 관련 수정
seocylucky Nov 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions src/components/Snackbar/Snackbar.mdx
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`입니다.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

모든 prop에 대한 설명이 사용법 섹션에 리스트로 들어가 있는데,
아래처럼 문서 내용을 좀 더 추가하는 건 어떨까요?

사용법 섹션

필수 프로퍼티인 message에 대한 내용만 남긴다
(두 줄 초과 시 말줄임표로 처리한다는 부분)

예시 섹션 (별도로 생성)

각 prop에 대한 구체적인 설명을 해당 위치로 분리

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 참고해서 수정 사항 반영 다 끝나면 수정해보겠습니다~

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정 완료했습니다~ prop 예시를 밑에 몇개 둘까했는데 스낵바 컴포넌트 특성상 일일이 예시 테스트 주기가 번거로울 것 같아서
위에 Control 있으니 설명만 넣었습니당 -> 3ea81f0


```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} />
143 changes: 143 additions & 0 deletions src/components/Snackbar/Snackbar.stories.tsx
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';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import/order 관련해서 에러/경고 줄이 몇 개 뜨던데, 혹시 저만 그런가요?? 제 린트 설정이 꼬인 건지 적용이 덜 된건지 잘 몰으겠네요.....................

image

Copy link
Member Author

@seocylucky seocylucky Oct 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

보니 저 코드가 상대경로로 되어 있어서 나는 린트 에러 같네여 절대경로로 다음처럼 바꿔놓을테니 확인 부탁드려요!!!
-> 01a5ad2

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',
},
};
128 changes: 128 additions & 0 deletions src/components/Snackbar/Snackbar.style.ts
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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

styled component prop 타입의 필드명엔 $를 붙이기로 했는데,
두 필드에선 붙이지 않은 이유가 따로 있을까요?

참고로 $를 붙이는 이유는 스타일을 위해 생성한 prop이 DOM까지 전달되는 것을 막기 위해서입니다! 지금 콘솔 경고가 뜨는 것도 이 이유에서에요

image

자세한 내용은 스타일드 컴포넌트 문서의 transient props 섹션에서 확인하실 수 있습니다!!


이 맥락에서 SnackbarProps 타입의 필드명에는 $를 떼는 게 덜 헷갈릴 거 같어용

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저것은..... 이유가 있습니다...
바로 저의 불찰 때문이죠.... 호호 반영 바로 해버렸습니다

이 맥락에서 SnackbarProps 타입의 필드명에는 $를 떼는 게 덜 헷갈릴 거 같어용

오 예리하네요 보리 반영했습니다ㅎㅎㅎㅎ -> 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;
Copy link
Collaborator

@nijuy nijuy Nov 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 padding 16이에요 24에요?..............
image

제가 왼쪽 보고 스타일 점검해가지고... 잠시 헷갈려서 댓글을 여러 번 고쳤는데 ^^... 미안합니다미안합니다
24가 맞다면 조용히 resolve 해주십시오

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}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

info일 때 typo ➡️ PC/Body/B3_Rg_14
error일 때 typo ➡️ PC/Body/B3_Sb_14

피그마에 비해 글자가 날씬하길래 보니까 타이포 스타일이 다르더라고요!

image

Suggested change
${({ theme }) => theme.typo.B3_Rg_14}
${({ $type, theme }) => ($type === 'info' ? theme.typo.B3_Rg_14 : theme.typo.B3_Sb_14)}

Copy link
Member Author

@seocylucky seocylucky Nov 1, 2024

Choose a reason for hiding this comment

The 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};
`;
Loading