From 7526c4d23a238427a82ea032ef54f3c874089666 Mon Sep 17 00:00:00 2001 From: seocylucky Date: Wed, 30 Oct 2024 20:04:25 +0900 Subject: [PATCH 01/20] =?UTF-8?q?feat:=20SnackbarProps=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/Snackbar.type.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/components/Snackbar/Snackbar.type.ts diff --git a/src/components/Snackbar/Snackbar.type.ts b/src/components/Snackbar/Snackbar.type.ts new file mode 100644 index 0000000..08c9154 --- /dev/null +++ b/src/components/Snackbar/Snackbar.type.ts @@ -0,0 +1,16 @@ +export type SnackbarType = 'info' | 'error'; +export type SnackbarHeightType = 1 | 2; +export type SnackbarPosition = 'left' | 'center' | 'right' | 'full-width'; + +export interface SnackbarProps { + id: string; + type?: SnackbarType; + width?: string; + margin?: string; + message: string; + onClose?: (id: string) => void; + duration?: number; + position?: SnackbarPosition; + isClosing: boolean; + $heightType?: SnackbarHeightType; +} From a2e1a2c5e1828fe62d7fe142831ab4dbf9238e4f Mon Sep 17 00:00:00 2001 From: seocylucky Date: Wed, 30 Oct 2024 20:09:17 +0900 Subject: [PATCH 02/20] =?UTF-8?q?feat:=20Snackbar.style.ts=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/Snackbar.style.ts | 135 ++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/components/Snackbar/Snackbar.style.ts diff --git a/src/components/Snackbar/Snackbar.style.ts b/src/components/Snackbar/Snackbar.style.ts new file mode 100644 index 0000000..36f443a --- /dev/null +++ b/src/components/Snackbar/Snackbar.style.ts @@ -0,0 +1,135 @@ +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 = ($type: SnackbarType) => { + return match($type) + .with('error', () => '#FFEBEB') + .otherwise(() => '#3F434D'); +}; + +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 +>` + 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', +})` + position: relative; + padding: 24px; + 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} + color: ${({ $type, theme }) => getFontColorStyle({ $type, theme })}; + background-color: ${({ $type }) => `${getBackgroundStyle($type)}`}; + + 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; + } + } + + &:not(:first-child) { + margin-bottom: 8px; + } + + &:first-child { + margin-bottom: 16px; + } +`; + +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}; +`; From bd7b682bea3b7f081e5ed177dd2c8fa7ddddc624 Mon Sep 17 00:00:00 2001 From: seocylucky Date: Wed, 30 Oct 2024 20:10:05 +0900 Subject: [PATCH 03/20] =?UTF-8?q?feat:=20=EB=A7=88=EC=9A=B0=EC=8A=A4=20?= =?UTF-8?q?=EB=93=9C=EB=9E=98=EA=B7=B8=20=EB=B0=8F=20=ED=84=B0=EC=B9=98=20?= =?UTF-8?q?=EB=93=9C=EB=9E=98=EA=B7=B8=20=EA=B8=B0=EB=8A=A5=20=ED=9B=85?= =?UTF-8?q?=EC=9D=B8=20useTouchMouseDrag=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/useMouseTouchDrag.ts | 56 ++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/components/Snackbar/useMouseTouchDrag.ts diff --git a/src/components/Snackbar/useMouseTouchDrag.ts b/src/components/Snackbar/useMouseTouchDrag.ts new file mode 100644 index 0000000..491ba51 --- /dev/null +++ b/src/components/Snackbar/useMouseTouchDrag.ts @@ -0,0 +1,56 @@ +import { useState, useRef, useEffect } from 'react'; + +export const useTouchMouseDrag = (onDismiss: () => void, threshold: number = 10) => { + const [startY, setStartY] = useState(null); + const [isDragging, setIsDragging] = useState(false); + const elementRef = useRef(null); + + const handleStart = (event: TouchEvent | MouseEvent) => { + const y = event instanceof TouchEvent ? event.touches[0].clientY : event.clientY; + setStartY(y); + setIsDragging(true); + }; + + const handleMove = (event: TouchEvent | MouseEvent) => { + if (!isDragging || startY === null) return; + + const currentY = event instanceof TouchEvent ? event.touches[0].clientY : event.clientY; + const diffY = currentY - startY; + + if (diffY > threshold) { + onDismiss(); + setIsDragging(false); + setStartY(null); + } + }; + + const handleEnd = () => { + setIsDragging(false); + setStartY(null); + }; + + useEffect(() => { + const element = elementRef.current; + if (element) { + element.addEventListener('mousedown', handleStart); + window.addEventListener('mousemove', handleMove); + window.addEventListener('mouseup', handleEnd); + + element.addEventListener('touchstart', handleStart); + element.addEventListener('touchmove', handleMove); + element.addEventListener('touchend', handleEnd); + + return () => { + element.removeEventListener('mousedown', handleStart); + window.removeEventListener('mousemove', handleMove); + window.removeEventListener('mouseup', handleEnd); + + element.removeEventListener('touchstart', handleStart); + element.removeEventListener('touchmove', handleMove); + element.removeEventListener('touchend', handleEnd); + }; + } + }, [isDragging, startY]); + + return elementRef; +}; From 71263edb3fe5737ccb81d593b6f3ee8f50b76982 Mon Sep 17 00:00:00 2001 From: seocylucky Date: Wed, 30 Oct 2024 20:12:00 +0900 Subject: [PATCH 04/20] =?UTF-8?q?feat:=20Snackbar,=20SnackbarProvider=20?= =?UTF-8?q?=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/Snackbar.tsx | 80 ++++++++++++++++++++ src/components/Snackbar/SnackbarProvider.tsx | 67 ++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 src/components/Snackbar/Snackbar.tsx create mode 100644 src/components/Snackbar/SnackbarProvider.tsx diff --git a/src/components/Snackbar/Snackbar.tsx b/src/components/Snackbar/Snackbar.tsx new file mode 100644 index 0000000..c5d258a --- /dev/null +++ b/src/components/Snackbar/Snackbar.tsx @@ -0,0 +1,80 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { IcAlertTriangleFilled, IcCloseFilled } from '@/style'; + +import { StyledErrorIc, StyledIcMessage, StyledMessage, StyledSnackbar } from './Snackbar.style'; +import { SnackbarHeightType, SnackbarProps } from './Snackbar.type'; +import { useTouchMouseDrag } from './useMouseTouchDrag'; + +export const Snackbar = ({ + id, + type = 'info', + width, + margin, + message, + onClose, + duration = 5000, + position = 'center', + isClosing: initialIsClosing, +}: SnackbarProps) => { + const messageRef = useRef(null); + const [heightType, setHeightType] = useState(1); + const [isClosing, setIsClosing] = useState(initialIsClosing); + + const closeToast = useCallback(() => { + if (!isClosing && onClose) { + setIsClosing(true); + setTimeout(() => onClose(id), 300); + } + }, [isClosing, onClose, id]); + + useEffect(() => { + const timeout = setTimeout(() => { + closeToast(); + }, duration); + + return () => clearTimeout(timeout); + }, [duration, closeToast]); + + useEffect(() => { + if (messageRef.current) { + const messageHeight = messageRef.current.clientHeight; + const lineHeight = parseInt(window.getComputedStyle(messageRef.current).lineHeight, 10); + const isMultiLine = messageHeight > lineHeight; + setHeightType(isMultiLine ? 2 : 1); + } + }, [message]); + + const handleAnimationEnd = () => { + if (isClosing && onClose) { + onClose(id); + } + }; + + const snackbarRef = useTouchMouseDrag(() => { + closeToast(); + }); + + return ( + + + {type === 'error' && } + {message} + {type === 'error' && ( + + + + )} + + + ); +}; diff --git a/src/components/Snackbar/SnackbarProvider.tsx b/src/components/Snackbar/SnackbarProvider.tsx new file mode 100644 index 0000000..3e27773 --- /dev/null +++ b/src/components/Snackbar/SnackbarProvider.tsx @@ -0,0 +1,67 @@ +import { createContext, PropsWithChildren, useCallback, useContext, useState } from 'react'; + +import ReactDOM from 'react-dom'; + +import { Snackbar } from './Snackbar'; +import { StyledSnackbarContainer } from './Snackbar.style'; +import { SnackbarProps } from './Snackbar.type'; + +type SnackbarContextType = { + showSnackbar: (props: Omit) => void; +}; + +const SnackbarContext = createContext({ showSnackbar: () => {} }); + +export const SnackbarProvider = ({ children }: PropsWithChildren) => { + const [snackbars, setSnackbars] = useState([]); + const MAX_SNACKBARS = 5; + + const showSnackbar = useCallback((props: Omit) => { + setSnackbars((prevSnackbars) => { + if (prevSnackbars.length >= MAX_SNACKBARS) { + return prevSnackbars; + } + const newSnackbar = { ...props, id: `snackbar-${Date.now()}`, isClosing: false }; + return [...prevSnackbars, newSnackbar]; + }); + }, []); + + const handleRemoveSnackbar = useCallback((id: string) => { + setSnackbars((prevSnackbars) => prevSnackbars.filter((snackbar) => snackbar.id !== id)); + }, []); + + const snackbarList = snackbars.map((snackbar) => ( + + )); + + return ( + + {children} + {ReactDOM.createPortal( + + {snackbarList} + , + document.body + )} + + ); +}; + +export const useSnackbarContext = () => { + const context = useContext(SnackbarContext); + if (!context) { + throw new Error('useSnackbar must be used within a SnackbarProvider'); + } + return context; +}; From 7be5e65ef33ab491b73c18e32de21682b8bde64f Mon Sep 17 00:00:00 2001 From: seocylucky Date: Wed, 30 Oct 2024 20:12:16 +0900 Subject: [PATCH 05/20] =?UTF-8?q?feat:=20useSnackbar=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/useSnackbar.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/components/Snackbar/useSnackbar.tsx diff --git a/src/components/Snackbar/useSnackbar.tsx b/src/components/Snackbar/useSnackbar.tsx new file mode 100644 index 0000000..9834f8a --- /dev/null +++ b/src/components/Snackbar/useSnackbar.tsx @@ -0,0 +1,12 @@ +import { SnackbarProps } from './Snackbar.type'; +import { useSnackbarContext } from './SnackbarProvider'; + +export const useSnackbar = () => { + const { showSnackbar } = useSnackbarContext(); + + const snackbar = (props: Omit) => { + showSnackbar(props); + }; + + return { snackbar }; +}; From a22180bd76b90740ea4a415b4c0545906da1c0e9 Mon Sep 17 00:00:00 2001 From: seocylucky Date: Wed, 30 Oct 2024 20:12:45 +0900 Subject: [PATCH 06/20] =?UTF-8?q?feat:=20export=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/index.ts | 3 +++ src/components/index.ts | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 src/components/Snackbar/index.ts diff --git a/src/components/Snackbar/index.ts b/src/components/Snackbar/index.ts new file mode 100644 index 0000000..5799747 --- /dev/null +++ b/src/components/Snackbar/index.ts @@ -0,0 +1,3 @@ +export { SnackbarProvider } from './SnackbarProvider'; +export { useSnackbar } from './useSnackbar'; +export type * from './Snackbar.type'; diff --git a/src/components/index.ts b/src/components/index.ts index f815e69..db8f9dd 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -50,4 +50,8 @@ export { TextField } from './TextField'; export type { TextFieldProps } from './TextField'; export { Switch } from './Switch'; -export type { SwitchProps } from './Switch'; \ No newline at end of file +export type { SwitchProps } from './Switch'; + +export { SnackbarProvider } from './Snackbar/SnackbarProvider'; +export { useSnackbar } from './Snackbar/useSnackbar'; +export type * from './Snackbar'; From 586128f5fb97bf69d78744f2f0dce4bff1eae2f9 Mon Sep 17 00:00:00 2001 From: seocylucky Date: Wed, 30 Oct 2024 20:13:19 +0900 Subject: [PATCH 07/20] =?UTF-8?q?docs:=20Snackbar=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/Snackbar.mdx | 74 +++++++++++ src/components/Snackbar/Snackbar.stories.tsx | 125 +++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 src/components/Snackbar/Snackbar.mdx create mode 100644 src/components/Snackbar/Snackbar.stories.tsx diff --git a/src/components/Snackbar/Snackbar.mdx b/src/components/Snackbar/Snackbar.mdx new file mode 100644 index 0000000..cff6ecd --- /dev/null +++ b/src/components/Snackbar/Snackbar.mdx @@ -0,0 +1,74 @@ +import { Canvas, Meta, Controls } from '@storybook/blocks'; +import * as SnackbarStories from './Snackbar.stories.tsx'; +import { Snackbar } from './Snackbar'; + + + +# Snackbar + +UI의 최하단에서 유저의 이용을 방해하지 않으면서 유저가 수행했거나 수행해야 할 작업에 대해 일시적으로 피드백을 제공합니다. + + + + +
+
+ +## 사용법 + +Snackbar 컴포넌트의 기본 사용법입니다. + +1. Snackbar를 노출할 영역을 `SnackbarProvider`로 감싸줍니다. + +```tsx + + + + + +``` + +2. `useSnackbar` 훅을 사용하여 snackbar를 가져옵니다. + +```tsx +const { snackbar } = useSnackbar(); +``` + +3. Snackbar를 호출하는 함수를 만들어서 Snackbar를 노출합니다. + +필수 프로퍼티인 `message`를 꼭 설정해주세요. + +`type` - Snackbar의 종류 (info 또는 error). 기본 값은 `info`입니다.
+`width` - Snackbar의 가로 길이 (px, %, 등). 기본적으로 글자 길이에 맞게 가로 길이가 정해집니다.
+`margin` - 왼쪽 오른쪽의 margin 값. 기본 값은 `16px`입니다.
+`message` - Snackbar의 내용 (메시지). 필수 프로퍼티입니다.
+`duration` - Snackbar가 자동으로 닫히기 전까지의 시간(ms). 기본 값은 `5000`입니다.
+`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 ( + <> + + Show Snackbar + + + ); +} +``` + +4. Snackbar를 닫을 시, info 타입일 때는 `드래그`, error 타입일 때는 `X 버튼`을 클릭하여 Snackbar를 닫을 수 있습니다. + + diff --git a/src/components/Snackbar/Snackbar.stories.tsx b/src/components/Snackbar/Snackbar.stories.tsx new file mode 100644 index 0000000..5eb2b32 --- /dev/null +++ b/src/components/Snackbar/Snackbar.stories.tsx @@ -0,0 +1,125 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { BoxButton } from '../BoxButton'; + +import { Snackbar } from './Snackbar'; +import { SnackbarProps } from './Snackbar.type'; +import { SnackbarProvider } from './SnackbarProvider'; +import { useSnackbar } from './useSnackbar'; + +const meta: Meta = { + 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의 내용 (메시지)', + }, + id: { table: { disable: true } }, + onClose: { table: { disable: true } }, + isClosing: { table: { disable: true } }, + }, +}; + +export default meta; +type Story = StoryObj; + +const SnackbarComponent = (args: Partial) => { + const { snackbar } = useSnackbar(); + + const addSnackbar = () => { + snackbar({ + ...args, + message: args.message || '기본 메시지입니다.', + }); + }; + + const buttonLabel = args.type === 'error' ? 'Error Snackbar' : 'Info Snackbar'; + + return ( +
+ + {buttonLabel} + +
+ ); +}; + +export const Test: Story = { + render: (args) => ( + + + + ), + args: { + type: 'info', + position: 'center', + width: '350px', + message: '테스트용 스낵바입니다.', + }, +}; + +export const Info: Story = { + render: (args) => ( + + + + ), + args: { + type: 'info', + position: 'center', + width: '350px', + message: '정보성 메시지가 들어갑니다. 최대 2줄 입력 가능합니다.', + }, +}; + +export const Error: Story = { + render: (args) => ( + + + + ), + args: { + type: 'error', + message: '에러 메시지가 들어갑니다. 최대 2줄 입력 가능합니다.', + width: '350px', + position: 'center', + }, +}; + +export const CloseTest: Story = { + render: (args) => ( + +
+ + +
+
+ ), + args: { + width: '350px', + position: 'center', + }, +}; From 40f2c8f29378d52340c43cbad72fbfeff284c622 Mon Sep 17 00:00:00 2001 From: seocylucky Date: Thu, 31 Oct 2024 01:44:09 +0900 Subject: [PATCH 08/20] =?UTF-8?q?fix:=20SnackbarWithoutClosingProps=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/Snackbar.type.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/Snackbar/Snackbar.type.ts b/src/components/Snackbar/Snackbar.type.ts index 08c9154..0c65f87 100644 --- a/src/components/Snackbar/Snackbar.type.ts +++ b/src/components/Snackbar/Snackbar.type.ts @@ -3,14 +3,15 @@ export type SnackbarHeightType = 1 | 2; export type SnackbarPosition = 'left' | 'center' | 'right' | 'full-width'; export interface SnackbarProps { - id: string; type?: SnackbarType; width?: string; margin?: string; message: string; - onClose?: (id: string) => void; + onClose?: () => void; duration?: number; position?: SnackbarPosition; isClosing: boolean; $heightType?: SnackbarHeightType; } + +export type SnackbarWithoutClosingProps = Omit; From 3a8416db83168aae1b5749a0575d07c9c3c756bb Mon Sep 17 00:00:00 2001 From: seocylucky Date: Thu, 31 Oct 2024 01:45:11 +0900 Subject: [PATCH 09/20] =?UTF-8?q?fix:=20=EC=8A=A4=EB=82=B5=EB=B0=94=20?= =?UTF-8?q?=ED=95=98=EB=82=98=EB=A7=8C=20=EC=83=9D=EC=84=B1=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/Snackbar.tsx | 14 ++--- src/components/Snackbar/SnackbarProvider.tsx | 54 ++++++-------------- src/components/Snackbar/useSnackbar.tsx | 4 +- 3 files changed, 22 insertions(+), 50 deletions(-) diff --git a/src/components/Snackbar/Snackbar.tsx b/src/components/Snackbar/Snackbar.tsx index c5d258a..060dfe9 100644 --- a/src/components/Snackbar/Snackbar.tsx +++ b/src/components/Snackbar/Snackbar.tsx @@ -7,7 +7,6 @@ import { SnackbarHeightType, SnackbarProps } from './Snackbar.type'; import { useTouchMouseDrag } from './useMouseTouchDrag'; export const Snackbar = ({ - id, type = 'info', width, margin, @@ -24,9 +23,9 @@ export const Snackbar = ({ const closeToast = useCallback(() => { if (!isClosing && onClose) { setIsClosing(true); - setTimeout(() => onClose(id), 300); + setTimeout(() => onClose(), 300); } - }, [isClosing, onClose, id]); + }, [isClosing, onClose]); useEffect(() => { const timeout = setTimeout(() => { @@ -45,24 +44,17 @@ export const Snackbar = ({ } }, [message]); - const handleAnimationEnd = () => { - if (isClosing && onClose) { - onClose(id); - } - }; - const snackbarRef = useTouchMouseDrag(() => { closeToast(); }); return ( diff --git a/src/components/Snackbar/SnackbarProvider.tsx b/src/components/Snackbar/SnackbarProvider.tsx index 3e27773..746b60e 100644 --- a/src/components/Snackbar/SnackbarProvider.tsx +++ b/src/components/Snackbar/SnackbarProvider.tsx @@ -1,59 +1,39 @@ import { createContext, PropsWithChildren, useCallback, useContext, useState } from 'react'; - import ReactDOM from 'react-dom'; - import { Snackbar } from './Snackbar'; import { StyledSnackbarContainer } from './Snackbar.style'; -import { SnackbarProps } from './Snackbar.type'; +import { SnackbarWithoutClosingProps } from './Snackbar.type'; type SnackbarContextType = { - showSnackbar: (props: Omit) => void; + showSnackbar: (props: SnackbarWithoutClosingProps) => void; }; const SnackbarContext = createContext({ showSnackbar: () => {} }); export const SnackbarProvider = ({ children }: PropsWithChildren) => { - const [snackbars, setSnackbars] = useState([]); - const MAX_SNACKBARS = 5; + const [snackbar, setSnackbar] = useState(null); + const [isClosing, setIsClosing] = useState(false); - const showSnackbar = useCallback((props: Omit) => { - setSnackbars((prevSnackbars) => { - if (prevSnackbars.length >= MAX_SNACKBARS) { - return prevSnackbars; - } - const newSnackbar = { ...props, id: `snackbar-${Date.now()}`, isClosing: false }; - return [...prevSnackbars, newSnackbar]; - }); + const showSnackbar = useCallback((props: SnackbarWithoutClosingProps) => { + setSnackbar(props); + setIsClosing(false); }, []); - const handleRemoveSnackbar = useCallback((id: string) => { - setSnackbars((prevSnackbars) => prevSnackbars.filter((snackbar) => snackbar.id !== id)); + const removeSnackbar = useCallback(() => { + setIsClosing(true); + setTimeout(() => setSnackbar(null), 300); }, []); - const snackbarList = snackbars.map((snackbar) => ( - - )); - return ( {children} - {ReactDOM.createPortal( - - {snackbarList} - , - document.body - )} + {snackbar && + ReactDOM.createPortal( + + + , + document.body + )} ); }; diff --git a/src/components/Snackbar/useSnackbar.tsx b/src/components/Snackbar/useSnackbar.tsx index 9834f8a..237d8b3 100644 --- a/src/components/Snackbar/useSnackbar.tsx +++ b/src/components/Snackbar/useSnackbar.tsx @@ -1,10 +1,10 @@ -import { SnackbarProps } from './Snackbar.type'; +import { SnackbarWithoutClosingProps } from './Snackbar.type'; import { useSnackbarContext } from './SnackbarProvider'; export const useSnackbar = () => { const { showSnackbar } = useSnackbarContext(); - const snackbar = (props: Omit) => { + const snackbar = (props: SnackbarWithoutClosingProps) => { showSnackbar(props); }; From 3a2c4e5ea015bcced34ff53d4e4f5b3edecf8369 Mon Sep 17 00:00:00 2001 From: seocylucky Date: Thu, 31 Oct 2024 01:46:26 +0900 Subject: [PATCH 10/20] =?UTF-8?q?docs:=20Overflow=20=EC=8B=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80,=20height=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=ED=85=8C=EC=9D=B4=EB=B8=94=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/Snackbar.mdx | 4 ++++ src/components/Snackbar/Snackbar.stories.tsx | 24 +++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/components/Snackbar/Snackbar.mdx b/src/components/Snackbar/Snackbar.mdx index cff6ecd..e7df613 100644 --- a/src/components/Snackbar/Snackbar.mdx +++ b/src/components/Snackbar/Snackbar.mdx @@ -72,3 +72,7 @@ function App() { 4. Snackbar를 닫을 시, info 타입일 때는 `드래그`, error 타입일 때는 `X 버튼`을 클릭하여 Snackbar를 닫을 수 있습니다. + +5. Snackbar는 `최대 두 줄까지` 입력되며, 두 줄을 넘어설 시, `ellipsis` 처리됩니다. + + diff --git a/src/components/Snackbar/Snackbar.stories.tsx b/src/components/Snackbar/Snackbar.stories.tsx index 5eb2b32..10127c6 100644 --- a/src/components/Snackbar/Snackbar.stories.tsx +++ b/src/components/Snackbar/Snackbar.stories.tsx @@ -1,7 +1,6 @@ import { Meta, StoryObj } from '@storybook/react'; import { BoxButton } from '../BoxButton'; - import { Snackbar } from './Snackbar'; import { SnackbarProps } from './Snackbar.type'; import { SnackbarProvider } from './SnackbarProvider'; @@ -37,16 +36,16 @@ const meta: Meta = { control: 'text', description: 'Snackbar의 내용 (메시지)', }, - id: { table: { disable: true } }, onClose: { table: { disable: true } }, isClosing: { table: { disable: true } }, + $heightType: { table: { disable: true } }, }, }; export default meta; type Story = StoryObj; -const SnackbarComponent = (args: Partial) => { +const SnackbarComponent = (args: SnackbarProps) => { const { snackbar } = useSnackbar(); const addSnackbar = () => { @@ -123,3 +122,22 @@ export const CloseTest: Story = { position: 'center', }, }; + +export const OverflowTest: Story = { + render: (args) => ( + +

두 줄 이상 입력 시

+
+
+ + +
+
+ ), + args: { + message: + '최대 2줄 입력 가능합니다. 입력 값이 넘칠 시, ellipsis 처리됩니다. 최대 2줄 입력 가능합니다. 입력 값이 넘칠 시, ellipsis 처리됩니다.', + width: '350px', + position: 'center', + }, +}; From ff4e890ee7e2356ca8f5f4dee7acb6c18f46746b Mon Sep 17 00:00:00 2001 From: seocylucky Date: Thu, 31 Oct 2024 01:56:50 +0900 Subject: [PATCH 11/20] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/Snackbar.style.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/Snackbar/Snackbar.style.ts b/src/components/Snackbar/Snackbar.style.ts index 36f443a..0985c92 100644 --- a/src/components/Snackbar/Snackbar.style.ts +++ b/src/components/Snackbar/Snackbar.style.ts @@ -69,6 +69,7 @@ export const StyledSnackbar = styled.div.withConfig({ })` position: relative; padding: 24px; + 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; @@ -101,14 +102,6 @@ export const StyledSnackbar = styled.div.withConfig({ opacity: 1; } } - - &:not(:first-child) { - margin-bottom: 8px; - } - - &:first-child { - margin-bottom: 16px; - } `; export const StyledIcMessage = styled.div` From 7139cec9d213bea4afcd0838ef921924c4cce097 Mon Sep 17 00:00:00 2001 From: seocylucky Date: Thu, 31 Oct 2024 02:42:57 +0900 Subject: [PATCH 12/20] =?UTF-8?q?fix:=20snackbar=20=EB=B0=B0=EA=B2=BD=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=AC=20snackbar=20semantic=20color=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/Snackbar.style.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Snackbar/Snackbar.style.ts b/src/components/Snackbar/Snackbar.style.ts index 0985c92..e077ccf 100644 --- a/src/components/Snackbar/Snackbar.style.ts +++ b/src/components/Snackbar/Snackbar.style.ts @@ -13,10 +13,10 @@ interface StyledSnackbarProps { $heightType?: SnackbarHeightType; } -const getBackgroundStyle = ($type: SnackbarType) => { - return match($type) - .with('error', () => '#FFEBEB') - .otherwise(() => '#3F434D'); +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 }) => { @@ -76,7 +76,7 @@ export const StyledSnackbar = styled.div.withConfig({ ${({ theme }) => theme.typo.B3_Rg_14} color: ${({ $type, theme }) => getFontColorStyle({ $type, theme })}; - background-color: ${({ $type }) => `${getBackgroundStyle($type)}`}; + background-color: ${({ $type, theme }) => `${getBackgroundStyle({ $type, theme })}`}; display: flex; gap: 12px; From 43d50c4ce14c0c39d92b9174fe3c2b93d745f18a Mon Sep 17 00:00:00 2001 From: seocylucky Date: Fri, 1 Nov 2024 00:03:23 +0900 Subject: [PATCH 13/20] =?UTF-8?q?refactor:=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=9B=85=EB=93=A4=20hooks=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/Snackbar.stories.tsx | 2 +- src/components/Snackbar/Snackbar.tsx | 2 +- src/components/Snackbar/{ => hooks}/useMouseTouchDrag.ts | 0 src/components/Snackbar/{ => hooks}/useSnackbar.tsx | 4 ++-- src/components/Snackbar/index.ts | 2 +- src/components/index.ts | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) rename src/components/Snackbar/{ => hooks}/useMouseTouchDrag.ts (100%) rename src/components/Snackbar/{ => hooks}/useSnackbar.tsx (62%) diff --git a/src/components/Snackbar/Snackbar.stories.tsx b/src/components/Snackbar/Snackbar.stories.tsx index 10127c6..0994394 100644 --- a/src/components/Snackbar/Snackbar.stories.tsx +++ b/src/components/Snackbar/Snackbar.stories.tsx @@ -4,7 +4,7 @@ import { BoxButton } from '../BoxButton'; import { Snackbar } from './Snackbar'; import { SnackbarProps } from './Snackbar.type'; import { SnackbarProvider } from './SnackbarProvider'; -import { useSnackbar } from './useSnackbar'; +import { useSnackbar } from './hooks/useSnackbar'; const meta: Meta = { title: 'Components/Snackbar', diff --git a/src/components/Snackbar/Snackbar.tsx b/src/components/Snackbar/Snackbar.tsx index 060dfe9..d8d2ea5 100644 --- a/src/components/Snackbar/Snackbar.tsx +++ b/src/components/Snackbar/Snackbar.tsx @@ -4,7 +4,7 @@ import { IcAlertTriangleFilled, IcCloseFilled } from '@/style'; import { StyledErrorIc, StyledIcMessage, StyledMessage, StyledSnackbar } from './Snackbar.style'; import { SnackbarHeightType, SnackbarProps } from './Snackbar.type'; -import { useTouchMouseDrag } from './useMouseTouchDrag'; +import { useTouchMouseDrag } from './hooks/useMouseTouchDrag'; export const Snackbar = ({ type = 'info', diff --git a/src/components/Snackbar/useMouseTouchDrag.ts b/src/components/Snackbar/hooks/useMouseTouchDrag.ts similarity index 100% rename from src/components/Snackbar/useMouseTouchDrag.ts rename to src/components/Snackbar/hooks/useMouseTouchDrag.ts diff --git a/src/components/Snackbar/useSnackbar.tsx b/src/components/Snackbar/hooks/useSnackbar.tsx similarity index 62% rename from src/components/Snackbar/useSnackbar.tsx rename to src/components/Snackbar/hooks/useSnackbar.tsx index 237d8b3..d47e6b5 100644 --- a/src/components/Snackbar/useSnackbar.tsx +++ b/src/components/Snackbar/hooks/useSnackbar.tsx @@ -1,5 +1,5 @@ -import { SnackbarWithoutClosingProps } from './Snackbar.type'; -import { useSnackbarContext } from './SnackbarProvider'; +import { SnackbarWithoutClosingProps } from '../Snackbar.type'; +import { useSnackbarContext } from '../SnackbarProvider'; export const useSnackbar = () => { const { showSnackbar } = useSnackbarContext(); diff --git a/src/components/Snackbar/index.ts b/src/components/Snackbar/index.ts index 5799747..396ee3f 100644 --- a/src/components/Snackbar/index.ts +++ b/src/components/Snackbar/index.ts @@ -1,3 +1,3 @@ export { SnackbarProvider } from './SnackbarProvider'; -export { useSnackbar } from './useSnackbar'; +export { useSnackbar } from './hooks/useSnackbar'; export type * from './Snackbar.type'; diff --git a/src/components/index.ts b/src/components/index.ts index 5090a78..94d7216 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -56,5 +56,5 @@ export { Divider } from './Divider'; export type { DividerProps } from './Divider'; export { SnackbarProvider } from './Snackbar/SnackbarProvider'; -export { useSnackbar } from './Snackbar/useSnackbar'; -export type * from './Snackbar'; \ No newline at end of file +export { useSnackbar } from './Snackbar/hooks/useSnackbar'; +export type * from './Snackbar'; From bb5de21b472f8c2aa6c8206edc21e3c84e095229 Mon Sep 17 00:00:00 2001 From: seocylucky Date: Fri, 1 Nov 2024 00:18:27 +0900 Subject: [PATCH 14/20] =?UTF-8?q?fix:=20style=20type=20transient=20prop=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC,=20SnackbarProps=20=EC=86=8D=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20$=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/Snackbar.stories.tsx | 2 +- src/components/Snackbar/Snackbar.style.ts | 18 +++++++++--------- src/components/Snackbar/Snackbar.tsx | 6 +++--- src/components/Snackbar/Snackbar.type.ts | 2 +- src/components/Snackbar/SnackbarProvider.tsx | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/Snackbar/Snackbar.stories.tsx b/src/components/Snackbar/Snackbar.stories.tsx index 0994394..bce9a09 100644 --- a/src/components/Snackbar/Snackbar.stories.tsx +++ b/src/components/Snackbar/Snackbar.stories.tsx @@ -38,7 +38,7 @@ const meta: Meta = { }, onClose: { table: { disable: true } }, isClosing: { table: { disable: true } }, - $heightType: { table: { disable: true } }, + heightType: { table: { disable: true } }, }, }; diff --git a/src/components/Snackbar/Snackbar.style.ts b/src/components/Snackbar/Snackbar.style.ts index e077ccf..1984b73 100644 --- a/src/components/Snackbar/Snackbar.style.ts +++ b/src/components/Snackbar/Snackbar.style.ts @@ -8,8 +8,8 @@ interface StyledSnackbarProps { $type: 'info' | 'error'; $width: SnackbarProps['width']; $margin?: SnackbarProps['margin']; - isClosing?: boolean; - position: SnackbarPosition; + $isClosing?: boolean; + $position: SnackbarPosition; $heightType?: SnackbarHeightType; } @@ -56,12 +56,12 @@ export const StyledSnackbarContainer = styled.div< >` position: fixed; bottom: 0; - width: ${({ position }) => (position === 'full-width' ? '100%' : 'fit-content')}; + 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)} + ${({ $position, $margin }) => getPositionStyle($position, $margin)} `; export const StyledSnackbar = styled.div.withConfig({ @@ -70,7 +70,7 @@ export const StyledSnackbar = styled.div.withConfig({ position: relative; padding: 24px; margin-bottom: 16px; - width: ${({ position, $width }) => (position === 'full-width' ? 'calc(100% - 32px)' : $width)}; + width: ${({ $position, $width }) => ($position === 'full-width' ? 'calc(100% - 32px)' : $width)}; height: ${({ $heightType }) => ($heightType === 2 ? '72px' : '52px')}; border-radius: ${({ theme }) => theme.semantic.radius.m}px; @@ -83,13 +83,13 @@ export const StyledSnackbar = styled.div.withConfig({ align-items: center; justify-content: space-between; - ${({ isClosing }) => css` - opacity: ${isClosing ? 0 : 1}; - transform: ${isClosing ? 'translateY(100%)' : 'translateY(0)'}; + ${({ $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'}; + animation: ${$isClosing ? 'none' : 'slideIn 500ms ease-out'}; `} @keyframes slideIn { diff --git a/src/components/Snackbar/Snackbar.tsx b/src/components/Snackbar/Snackbar.tsx index d8d2ea5..788e957 100644 --- a/src/components/Snackbar/Snackbar.tsx +++ b/src/components/Snackbar/Snackbar.tsx @@ -50,12 +50,12 @@ export const Snackbar = ({ return ( diff --git a/src/components/Snackbar/Snackbar.type.ts b/src/components/Snackbar/Snackbar.type.ts index 0c65f87..101c1a6 100644 --- a/src/components/Snackbar/Snackbar.type.ts +++ b/src/components/Snackbar/Snackbar.type.ts @@ -11,7 +11,7 @@ export interface SnackbarProps { duration?: number; position?: SnackbarPosition; isClosing: boolean; - $heightType?: SnackbarHeightType; + heightType?: SnackbarHeightType; } export type SnackbarWithoutClosingProps = Omit; diff --git a/src/components/Snackbar/SnackbarProvider.tsx b/src/components/Snackbar/SnackbarProvider.tsx index 746b60e..406f89c 100644 --- a/src/components/Snackbar/SnackbarProvider.tsx +++ b/src/components/Snackbar/SnackbarProvider.tsx @@ -29,7 +29,7 @@ export const SnackbarProvider = ({ children }: PropsWithChildren) => { {children} {snackbar && ReactDOM.createPortal( - + , document.body From 01a5ad27617fd473b2f584e4362c422c51b5d32b Mon Sep 17 00:00:00 2001 From: seocylucky Date: Fri, 1 Nov 2024 02:11:17 +0900 Subject: [PATCH 15/20] =?UTF-8?q?fix:=20import/order=20lint=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20->=20=EC=A0=88=EB=8C=80=EA=B2=BD=EB=A1=9C=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/Snackbar.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Snackbar/Snackbar.stories.tsx b/src/components/Snackbar/Snackbar.stories.tsx index bce9a09..e178977 100644 --- a/src/components/Snackbar/Snackbar.stories.tsx +++ b/src/components/Snackbar/Snackbar.stories.tsx @@ -1,6 +1,6 @@ import { Meta, StoryObj } from '@storybook/react'; -import { BoxButton } from '../BoxButton'; +import { BoxButton } from '@/components/BoxButton'; import { Snackbar } from './Snackbar'; import { SnackbarProps } from './Snackbar.type'; import { SnackbarProvider } from './SnackbarProvider'; From 08c7f4c9357ab688fd93cea013a63b522237077c Mon Sep 17 00:00:00 2001 From: seocylucky Date: Fri, 1 Nov 2024 15:38:48 +0900 Subject: [PATCH 16/20] =?UTF-8?q?fix:=20full-width=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/Snackbar.stories.tsx | 6 +++--- src/components/Snackbar/Snackbar.style.ts | 16 +++++++++------- src/components/Snackbar/Snackbar.type.ts | 12 ++++++++++-- src/components/Snackbar/SnackbarProvider.tsx | 7 ++++++- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/components/Snackbar/Snackbar.stories.tsx b/src/components/Snackbar/Snackbar.stories.tsx index e178977..f34bfcb 100644 --- a/src/components/Snackbar/Snackbar.stories.tsx +++ b/src/components/Snackbar/Snackbar.stories.tsx @@ -22,15 +22,15 @@ const meta: Meta = { }, width: { control: 'text', - description: 'Snackbar의 가로 길이 (px, %, 등)', + description: `Snackbar의 가로 길이 (px, rem, em, %, vh, calc())`, }, duration: { description: 'Snackbar가 자동으로 닫히기 전까지의 시간 (ms)', control: 'number', }, position: { - description: 'Snackbar의 위치 (left, center, right, full-width)', - control: { type: 'radio', options: ['left', 'center', 'right', 'full-width'] }, + description: 'Snackbar의 위치 (left, center, right)', + control: { type: 'radio', options: ['left', 'center', 'right'] }, }, message: { control: 'text', diff --git a/src/components/Snackbar/Snackbar.style.ts b/src/components/Snackbar/Snackbar.style.ts index 1984b73..6baf881 100644 --- a/src/components/Snackbar/Snackbar.style.ts +++ b/src/components/Snackbar/Snackbar.style.ts @@ -31,13 +31,15 @@ const getPositionStyle = (position: SnackbarPosition, margin?: string) => { 'left', () => css` left: 0; + align-items: start; margin-left: ${margin || '16px'}; ` ) .with( 'right', () => css` - right: 0px; + right: 0; + align-items: end; margin-right: ${margin || '16px'}; ` ) @@ -51,17 +53,17 @@ const getPositionStyle = (position: SnackbarPosition, margin?: string) => { ); }; -export const StyledSnackbarContainer = styled.div< - Omit ->` +export const StyledSnackbarContainer = styled.div>` position: fixed; bottom: 0; - width: ${({ $position }) => ($position === 'full-width' ? '100%' : 'fit-content')}; + width: ${({ $width }) => ($width === 'full-width' ? '100%' : $width)}; height: fit-content; display: flex; flex-direction: column-reverse; margin: 0 auto; - ${({ $position, $margin }) => getPositionStyle($position, $margin)} + align-items: center; + ${({ $width, $position, $margin }) => + $width !== 'full-width' && getPositionStyle($position, $margin)} `; export const StyledSnackbar = styled.div.withConfig({ @@ -70,7 +72,7 @@ export const StyledSnackbar = styled.div.withConfig({ position: relative; padding: 24px; margin-bottom: 16px; - width: ${({ $position, $width }) => ($position === 'full-width' ? 'calc(100% - 32px)' : $width)}; + width: ${({ $width }) => ($width === 'full-width' ? 'calc(100% - 32px)' : $width)}; height: ${({ $heightType }) => ($heightType === 2 ? '72px' : '52px')}; border-radius: ${({ theme }) => theme.semantic.radius.m}px; diff --git a/src/components/Snackbar/Snackbar.type.ts b/src/components/Snackbar/Snackbar.type.ts index 101c1a6..8df12ee 100644 --- a/src/components/Snackbar/Snackbar.type.ts +++ b/src/components/Snackbar/Snackbar.type.ts @@ -1,10 +1,18 @@ export type SnackbarType = 'info' | 'error'; export type SnackbarHeightType = 1 | 2; -export type SnackbarPosition = 'left' | 'center' | 'right' | 'full-width'; +export type SnackbarPosition = 'left' | 'center' | 'right'; +export type SnackbarWidth = + | 'full-width' + | `${number}px` + | `${number}rem` + | `${number}em` + | `${number}%` + | `${number}vh` + | `calc(${string})`; export interface SnackbarProps { type?: SnackbarType; - width?: string; + width?: SnackbarWidth; margin?: string; message: string; onClose?: () => void; diff --git a/src/components/Snackbar/SnackbarProvider.tsx b/src/components/Snackbar/SnackbarProvider.tsx index 406f89c..b3d1d45 100644 --- a/src/components/Snackbar/SnackbarProvider.tsx +++ b/src/components/Snackbar/SnackbarProvider.tsx @@ -1,5 +1,7 @@ import { createContext, PropsWithChildren, useCallback, useContext, useState } from 'react'; + import ReactDOM from 'react-dom'; + import { Snackbar } from './Snackbar'; import { StyledSnackbarContainer } from './Snackbar.style'; import { SnackbarWithoutClosingProps } from './Snackbar.type'; @@ -29,7 +31,10 @@ export const SnackbarProvider = ({ children }: PropsWithChildren) => { {children} {snackbar && ReactDOM.createPortal( - + , document.body From 3ea81f04c5ec65e9d62f991ca5ca4aa1e05e853d Mon Sep 17 00:00:00 2001 From: seocylucky Date: Fri, 1 Nov 2024 16:04:31 +0900 Subject: [PATCH 17/20] =?UTF-8?q?docs:=20prop=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=EB=A6=AC=EB=B6=81=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/Snackbar.mdx | 52 +++++++++++++++----- src/components/Snackbar/Snackbar.stories.tsx | 2 +- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/components/Snackbar/Snackbar.mdx b/src/components/Snackbar/Snackbar.mdx index e7df613..91f8451 100644 --- a/src/components/Snackbar/Snackbar.mdx +++ b/src/components/Snackbar/Snackbar.mdx @@ -38,12 +38,7 @@ const { snackbar } = useSnackbar(); 필수 프로퍼티인 `message`를 꼭 설정해주세요. -`type` - Snackbar의 종류 (info 또는 error). 기본 값은 `info`입니다.
-`width` - Snackbar의 가로 길이 (px, %, 등). 기본적으로 글자 길이에 맞게 가로 길이가 정해집니다.
-`margin` - 왼쪽 오른쪽의 margin 값. 기본 값은 `16px`입니다.
-`message` - Snackbar의 내용 (메시지). 필수 프로퍼티입니다.
-`duration` - Snackbar가 자동으로 닫히기 전까지의 시간(ms). 기본 값은 `5000`입니다.
-`position` - Snackbar의 위치 (left, center, right, full-width). 기본 값은 `center`입니다. +이외의 프로퍼티들은 하단의 예시에서 확인할 수 있습니다. ```tsx function App() { @@ -51,12 +46,12 @@ function App() { const handleShowSnackbar = () => { snackbar({ - type: 'info', // Snackbar의 종류 (info 또는 error) - width: '350px', // Snackbar의 가로 길이 (px, %, 등) + type: 'info', // Snackbar의 종류 + width: '350px', // Snackbar의 가로 길 margin: '16px', // 왼쪽 오른쪽의 margin 값 - message: '테스트용 스낵바입니다.', // Snackbar의 내용 (메시지) + message: '테스트용 스낵바입니다.', // Snackbar의 내용 duration: 3000, // Snackbar가 자동으로 닫히기 전까지의 시간(ms) - position: 'center', // Snackbar의 위치 (left, center, right, full-width) + position: 'center', // Snackbar의 위치 }); }; return ( @@ -71,8 +66,43 @@ function App() { 4. Snackbar를 닫을 시, info 타입일 때는 `드래그`, error 타입일 때는 `X 버튼`을 클릭하여 Snackbar를 닫을 수 있습니다. - + 5. Snackbar는 `최대 두 줄까지` 입력되며, 두 줄을 넘어설 시, `ellipsis` 처리됩니다. + +## 예시 + +### type + +`type` prop으로 Snackbar의 종류를 설정합니다. (info 또는 error)
+기본 값은 `info`입니다. + + + +### width + +`width` prop으로 원하는 Snackbar의 가로 길이를 설정합니다. (full-width, px, rem, em, %, vh, calc())
+기본 값으로 글자 길이에 맞게 가로 길이가 정해집니다. + +`full-width`인 경우, 기본적으로 `양쪽 margin 16px`이 설정됩니다. 이 때, position 설정은 적용되지 않습니다. + +### margin + +`margin` prop으로 Snackbar의 왼쪽 오른쪽의 margin 값을 정해줍니다.
+기본 값은 `16px`입니다. + +### message + +Snackbar의 필수 프로퍼티로, Snackbar의 내용을 설정합니다.
+ +### duration + +`duration` prop으로 Snackbar가 자동으로 닫히기까지의 시간을 설정합니다. (단위: `ms`)
+기본 값은 `5000`입니다. + +### position + +`position` prop으로 Snackbar의 위치를 설정합니다. (left, center, right)
+기본 값은 `center`입니다. diff --git a/src/components/Snackbar/Snackbar.stories.tsx b/src/components/Snackbar/Snackbar.stories.tsx index f34bfcb..a00d0a4 100644 --- a/src/components/Snackbar/Snackbar.stories.tsx +++ b/src/components/Snackbar/Snackbar.stories.tsx @@ -108,7 +108,7 @@ export const Error: Story = { }, }; -export const CloseTest: Story = { +export const type: Story = { render: (args) => (
From 27ba6ad983612d91bc5127bf0d978616319dfa8f Mon Sep 17 00:00:00 2001 From: seocylucky Date: Fri, 1 Nov 2024 18:05:20 +0900 Subject: [PATCH 18/20] =?UTF-8?q?fix:=20=EC=8A=A4=EB=82=B5=EB=B0=94=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=20=EA=B4=80=EB=A0=A8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/Snackbar.style.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Snackbar/Snackbar.style.ts b/src/components/Snackbar/Snackbar.style.ts index 6baf881..6fc17ea 100644 --- a/src/components/Snackbar/Snackbar.style.ts +++ b/src/components/Snackbar/Snackbar.style.ts @@ -70,13 +70,13 @@ export const StyledSnackbar = styled.div.withConfig({ shouldForwardProp: (prop) => prop !== 'isClosing', })` position: relative; - padding: 24px; + 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; - ${({ theme }) => theme.typo.B3_Rg_14} + ${({ $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 })}`}; From bb4859ebfc9f93eeacedb7255564dadb05fb84ee Mon Sep 17 00:00:00 2001 From: seocylucky Date: Sun, 3 Nov 2024 00:22:59 +0900 Subject: [PATCH 19/20] =?UTF-8?q?docs:=20type=20prop=20=EC=98=88=EC=8B=9C?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/Snackbar.mdx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Snackbar/Snackbar.mdx b/src/components/Snackbar/Snackbar.mdx index 91f8451..ed04cda 100644 --- a/src/components/Snackbar/Snackbar.mdx +++ b/src/components/Snackbar/Snackbar.mdx @@ -79,8 +79,6 @@ function App() { `type` prop으로 Snackbar의 종류를 설정합니다. (info 또는 error)
기본 값은 `info`입니다. - - ### width `width` prop으로 원하는 Snackbar의 가로 길이를 설정합니다. (full-width, px, rem, em, %, vh, calc())
From 6a732d2215730afc838b2c1aea338e8e54c154d0 Mon Sep 17 00:00:00 2001 From: seocylucky Date: Fri, 8 Nov 2024 16:14:42 +0900 Subject: [PATCH 20/20] =?UTF-8?q?docs:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20Story=20=EC=82=AD=EC=A0=9C=20=EB=B0=8F=20=EB=8C=80?= =?UTF-8?q?=EC=86=8C=EB=AC=B8=EC=9E=90=20=EA=B4=80=EB=A0=A8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Snackbar/Snackbar.mdx | 2 +- src/components/Snackbar/Snackbar.stories.tsx | 31 ++------------------ 2 files changed, 3 insertions(+), 30 deletions(-) diff --git a/src/components/Snackbar/Snackbar.mdx b/src/components/Snackbar/Snackbar.mdx index ed04cda..0d9ddfd 100644 --- a/src/components/Snackbar/Snackbar.mdx +++ b/src/components/Snackbar/Snackbar.mdx @@ -66,7 +66,7 @@ function App() { 4. Snackbar를 닫을 시, info 타입일 때는 `드래그`, error 타입일 때는 `X 버튼`을 클릭하여 Snackbar를 닫을 수 있습니다. - + 5. Snackbar는 `최대 두 줄까지` 입력되며, 두 줄을 넘어설 시, `ellipsis` 처리됩니다. diff --git a/src/components/Snackbar/Snackbar.stories.tsx b/src/components/Snackbar/Snackbar.stories.tsx index a00d0a4..aa79c62 100644 --- a/src/components/Snackbar/Snackbar.stories.tsx +++ b/src/components/Snackbar/Snackbar.stories.tsx @@ -1,6 +1,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { BoxButton } from '@/components/BoxButton'; + import { Snackbar } from './Snackbar'; import { SnackbarProps } from './Snackbar.type'; import { SnackbarProvider } from './SnackbarProvider'; @@ -80,35 +81,7 @@ export const Test: Story = { }, }; -export const Info: Story = { - render: (args) => ( - - - - ), - args: { - type: 'info', - position: 'center', - width: '350px', - message: '정보성 메시지가 들어갑니다. 최대 2줄 입력 가능합니다.', - }, -}; - -export const Error: Story = { - render: (args) => ( - - - - ), - args: { - type: 'error', - message: '에러 메시지가 들어갑니다. 최대 2줄 입력 가능합니다.', - width: '350px', - position: 'center', - }, -}; - -export const type: Story = { +export const Type: Story = { render: (args) => (