diff --git a/src/components/Toast/Toast.stories.tsx b/src/components/Toast/Toast.stories.tsx new file mode 100644 index 0000000..5721217 --- /dev/null +++ b/src/components/Toast/Toast.stories.tsx @@ -0,0 +1,82 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { useToast } from '@/hooks/useToast'; + +import { Toast } from './Toast'; +import { ToastDuration } from './Toast.type'; + +const meta: Meta = { + title: 'Component/Toast', + component: Toast, + parameters: { + layout: 'centered', + }, +}; +export default meta; + +const ToastStory = ({ ...toastProps }) => { + return ( +
+
+ short duration toast (1.5s) + +
+
+ long duration toast (3s) + +
+
+ ); +}; + +const HookTest = () => { + const toastProps = { + children: 'useToast를 사용한 토스트 메시지', + duration: 'long' as ToastDuration, + }; + const { showToast, isShowToast } = useToast(); + + return ( +
+ + {isShowToast && } +
+ ); +}; + +type Story = StoryObj; +export const SingleLine: Story = { + args: { + children: '토스트 메시지', + }, + render: ToastStory, +}; +export const MultiLine: Story = { + args: { + children: '줄 수가 두 줄 이상이 되는 토스트 메시지입니다. 좌측 정렬을 해주세요.', + }, + render: ToastStory, +}; +export const ToastHook: Story = { + render: HookTest, +}; diff --git a/src/components/Toast/Toast.style.ts b/src/components/Toast/Toast.style.ts new file mode 100644 index 0000000..15a0433 --- /dev/null +++ b/src/components/Toast/Toast.style.ts @@ -0,0 +1,59 @@ +import { css, keyframes, styled } from 'styled-components'; + +import { ToastDuration } from './Toast.type'; + +interface StyledToastProps { + $duration: ToastDuration; +} + +const SHORT_DURATION = 1.5; +const LONG_DURATION = 3; +const FADE_DURATION = 0.25; + +export const ToastFadeIn = keyframes` +to { + opacity: 1; +} +`; + +export const ToastFadeOut = keyframes` +to { + opacity: 0; +} +`; + +const setToastAnimation = ($duration: ToastDuration) => { + switch ($duration) { + case 'short': + return css` + ${ToastFadeIn} ${FADE_DURATION}s ease-in forwards, + ${ToastFadeOut} ${FADE_DURATION}s ${SHORT_DURATION + FADE_DURATION}s ease-out forwards + `; + case 'long': + return css` + ${ToastFadeIn} ${FADE_DURATION}s ease-in forwards, + ${ToastFadeOut} ${FADE_DURATION}s ${LONG_DURATION + FADE_DURATION}s ease-out forwards + `; + } +}; + +export const StyledToastWrapper = styled.div` + position: absolute; + bottom: 66px; + width: 100%; + padding: 0px 8px; +`; + +export const StyledToast = styled.div` + opacity: 0; + border-radius: 8px; + width: 100%; + padding: 16px 24px; + display: flex; + justify-content: center; + background-color: ${({ theme }) => theme.color.toastBG}; + color: ${({ theme }) => theme.color.textBright}; + ${({ theme }) => theme.typo.body2}; + + animation: ${({ $duration }) => setToastAnimation($duration)}; +`; diff --git a/src/components/Toast/Toast.tsx b/src/components/Toast/Toast.tsx new file mode 100644 index 0000000..3f7a519 --- /dev/null +++ b/src/components/Toast/Toast.tsx @@ -0,0 +1,14 @@ +import { StyledToast, StyledToastWrapper } from './Toast.style'; +import { ToastProps } from './Toast.type'; + +export const Toast = ({ children, duration = 'short', ...props }: ToastProps) => { + if (!children) return; + + return ( + + + {children} + + + ); +}; diff --git a/src/components/Toast/Toast.type.ts b/src/components/Toast/Toast.type.ts new file mode 100644 index 0000000..0fcd8e2 --- /dev/null +++ b/src/components/Toast/Toast.type.ts @@ -0,0 +1,6 @@ +export type ToastDuration = 'short' | 'long'; + +export interface ToastProps extends React.HTMLAttributes { + children?: React.ReactNode; + duration?: ToastDuration; +} diff --git a/src/components/Toast/index.ts b/src/components/Toast/index.ts new file mode 100644 index 0000000..9a40e05 --- /dev/null +++ b/src/components/Toast/index.ts @@ -0,0 +1,2 @@ +export { Toast } from './Toast'; +export type { ToastProps } from './Toast.type'; diff --git a/src/components/index.ts b/src/components/index.ts index 049f4e0..2016b4e 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -18,3 +18,6 @@ export type { PlainButtonProps } from './PlainButton'; export { Toggle } from './Toggle'; export type { ToggleProps } from './Toggle'; + +export { Toast } from './Toast'; +export type { ToastProps } from './Toast'; diff --git a/src/hooks/useToast/index.ts b/src/hooks/useToast/index.ts new file mode 100644 index 0000000..2c4979c --- /dev/null +++ b/src/hooks/useToast/index.ts @@ -0,0 +1 @@ +export { useToast } from './useToast'; diff --git a/src/hooks/useToast/useToast.ts b/src/hooks/useToast/useToast.ts new file mode 100644 index 0000000..459492f --- /dev/null +++ b/src/hooks/useToast/useToast.ts @@ -0,0 +1,29 @@ +import { useState } from 'react'; + +import { ToastDuration } from '@/components/Toast/Toast.type'; + +export const useToast = () => { + const removeTime = { + short: 2000, + long: 3500, + }; + + const [isShowToast, setIsShowToast] = useState(false); + + const showToast = (duration: ToastDuration) => { + setIsShowToast(true); + removeToast(duration); + }; + + const removeToast = (duration: ToastDuration) => { + const timer = setTimeout(() => { + setIsShowToast(false); + }, removeTime[duration]); + + return () => { + clearTimeout(timer); + }; + }; + + return { isShowToast, showToast }; +}; diff --git a/src/style/foundation/color/semanticColor/semanticColor.type.ts b/src/style/foundation/color/semanticColor/semanticColor.type.ts index 830b0c1..594931b 100644 --- a/src/style/foundation/color/semanticColor/semanticColor.type.ts +++ b/src/style/foundation/color/semanticColor/semanticColor.type.ts @@ -15,6 +15,7 @@ export type SemanticTextColor = | 'textSecondary' | 'textTertiary' | 'textDisabled' + | 'textBright' | 'textPointed' | 'textWarned'; diff --git a/src/style/foundation/color/semanticColor/semanticColorPalette.ts b/src/style/foundation/color/semanticColor/semanticColorPalette.ts index 2770563..02e9a69 100644 --- a/src/style/foundation/color/semanticColor/semanticColorPalette.ts +++ b/src/style/foundation/color/semanticColor/semanticColorPalette.ts @@ -21,6 +21,7 @@ const lightSemanticColorPalette: SemanticColorPalette = { textSecondary: baseColorPalettes.light.gray900, textTertiary: baseColorPalettes.light.gray600, textDisabled: baseColorPalettes.light.gray500, + textBright: baseColorPalettes.light.white000, textPointed: baseColorPalettes.light.pointColor400, textWarned: baseColorPalettes.light.warningRed400, @@ -141,6 +142,7 @@ const darkSemanticColorPalette: SemanticColorPalette = { textSecondary: baseColorPalettes.dark.gray800, textTertiary: baseColorPalettes.dark.gray600, textDisabled: baseColorPalettes.dark.gray400, + textBright: baseColorPalettes.dark.white000, textPointed: baseColorPalettes.dark.pointColor400, textWarned: baseColorPalettes.dark.warningRed400,