diff --git a/src/components/common/Toast/Toast.tsx b/src/components/common/Toast/Toast.tsx index f965850..d435ad2 100644 --- a/src/components/common/Toast/Toast.tsx +++ b/src/components/common/Toast/Toast.tsx @@ -5,10 +5,11 @@ import React from 'react'; import { DefaultVariantType } from '@/types/variant'; import { useToast } from '.'; -import { ToastData } from './type'; +import { ToastData, ToastPosition } from './type'; interface ToastProps extends React.ComponentPropsWithoutRef<'li'> { toast: ToastData; + position: ToastPosition; } export const Toast = React.forwardRef(({ toast, ...props }, ref) => { @@ -85,7 +86,37 @@ const ToastVariantStyles = ({ theme, variant }: { theme: Theme; variant: Default } }; -const ToastStyled = styled.li<{ variant?: DefaultVariantType }>` +const ToastPositionStyles = ({ position }: { position: ToastPosition }) => { + switch (position) { + case 'top-left': + return css` + transform: translateX(calc(-100% - 2rem)); + `; + case 'top-center': + return css` + transform: translateY(-100vh); + `; + case 'top-right': + return css` + transform: translateX(calc(100% + 2rem)); + `; + case 'bottom-left': + return css` + transform: translateX(calc(-100% - 2rem)); + `; + case 'bottom-center': + return css` + transform: translateY(100vh); + `; + case 'bottom-right': + default: + return css` + transform: translateX(calc(100% + 2rem)); + `; + } +}; + +const ToastStyled = styled.li<{ variant?: DefaultVariantType; position?: ToastPosition }>` padding: 1rem; background-color: ${({ theme }) => theme.colors.white}; border-radius: 0.5rem; @@ -96,14 +127,14 @@ const ToastStyled = styled.li<{ variant?: DefaultVariantType }>` justify-content: space-between; box-shadow: 0 0px 3px ${({ theme }) => theme.colors.gray['400']}; + transition: transform 0.25s cubic-bezier(0.75, -0.5, 0.25, 1.25); - transform: translateX(calc(100% + 2rem)); - transition: all 0.25s cubic-bezier(0.75, -0.5, 0.25, 1.25); cursor: pointer; &.active { - transform: translateX(0); + transform: translateX(0) translateY(0); } ${({ theme, variant }) => variant && ToastVariantStyles({ theme, variant })} + ${({ position }) => position && ToastPositionStyles({ position })} `; export const ToastTitle = styled.div` diff --git a/src/components/common/Toast/Toaster.tsx b/src/components/common/Toast/Toaster.tsx index d963ce6..c2a2887 100644 --- a/src/components/common/Toast/Toaster.tsx +++ b/src/components/common/Toast/Toaster.tsx @@ -1,24 +1,31 @@ +import { css } from '@emotion/react'; import styled from '@emotion/styled'; +import React from 'react'; import { createPortal } from 'react-dom'; import { Toast } from './Toast'; import { ToasterProvider } from './ToastContext'; +import { ToastPosition } from './type'; import { useToast } from './useToast'; +interface ToasterProps { + position?: ToastPosition; +} + /** * Toast Context Provider * @returns */ -export const Toaster = () => { +export const Toaster = ({ position = 'bottom-right' }: ToasterProps) => { const { toasts } = useToast(); return ( {toasts.length > 0 && createPortal( - + {toasts.map(t => ( - + ))} , document.getElementById('toaster') || document.body, @@ -26,7 +33,52 @@ export const Toaster = () => { ); }; -const ToastList = styled.ol` + +const ToastPositionStyles = ({ position }: { position: ToastPosition }) => { + switch (position) { + case 'top-left': + return css` + top: 0px; + left: 0px; + bottom: auto; + `; + case 'top-center': + return css` + top: 0px; + left: 50%; + bottom: auto; + transform: translate(-50%); + `; + case 'top-right': + return css` + top: 0px; + right: 0px; + bottom: auto; + `; + case 'bottom-left': + return css` + top: auto; + left: 0px; + bottom: 0px; + `; + case 'bottom-center': + return css` + bottom: 0px; + left: 50%; + top: auto; + transform: translate(-50%); + `; + case 'bottom-right': + default: + return css` + top: auto; + right: 0px; + bottom: 0px; + `; + } +}; + +const ToastList = styled.ol<{ position?: ToastPosition }>` list-style: none; position: fixed; display: flex; @@ -35,10 +87,9 @@ const ToastList = styled.ol` max-height: 100vh; width: 100%; padding: 1rem; - top: auto; - right: 0px; - bottom: 0px; flex-direction: column; max-width: 25rem; gap: 1rem; + + ${({ position }) => position && ToastPositionStyles({ position })} `; diff --git a/src/components/common/Toast/type.ts b/src/components/common/Toast/type.ts index 204c404..9181908 100644 --- a/src/components/common/Toast/type.ts +++ b/src/components/common/Toast/type.ts @@ -4,6 +4,8 @@ import { Toast } from '.'; export type ToastProps = React.ComponentPropsWithoutRef; +export type ToastPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'; + export const TOAST_LIMIT = 5; // export Type diff --git a/src/components/common/Toast/useToast.tsx b/src/components/common/Toast/useToast.tsx index 355a032..654e097 100644 --- a/src/components/common/Toast/useToast.tsx +++ b/src/components/common/Toast/useToast.tsx @@ -9,12 +9,12 @@ const listeners: Array<(state: State) => void> = []; let memoryState: State = { toasts: [] }; -function dispatch(action: Action) { +const dispatch = (action: Action) => { memoryState = reducer(memoryState, action); listeners.forEach(listener => { listener(memoryState); }); -} +}; let cnt = 1; const toast = ({ ...props }: ToastProps) => { diff --git a/src/stories/common/Toast.stories.tsx b/src/stories/common/Toast.stories.tsx index 807f369..5e71c4b 100644 --- a/src/stories/common/Toast.stories.tsx +++ b/src/stories/common/Toast.stories.tsx @@ -30,7 +30,7 @@ const meta = { return ( <> - + ); }, @@ -47,7 +47,7 @@ type Story = StoryObj; export const Default: Story = { args: { title: 'Toast Title', - description: `When you click Toast, containing 'data' 'action' is executed.`, + description: `When you click Toast, containing 'data' 'action' is executed.`, variant: 'primary', time: 5000, data: 'Hi',