-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat(ui-kit): 스낵바 컴포넌트 추가 * feat(ui-kit): 스낵바 타입 업데이트 * feat(ui-kit): Snackbar 컴포넌트 타입에서 children 제거 * feat(ui-kit): 스낵바에 버튼이 연속으로 등장할 때 간격 설정 * feat(ui-kit): 의미없는 모듈 임포트 제거
- Loading branch information
Showing
10 changed files
with
344 additions
and
6 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
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,25 @@ | ||
import React, { ReactNode, isValidElement } from 'react'; | ||
import classnames from 'classnames'; | ||
import Text from 'components/Text'; | ||
import Button from '../Button'; | ||
|
||
interface Props { | ||
message: string; | ||
button: ReactNode; | ||
onClick?: () => void; | ||
} | ||
|
||
const SnackbarBody = ({ message, button, onClick }: Props) => { | ||
return ( | ||
<div className={classnames('lubycon-snackbar__body', 'lubycon-shadow--3')}> | ||
<Text typography="p2" className="lubycon-snackbar__text"> | ||
{message} | ||
</Text> | ||
<div className="lubycon-snackbar__body__buttons"> | ||
{isValidElement(button) ? button : <Button onClick={onClick}>{button}</Button>} | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default SnackbarBody; |
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,93 @@ | ||
import React, { HTMLAttributes, useEffect, useState, ReactNode } from 'react'; | ||
import { animated, useTransition } from 'react-spring'; | ||
import classnames from 'classnames'; | ||
import SnackbarBody from './SnackbarBody'; | ||
import { Combine } from 'src/types/utils'; | ||
|
||
export type SnackbarProps = Combine< | ||
{ | ||
show: boolean; | ||
message: string; | ||
button: ReactNode; | ||
autoHideDuration?: number; | ||
onShow?: () => void; | ||
onHide?: () => void; | ||
onClick?: () => void; | ||
}, | ||
Omit<HTMLAttributes<HTMLDivElement>, 'children'> | ||
>; | ||
|
||
const Snackbar = ({ | ||
show, | ||
message, | ||
button, | ||
autoHideDuration, | ||
onShow, | ||
onHide, | ||
onClick, | ||
className, | ||
style, | ||
...rest | ||
}: SnackbarProps) => { | ||
const [isOpen, setOpen] = useState(show); | ||
const transition = useTransition(isOpen, null, { | ||
from: { | ||
opacity: 0, | ||
transform: 'translateX(-100%)', | ||
height: 60, | ||
}, | ||
enter: [ | ||
{ height: 60 }, | ||
{ | ||
opacity: 1, | ||
transform: 'translateX(0)', | ||
}, | ||
], | ||
leave: [ | ||
{ | ||
opacity: 0, | ||
transform: 'translateX(-100%)', | ||
}, | ||
{ height: 0 }, | ||
], | ||
onStart: () => { | ||
onShow?.(); | ||
}, | ||
onDestroyed: () => { | ||
onHide?.(); | ||
}, | ||
}); | ||
|
||
useEffect(() => { | ||
let timer: NodeJS.Timeout; | ||
if (autoHideDuration != null && isOpen === true) { | ||
timer = setTimeout(() => { | ||
setOpen(false); | ||
}, autoHideDuration); | ||
} | ||
|
||
return () => clearTimeout(timer); | ||
}, []); | ||
|
||
return ( | ||
<> | ||
{transition.map(({ item, key, props }) => { | ||
return item ? ( | ||
<animated.div | ||
key={key} | ||
className={classnames('lubycon-snackbar', className)} | ||
style={{ | ||
...style, | ||
...props, | ||
}} | ||
{...rest} | ||
> | ||
<SnackbarBody message={message} button={button} onClick={onClick} /> | ||
</animated.div> | ||
) : null; | ||
})} | ||
</> | ||
); | ||
}; | ||
|
||
export default Snackbar; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import React, { ReactNode, createContext, useState, useCallback, useContext } from 'react'; | ||
import classnames from 'classnames'; | ||
import Snackbar, { SnackbarProps } from 'components/Snackbar'; | ||
import { generateID } from 'src/utils'; | ||
import { Portal } from './Portal'; | ||
interface SnackbarOptions extends Omit<SnackbarProps, 'show'> { | ||
duration?: number; | ||
} | ||
|
||
interface SnackbarGlobalState { | ||
openSnackbar: (option: SnackbarOptions) => void; | ||
closeSnackbar: (toastId: string) => void; | ||
} | ||
const SnackbarContext = createContext<SnackbarGlobalState>({ | ||
openSnackbar: () => {}, | ||
closeSnackbar: () => {}, | ||
}); | ||
|
||
interface SnackbarProviderProps { | ||
children: ReactNode; | ||
maxStack?: number; | ||
} | ||
export function SnackbarProvider({ children, maxStack = 1 }: SnackbarProviderProps) { | ||
const [openedSnackbarQueue, setOpenedSnackbarQueue] = useState<SnackbarOptions[]>([]); | ||
|
||
const openSnackbar = useCallback( | ||
({ id = generateID('lubycon-snackbar'), ...option }: SnackbarOptions) => { | ||
const snackbar = { id, ...option }; | ||
const [, ...rest] = openedSnackbarQueue; | ||
|
||
if (openedSnackbarQueue.length >= maxStack) { | ||
setOpenedSnackbarQueue([...rest, snackbar]); | ||
} else { | ||
setOpenedSnackbarQueue([...openedSnackbarQueue, snackbar]); | ||
} | ||
}, | ||
[openedSnackbarQueue] | ||
); | ||
|
||
const closeSnackbar = useCallback( | ||
(closedSnackbarId: string) => { | ||
setOpenedSnackbarQueue( | ||
openedSnackbarQueue.filter((snackbar) => snackbar.id !== closedSnackbarId) | ||
); | ||
}, | ||
[openedSnackbarQueue] | ||
); | ||
|
||
return ( | ||
<SnackbarContext.Provider | ||
value={{ | ||
openSnackbar, | ||
closeSnackbar, | ||
}} | ||
> | ||
{children} | ||
<Portal> | ||
<div className={classnames('lubycon-snackbar__context-container')}> | ||
{openedSnackbarQueue.map(({ id, onHide, duration = 3000, ...snackbarProps }) => ( | ||
<Snackbar | ||
key={id} | ||
show={true} | ||
autoHideDuration={duration} | ||
onHide={() => { | ||
closeSnackbar(id ?? ''); | ||
onHide?.(); | ||
}} | ||
{...snackbarProps} | ||
/> | ||
))} | ||
</div> | ||
</Portal> | ||
</SnackbarContext.Provider> | ||
); | ||
} | ||
|
||
export function useSnackbar() { | ||
return useContext(SnackbarContext); | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
.lubycon-snackbar { | ||
overflow: visible; | ||
|
||
.lubycon-snackbar__body { | ||
display: inline-flex; | ||
justify-content: space-between; | ||
align-items: center; | ||
padding: 8px 16px; | ||
min-width: 400px; | ||
border-radius: 4px; | ||
background-color: white; | ||
margin: 12px 0; | ||
} | ||
|
||
.lubycon-snackbar__text { | ||
white-space: pre; | ||
} | ||
|
||
.lubycon-snackbar__body__buttons { | ||
margin-left: 16px; | ||
.lubycon-button + .lubycon-button { | ||
margin-left: 8px; | ||
} | ||
} | ||
|
||
&:first-of-type { | ||
.lubycon-snackbar__body { | ||
margin-bottom: 0; | ||
} | ||
} | ||
} | ||
|
||
.lubycon-snackbar__context-container { | ||
position: fixed; | ||
display: flex; | ||
flex-direction: column-reverse; | ||
top: auto; | ||
bottom: 40px; | ||
left: 40px; | ||
right: auto; | ||
} |
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 |
---|---|---|
|
@@ -10,3 +10,4 @@ | |
@import './Toast'; | ||
@import './Tooltip'; | ||
@import './Tabs'; | ||
@import './Snackbar'; |
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,99 @@ | ||
import React, { useState } from 'react'; | ||
import { Meta } from '@storybook/react/types-6-0'; | ||
import Snackbar from 'components/Snackbar'; | ||
import Button from 'components/Button'; | ||
import { useSnackbar } from 'contexts/Snackbar'; | ||
|
||
export default { | ||
title: 'Lubycon UI Kit/Snackbar', | ||
component: Snackbar, | ||
} as Meta; | ||
|
||
export const Default = () => { | ||
return ( | ||
<div> | ||
<Snackbar show={true} message="데이터 전송이 완료되었습니다." button="실행취소" /> | ||
<Snackbar | ||
show={true} | ||
message={`16개의 이미지가\n“동물" 폴더에 추가되었습니다.`} | ||
button="실행취소" | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
export const AutoHide = () => { | ||
const [show, setShow] = useState(true); | ||
return ( | ||
<div> | ||
<Snackbar show={true} message="데이터 전송이 완료되었습니다." button="실행취소" /> | ||
<Snackbar | ||
show={show} | ||
autoHideDuration={3000} | ||
onHide={() => setShow(true)} | ||
message={`16개의 이미지가\n“동물" 폴더에 추가되었습니다.`} | ||
button="실행취소" | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
export const SnackbarHooks = () => { | ||
const { openSnackbar } = useSnackbar(); | ||
return ( | ||
<div> | ||
<Button | ||
onClick={() => | ||
openSnackbar({ | ||
message: `파일이 휴지통으로 이동되었습니다.`, | ||
button: '실행취소', | ||
}) | ||
} | ||
> | ||
스낵바 열기 | ||
</Button> | ||
</div> | ||
); | ||
}; | ||
|
||
export const onClick = () => { | ||
const { openSnackbar } = useSnackbar(); | ||
return ( | ||
<div> | ||
<Button | ||
onClick={() => | ||
openSnackbar({ | ||
message: `파일이 휴지통으로 이동되었습니다.`, | ||
button: '실행취소', | ||
onClick: () => alert('실행 취소 완료'), | ||
}) | ||
} | ||
> | ||
스낵바 열기 | ||
</Button> | ||
</div> | ||
); | ||
}; | ||
|
||
export const multipleButton = () => { | ||
const { openSnackbar } = useSnackbar(); | ||
return ( | ||
<div> | ||
<Button | ||
onClick={() => | ||
openSnackbar({ | ||
message: '메세지가 전송되었습니다.', | ||
button: ( | ||
<> | ||
<Button onClick={() => alert('실행 취소 완료')}>실행취소</Button> | ||
<Button onClick={() => alert('메세지 보기 클릭')}>메세지 보기</Button> | ||
</> | ||
), | ||
}) | ||
} | ||
> | ||
스낵바 열기 | ||
</Button> | ||
</div> | ||
); | ||
}; |
Oops, something went wrong.