From 93256a8eb8f3b71f372e561ae810fe6ec2819fc3 Mon Sep 17 00:00:00 2001 From: sikkzz Date: Sat, 22 Feb 2025 01:34:02 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ui/Toast/Toast.module.scss | 72 +++++++---------------- src/components/ui/Toast/Toast.stories.tsx | 20 ------- src/components/ui/Toast/Toast.tsx | 26 ++++++-- src/components/ui/Toast/Toaster.tsx | 20 +++++-- src/hooks/common/useToast.tsx | 12 +++- 5 files changed, 63 insertions(+), 87 deletions(-) delete mode 100644 src/components/ui/Toast/Toast.stories.tsx diff --git a/src/components/ui/Toast/Toast.module.scss b/src/components/ui/Toast/Toast.module.scss index 34c4606..27de95d 100644 --- a/src/components/ui/Toast/Toast.module.scss +++ b/src/components/ui/Toast/Toast.module.scss @@ -19,61 +19,17 @@ } } -// .Toast { -// width: 100%; -// height: 3.25rem; -// z-index: 2; -// border-radius: 0.75rem; -// background-color: white; -// padding: 0.875rem 1.125rem; - -// transition: -// opacity 0.3s, -// transform 0.3s; -// } - -// .show { -// opacity: 1; -// transform: translateY(0); -// } - -// .ToastRoot[data-state="open"] { -// animation: slideUp 150ms cubic-bezier(0.16, 1, 0.3, 1); -// } -// .ToastRoot[data-state="closed"] { -// animation: hide 100ms ease-in; -// } - -// @keyframes hide { -// from { -// opacity: 1; -// } -// to { -// opacity: 0; -// } -// } - -// @keyframes slideUp { -// from { -// transform: translateY(100%); -// opacity: 0; -// } -// to { -// transform: translateY(0); -// opacity: 1; -// } -// } - .ToastViewport { position: fixed; left: 50%; - top: 0.75rem; + bottom: 7rem; z-index: 9999; width: fit-content; display: flex; flex-direction: column; gap: 0.5rem; transform: translateX(-50%); + width: calc(100% - 2.5rem); } .Toast { @@ -87,17 +43,18 @@ transform: translateX(var(--radix-toast-swipe-move-x)); } &[data-state="open"] { - animation: slide-in-from-top 0.3s ease-out forwards; + animation: slide-in-from-bottom-full 0.4s ease-out forwards; } + &[data-state="closed"] { - animation: slide-out-to-top 0.3s ease-in forwards; + animation: slide-out-to-bottom-full 0.4s ease-in forwards; } } -@keyframes slide-in-from-top { +@keyframes slide-in-from-bottom-full { from { opacity: 0; - transform: translateY(-10px); + transform: translateY(300%); } to { opacity: 1; @@ -105,13 +62,24 @@ } } -@keyframes slide-out-to-top { +@keyframes slide-out-to-bottom-full { from { opacity: 1; transform: translateY(0); } to { opacity: 0; - transform: translateY(-10px); + transform: translateY(300%); + } +} + +.Toaster { + border-radius: 0.75rem; + background-color: var(--color-white); + width: 100%; + padding: 0.875rem 1.125rem; + + & > span { + line-height: 1.5rem; } } diff --git a/src/components/ui/Toast/Toast.stories.tsx b/src/components/ui/Toast/Toast.stories.tsx deleted file mode 100644 index 2578950..0000000 --- a/src/components/ui/Toast/Toast.stories.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import Toast from "@/components/ui/Toast/Toast"; - -import type { Meta, StoryObj } from "@storybook/react"; - -type ToastProps = { - text: string; -}; - -const meta: Meta = { - title: "Example/Toast", - component: Toast, -}; - -export default meta; - -export const Primary: StoryObj = { - args: { - text: "링크가 복사되었어요.", - }, -}; diff --git a/src/components/ui/Toast/Toast.tsx b/src/components/ui/Toast/Toast.tsx index 026902f..36b0b82 100644 --- a/src/components/ui/Toast/Toast.tsx +++ b/src/components/ui/Toast/Toast.tsx @@ -1,13 +1,15 @@ -import * as React from "react"; +import { forwardRef } from "react"; import * as ToastPrimitives from "@radix-ui/react-toast"; import classNames from "classnames"; import styles from "@/components/ui/Toast/Toast.module.scss"; +import { useToast } from "@/hooks/common/useToast"; + const ToastProvider = ToastPrimitives.Provider; -const ToastViewport = React.forwardRef< +const ToastViewport = forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( @@ -19,17 +21,29 @@ const ToastViewport = React.forwardRef< )); ToastViewport.displayName = ToastPrimitives.Viewport.displayName; -const Toast = React.forwardRef< +const Toast = forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => { +>(({ className, open, id, ...props }, ref) => { + const { removeToast } = useToast(); + return ( - + { + if (!isOpen) { + setTimeout(() => removeToast(id), 400); // 0.4초 뒤 제거 + } + }} + className={classNames(styles.Toast, className)} + {...props} + /> ); }); Toast.displayName = ToastPrimitives.Root.displayName; -const ToastTitle = React.forwardRef< +const ToastTitle = forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( diff --git a/src/components/ui/Toast/Toaster.tsx b/src/components/ui/Toast/Toaster.tsx index d6eba95..8701f84 100644 --- a/src/components/ui/Toast/Toaster.tsx +++ b/src/components/ui/Toast/Toaster.tsx @@ -1,4 +1,6 @@ -import { Toast, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/Toast/Toast"; +import Text from "@/components/ui/Text/Text"; +import { Toast, ToastProvider, ToastViewport } from "@/components/ui/Toast/Toast"; +import styles from "@/components/ui/Toast/Toast.module.scss"; import { useToast } from "@/hooks/common/useToast"; @@ -7,15 +9,21 @@ export function Toaster() { return ( - {toasts.map(({ id, title, duration }) => ( + {toasts.map(({ id, duration, open }) => ( !open && removeToast(id)} - className="flex items-center gap-3 rounded-3 bg-white px-3 py-2 shadow-1" + onOpenChange={(isOpen) => { + if (!isOpen) { + setTimeout(() => removeToast(id), 400); + } + }} + className={styles.Toaster} > - {title && {title}} + + 링크가 복사되었어요. + ))} diff --git a/src/hooks/common/useToast.tsx b/src/hooks/common/useToast.tsx index ed0b705..9c315b3 100644 --- a/src/hooks/common/useToast.tsx +++ b/src/hooks/common/useToast.tsx @@ -11,7 +11,7 @@ type ToastType = { type ToastContextType = { toasts: ToastType[]; addToast: (title: string, duration?: number) => string; - removeToast: (id: string) => void; + removeToast: (id?: string) => void; }; const ToastContext = createContext(undefined); @@ -22,6 +22,12 @@ export function ToastProvider({ children }: { children: ReactNode }) { const addToast = (title: string, duration = 3000): string => { const id = crypto.randomUUID(); + const isToastOpen = toasts.some((toast) => toast.open); + + if (isToastOpen) { + return ""; + } + const newToast: ToastType = { id, title, @@ -33,8 +39,8 @@ export function ToastProvider({ children }: { children: ReactNode }) { return id; }; - const removeToast = (id: string) => { - setToasts((prev) => prev.filter((toast) => toast.id !== id)); + const removeToast = (id?: string) => { + setToasts((prev) => prev.map((toast) => (toast.id === id ? { ...toast, open: false } : toast))); }; return (