-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
459 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<div id="bmates-toaster"></div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
<link rel="preconnect" href="https://fonts.googleapis.com"> | ||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | ||
<link href="https://fonts.googleapis.com/css2?family=Barlow:wght@300;400;500;700;900&display=swap" rel="stylesheet"> | ||
<link rel="preconnect" href="https://fonts.googleapis.com" /> | ||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | ||
<link href="https://fonts.googleapis.com/css2?family=Barlow:wght@300;400;500;700;900&display=swap" rel="stylesheet" /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import { Theme, css } from '@emotion/react'; | ||
import styled from '@emotion/styled'; | ||
import React from 'react'; | ||
|
||
import { DefaultVariantType } from '@/types/variant'; | ||
|
||
import { useToast } from '.'; | ||
import { ToastData } from './type'; | ||
|
||
interface ToastProps extends React.ComponentPropsWithoutRef<'li'> { | ||
toast: ToastData; | ||
} | ||
|
||
export const Toast = React.forwardRef<HTMLLIElement, ToastProps>(({ toast, ...props }, ref) => { | ||
const { toastId, variant = 'default', title, description, time = -1, action, data } = toast; | ||
const [active, setActive] = React.useState<boolean>(false); | ||
const { removeToast } = useToast(); | ||
|
||
React.useEffect(() => { | ||
setActive(true); | ||
|
||
if (time >= 0) { | ||
const timer = setTimeout(() => { | ||
close(); | ||
}, time); | ||
|
||
return () => clearTimeout(timer); | ||
} | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [toastId, time]); | ||
|
||
const close = () => { | ||
setActive(false); | ||
|
||
setTimeout(() => { | ||
removeToast(toastId); | ||
}, 500); | ||
}; | ||
|
||
return ( | ||
<ToastStyled | ||
ref={ref} | ||
variant={variant} | ||
className={active ? 'active' : ''} | ||
onClick={() => { | ||
if (action) action(data); | ||
close(); | ||
}} | ||
data-toastData={data} | ||
{...props} | ||
> | ||
{title && <ToastTitle>{title}</ToastTitle>} | ||
{description && <ToastDescription>{description}</ToastDescription>} | ||
</ToastStyled> | ||
); | ||
}); | ||
Toast.displayName = 'Toast'; | ||
|
||
const ToastVariantStyles = ({ theme, variant }: { theme: Theme; variant: DefaultVariantType }) => { | ||
switch (variant) { | ||
case 'secondary': | ||
return css` | ||
background-color: ${theme.colors.secondary}; | ||
`; | ||
case 'danger': | ||
return css` | ||
color: ${theme.colors.white}; | ||
background-color: ${theme.colors.danger}; | ||
`; | ||
case 'warning': | ||
return css` | ||
color: ${theme.colors.white}; | ||
background-color: ${theme.colors.warning}; | ||
`; | ||
case 'primary': | ||
return css` | ||
color: white; | ||
background-color: ${theme.colors.primary}; | ||
`; | ||
default: | ||
return css` | ||
color: ${theme.colors.black}; | ||
background-color: ${theme.colors.background}; | ||
`; | ||
} | ||
}; | ||
|
||
const ToastStyled = styled.li<{ variant?: DefaultVariantType }>` | ||
padding: 1rem; | ||
background-color: ${({ theme }) => theme.colors.white}; | ||
border-radius: 0.5rem; | ||
border: 1px solid ${({ theme }) => theme.colors.gray['200']}; | ||
display: flex; | ||
flex-direction: column; | ||
justify-content: space-between; | ||
box-shadow: 0 0px 3px ${({ theme }) => theme.colors.gray['400']}; | ||
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); | ||
} | ||
${({ theme, variant }) => variant && ToastVariantStyles({ theme, variant })} | ||
`; | ||
|
||
export const ToastTitle = styled.div` | ||
font-size: 0.875rem; | ||
line-height: 1.25rem; | ||
font-weight: 600; | ||
`; | ||
export const ToastDescription = styled.div` | ||
font-size: 0.75rem; | ||
line-height: 1rem; | ||
font-weight: 400; | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import React from 'react'; | ||
|
||
import { Action, State, TOAST_LIMIT, ToastDispatch } from './type'; | ||
|
||
export const ToastStateContext = React.createContext<State | null>(null); | ||
export const ToastDispatchContext = React.createContext<ToastDispatch | null>(null); | ||
|
||
export const reducer = (state: State, action: Action): State => { | ||
switch (action.type) { | ||
case 'ADD_TOAST': | ||
return { | ||
...state, | ||
toasts: [...state.toasts, action.toast].slice(0, TOAST_LIMIT), | ||
}; | ||
|
||
case 'UPDATE_TOAST': | ||
return { | ||
...state, | ||
toasts: state.toasts.map(t => (t.toastId === action.toast.toastId ? { ...t, ...action.toast } : t)), | ||
}; | ||
|
||
case 'REMOVE_TOAST': | ||
if (action.toastId === undefined) { | ||
return { | ||
...state, | ||
toasts: [], | ||
}; | ||
} | ||
return { | ||
...state, | ||
toasts: state.toasts.filter(t => t.toastId !== action.toastId), | ||
}; | ||
} | ||
}; | ||
|
||
/** | ||
* Toast Context Provider | ||
* @returns | ||
*/ | ||
export const ToasterProvider = ({ children }: React.PropsWithChildren) => { | ||
const [state, dispatch] = React.useReducer(reducer, { | ||
toasts: [], | ||
}); | ||
|
||
return ( | ||
<ToastStateContext.Provider value={state}> | ||
<ToastDispatchContext.Provider value={dispatch}>{children}</ToastDispatchContext.Provider> | ||
</ToastStateContext.Provider> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import styled from '@emotion/styled'; | ||
import { createPortal } from 'react-dom'; | ||
|
||
import { Toast } from './Toast'; | ||
import { ToasterProvider } from './ToastContext'; | ||
import { useToast } from './useToast'; | ||
|
||
/** | ||
* Toast Context Provider | ||
* @returns | ||
*/ | ||
export const Toaster = () => { | ||
const { toasts } = useToast(); | ||
|
||
return ( | ||
<ToasterProvider> | ||
{toasts.length > 0 && | ||
createPortal( | ||
<ToastList tabIndex={-1}> | ||
{toasts.map(t => ( | ||
<Toast key={t.toastId} toast={t} /> | ||
))} | ||
</ToastList>, | ||
document.getElementById('toaster') || document.body, | ||
)} | ||
</ToasterProvider> | ||
); | ||
}; | ||
const ToastList = styled.ol` | ||
list-style: none; | ||
position: fixed; | ||
display: flex; | ||
margin: 0; | ||
z-index: 100; | ||
max-height: 100vh; | ||
width: 100%; | ||
padding: 1rem; | ||
top: auto; | ||
right: 0px; | ||
bottom: 0px; | ||
flex-direction: column; | ||
max-width: 25rem; | ||
gap: 1rem; | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export { Toaster } from './Toaster'; | ||
export { Toast, ToastDescription, ToastTitle } from './Toast'; | ||
|
||
// eslint-disable-next-line react-refresh/only-export-components | ||
export { useToast } from './useToast'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { DefaultVariantType } from '@/types/variant'; | ||
|
||
import { Toast } from '.'; | ||
|
||
export type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>; | ||
|
||
export const TOAST_LIMIT = 5; | ||
|
||
// export Type | ||
|
||
export type ToastData = { | ||
/* | ||
Toast unique number | ||
*/ | ||
toastId: number; | ||
/* | ||
Toast Title | ||
*/ | ||
title?: React.ReactNode; | ||
/* | ||
Toast Description | ||
*/ | ||
description?: React.ReactNode; | ||
/* | ||
Toast Variant | ||
*/ | ||
variant?: DefaultVariantType; | ||
/* | ||
Toast duration time. | ||
if time < 0: primitive | ||
*/ | ||
time?: number; | ||
|
||
/* | ||
Toast Custom Data. | ||
It just for action callback | ||
*/ | ||
data?: unknown; | ||
/* | ||
Toast Click Action. | ||
Called when Toast is clicked. | ||
*/ | ||
action?: (data: unknown) => void; | ||
}; | ||
|
||
export interface State { | ||
toasts: ToastData[]; | ||
} | ||
|
||
export type Action = | ||
| { type: 'ADD_TOAST'; toast: ToastData } | ||
| { type: 'UPDATE_TOAST'; toast: ToastData } | ||
| { type: 'REMOVE_TOAST'; toastId: number }; | ||
|
||
export type ToastDispatch = React.Dispatch<Action>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { useEffect, useState } from 'react'; | ||
|
||
import { reducer } from './ToastContext'; | ||
import { Action, State, ToastData } from './type'; | ||
|
||
type ToastProps = Omit<ToastData, 'toastId'>; | ||
|
||
const listeners: Array<(state: State) => void> = []; | ||
|
||
let memoryState: State = { toasts: [] }; | ||
|
||
function dispatch(action: Action) { | ||
memoryState = reducer(memoryState, action); | ||
listeners.forEach(listener => { | ||
listener(memoryState); | ||
}); | ||
} | ||
|
||
let cnt = 1; | ||
const toast = ({ ...props }: ToastProps) => { | ||
const toastId = cnt++; | ||
dispatch({ | ||
type: 'ADD_TOAST', | ||
toast: { | ||
...props, | ||
toastId, | ||
}, | ||
}); | ||
|
||
return { | ||
toastId, | ||
}; | ||
}; | ||
|
||
const removeToast = (toastId: number) => { | ||
dispatch({ type: 'REMOVE_TOAST', toastId }); | ||
}; | ||
|
||
export const useToast = () => { | ||
const [state, setState] = useState<State>(memoryState); | ||
|
||
useEffect(() => { | ||
listeners.push(setState); | ||
|
||
return () => { | ||
const index = listeners.indexOf(setState); | ||
if (index > -1) { | ||
listeners.splice(index, 1); | ||
} | ||
}; | ||
}, [state]); | ||
|
||
return { | ||
...state, | ||
toast, | ||
removeToast, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.