diff --git a/.yarn/cache/react-spring-npm-8.0.27-e2e99c79a8-8f041fd303.zip b/.yarn/cache/react-spring-npm-8.0.27-e2e99c79a8-8f041fd303.zip index 79de276e..309f0411 100644 Binary files a/.yarn/cache/react-spring-npm-8.0.27-e2e99c79a8-8f041fd303.zip and b/.yarn/cache/react-spring-npm-8.0.27-e2e99c79a8-8f041fd303.zip differ diff --git a/package.json b/package.json index 8c70dbb5..de59c7d6 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,10 @@ "url": "https://github.com/Lubycon/lubycon-ui-kit.git" }, "keywords": [ - "lubycon" + "lubycon", + "ui", + "ui kit", + "design system" ], "author": "Lubycon", "dependencies": { diff --git a/src/components/LubyconUIKitProvider/index.tsx b/src/components/LubyconUIKitProvider/index.tsx index 74ae60ab..eb91dfc3 100644 --- a/src/components/LubyconUIKitProvider/index.tsx +++ b/src/components/LubyconUIKitProvider/index.tsx @@ -1,6 +1,5 @@ import React, { ReactNode } from 'react'; import { PortalProvider } from 'contexts/Portal'; -import { SnackbarProvider } from 'contexts/Snackbar'; import { OverlayProvider } from 'contexts/Overlay'; interface Props { @@ -10,9 +9,7 @@ interface Props { function LubyconUIKitProvider({ children }: Props) { return ( - - {children} - + {children} ); } diff --git a/src/components/Snackbar/SnackbarBody.tsx b/src/components/Snackbar/SnackbarBody.tsx deleted file mode 100644 index 6a58db72..00000000 --- a/src/components/Snackbar/SnackbarBody.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { ReactNode, isValidElement, useMemo } 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: buttonProp, onClick }: Props) => { - const button = useMemo( - () => - isValidElement(buttonProp) ? buttonProp : , - [buttonProp] - ); - - return ( -
- - {message} - -
{buttonProp == null ? null : button}
-
- ); -}; - -export default SnackbarBody; diff --git a/src/components/Snackbar/index.tsx b/src/components/Snackbar/index.tsx deleted file mode 100644 index 47b852ce..00000000 --- a/src/components/Snackbar/index.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { useEffect, ReactNode, useMemo } from 'react'; -import { animated, useTransition } from 'react-spring'; -import classnames from 'classnames'; -import SnackbarBody from './SnackbarBody'; -import { CombineElementProps } from 'src/types/utils'; -import { getTranslateAnimation } from './utils'; - -export type SnackbarAlign = 'left' | 'center' | 'right'; - -export type SnackbarProps = Omit< - CombineElementProps< - 'div', - { - show: boolean; - message: string; - button?: ReactNode; - autoHideDuration?: number; - onClose?: () => void; - onShow?: () => void; - onHide?: () => void; - onClick?: () => void; - align?: SnackbarAlign; - } - >, - 'children' ->; - -const Snackbar = ({ - show, - message, - button, - autoHideDuration, - onClose, - onShow, - onHide, - onClick, - className, - style, - align = 'left', - ...rest -}: SnackbarProps) => { - const translateAnimation = useMemo(() => getTranslateAnimation(align), [align]); - const transition = useTransition(show, null, { - from: { - opacity: 0, - transform: translateAnimation.from, - height: 60, - }, - enter: [ - { height: 60 }, - { - opacity: 1, - transform: translateAnimation.to, - }, - ], - leave: [ - { - opacity: 0, - transform: translateAnimation.from, - }, - { height: 0 }, - ], - onStart: () => { - onShow?.(); - }, - onDestroyed: () => { - onHide?.(); - }, - }); - - useEffect(() => { - if (autoHideDuration && onClose == undefined) { - throw Error('autoHideDuration prop은 onClose prop을 함께 제공해야만 합니다.'); - } - }, [autoHideDuration]); - - useEffect(() => { - let timer: NodeJS.Timeout; - if (autoHideDuration != null && show === true) { - timer = setTimeout(() => { - onClose?.(); - }, autoHideDuration); - } - - return () => clearTimeout(timer); - }, []); - - return ( - <> - {transition.map(({ item, key, props }) => { - return item ? ( - - - - ) : null; - })} - - ); -}; - -export default Snackbar; diff --git a/src/components/Snackbar/utils.ts b/src/components/Snackbar/utils.ts deleted file mode 100644 index 1c48ba27..00000000 --- a/src/components/Snackbar/utils.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { SnackbarAlign } from '.'; - -export const getTranslateAnimation = (align: SnackbarAlign) => { - switch (align) { - case 'left': - return { - from: 'translateX(-100%)', - to: 'translateX(0)', - }; - case 'center': - return { - from: 'translateY(100%)', - to: 'translateY(0)', - }; - case 'right': - return { - from: 'translateX(100%)', - to: 'translateX(0)', - }; - } -}; diff --git a/src/components/TransitionMotion/index.tsx b/src/components/TransitionMotion/index.tsx new file mode 100644 index 00000000..816c64fd --- /dev/null +++ b/src/components/TransitionMotion/index.tsx @@ -0,0 +1,52 @@ +import React, { CSSProperties, ElementType, PropsWithChildren } from 'react'; +import { TransitionKeyProps, useTransition, animated } from 'react-spring'; +import { OverridableProps } from 'src/types/OverridableProps'; + +type Props = OverridableProps< + E, + { + flag: boolean; + keys?: TransitionKeyProps | TransitionKeyProps[] | null; + from?: Partial; + enter?: Partial | Partial[]; + leave?: Partial | Partial[]; + onStart?: () => void; + onDestroyed?: () => void; + } +>; +const TransitionMotion = ({ + flag, + keys = null, + from, + enter, + leave, + children, + onStart, + onDestroyed, + as, + style, + ...rest +}: PropsWithChildren) => { + const transitions = useTransition(flag, keys, { + from, + enter, + leave, + onStart, + onDestroyed, + }); + const Component = animated[as ?? 'div']; + + return ( + <> + {transitions.map(({ item, key, props }) => { + return item ? ( + + {children} + + ) : null; + })} + + ); +}; + +export default TransitionMotion; diff --git a/src/contexts/Snackbar.tsx b/src/contexts/Snackbar.tsx deleted file mode 100644 index c7d829f2..00000000 --- a/src/contexts/Snackbar.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import React, { ReactNode, createContext, useState, useCallback, useContext } from 'react'; -import classnames from 'classnames'; -import Snackbar, { SnackbarAlign, SnackbarProps } from 'components/Snackbar'; -import { generateID } from 'src/utils'; -import { Portal } from './Portal'; -import { isMatchedSM } from 'src/utils/mediaQuery'; - -type SnackbarOptions = Omit; - -const aligns: SnackbarAlign[] = ['left', 'center', 'right']; - -interface SnackbarGlobalState { - openSnackbar: (option: SnackbarOptions) => string; - closeSnackbar: (toastId: string) => void; -} -const SnackbarContext = createContext({ - openSnackbar: () => '', - closeSnackbar: () => {}, -}); - -interface SnackbarProviderProps { - children: ReactNode; - maxStack?: number; -} -export function SnackbarProvider({ children, maxStack = 3 }: SnackbarProviderProps) { - const [openedSnackbarQueue, setOpenedSnackbarQueue] = useState([]); - - const openSnackbar = useCallback( - ({ - id = generateID('lubycon-snackbar'), - align: rawAlign = 'left', - ...option - }: SnackbarOptions) => { - const align = isMatchedSM() ? 'center' : rawAlign; - const snackbar = { id, align, show: true, ...option }; - const [, ...rest] = openedSnackbarQueue; - - if (openedSnackbarQueue.length >= maxStack) { - setOpenedSnackbarQueue([...rest, snackbar]); - } else { - setOpenedSnackbarQueue([...openedSnackbarQueue, snackbar]); - } - return id; - }, - [openedSnackbarQueue] - ); - - const closeSnackbar = useCallback((closeSnackbarId?: string) => { - setOpenedSnackbarQueue((queue) => - queue.map((snackbar) => { - return snackbar.id === closeSnackbarId - ? { - ...snackbar, - show: false, - } - : snackbar; - }) - ); - }, []); - - const removeSnackbarFromQueue = useCallback( - (closedSnackbarId: string) => { - setOpenedSnackbarQueue( - openedSnackbarQueue.filter((snackbar) => snackbar.id !== closedSnackbarId) - ); - }, - [openedSnackbarQueue] - ); - - return ( - - {children} - - {aligns.map((align) => ( -
- {openedSnackbarQueue - .filter((snackbar) => snackbar.align === align) - .map(({ id, show, onHide, autoHideDuration = 3000, ...snackbarProps }) => ( - { - removeSnackbarFromQueue(id ?? ''); - onHide?.(); - }} - align={align} - onClose={() => closeSnackbar(id ?? '')} - {...snackbarProps} - /> - ))} -
- ))} -
-
- ); -} - -export function useSnackbar() { - return useContext(SnackbarContext); -} diff --git a/src/index.ts b/src/index.ts index 5d126c26..9d708c30 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,11 +13,9 @@ export { CardImageContent, CardFooter, } from './components/Card'; -export { default as Snackbar } from './components/Snackbar'; export { default as Input } from './components/Input'; export { default as Accordion } from './components/Accordion'; export { Portal } from './contexts/Portal'; -export { useSnackbar } from './contexts/Snackbar'; export { colors } from './constants/colors'; export { default as Icon } from './components/Icon'; export { default as Shadow } from './components/Shadow'; @@ -25,3 +23,4 @@ export { default as Spacing } from './components/Spacing'; export { default as useProgress } from './hooks/useProgress'; export { default as useResizeObserver } from './hooks/useResizeObserver'; export { useOverlay } from './contexts/Overlay'; +export { default as TransitionMotion } from './components/TransitionMotion'; diff --git a/src/stories/Components/Modal/Modal/index.tsx b/src/stories/Components/Modal/Modal/index.tsx index dd69a7cb..8f98ac82 100644 --- a/src/stories/Components/Modal/Modal/index.tsx +++ b/src/stories/Components/Modal/Modal/index.tsx @@ -2,8 +2,8 @@ import React, { ReactElement, cloneElement, useRef, useCallback, useEffect, Chil import ModalBackdrop from './ModalBackdrop'; import ModalWindow from './ModalWindow'; import { generateID } from 'utils/index'; -import { animated, useTransition } from 'react-spring'; import { CombineElementProps } from 'src/types/utils'; +import { TransitionMotion } from 'src'; export type ModalProps = CombineElementProps< 'div', @@ -26,17 +26,6 @@ const Modal = ({ ...props }: ModalProps) => { const backdropRef = useRef(null); - const backdropTransition = useTransition(show, null, { - from: { opacity: 0 }, - enter: { opacity: 1 }, - leave: { opacity: 0 }, - }); - const modalTransition = useTransition(show, null, { - from: { transform: 'translate(-50%, 100%)', opacity: 0 }, - enter: { transform: 'translate(-50%, -50%)', opacity: 1 }, - leave: { transform: 'translate(-50%, 100%)', opacity: 0 }, - onDestroyed: () => onCloseTransitionEnd?.(), - }); const handleBackdropClick = useCallback( (event: React.MouseEvent) => { @@ -64,33 +53,31 @@ const Modal = ({ return (
- {backdropTransition.map( - ({ item: show, key, props }) => - show && ( - - - - ) - )} - {modalTransition.map( - ({ item: show, key, props: animationProps }) => - show && ( - - - {Children.map(children, (child) => { - return cloneElement(child, { - key: generateID('lubycon-modal__children'), - size: size, - }); - })} - - - ) - )} + + + + onCloseTransitionEnd?.()} + > + + {Children.map(children, (child) => { + return cloneElement(child, { + key: generateID('lubycon-modal__children'), + size: size, + }); + })} + +
); }; diff --git a/src/stories/Components/Snackbar/Components.tsx b/src/stories/Components/Snackbar/Components.tsx deleted file mode 100644 index 68c91878..00000000 --- a/src/stories/Components/Snackbar/Components.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import React, { useState } from 'react'; -import { Snackbar, Button, useSnackbar } from 'src'; -import { SnackbarAlign } from 'src/components/Snackbar'; - -export const OpenSnackbar = () => { - const [show, setShow] = useState(false); - const handleClose = () => setShow(false); - - return ( -
- - -
- ); -}; - -export const LongText = () => { - const [show, setShow] = useState(false); - const handleClose = () => setShow(false); - - return ( -
- - -
- ); -}; - -export const AutoHide = () => { - const [show, setShow] = useState(true); - const handleClose = () => setShow(false); - - return ( -
- - setShow(true)} - onClose={() => setShow(false)} - message={`16개의 이미지가\n“동물" 폴더에 추가되었습니다.`} - button="실행취소" - onClick={handleClose} - /> -
- ); -}; - -export const SnackbarHooks = () => { - const { openSnackbar, closeSnackbar } = useSnackbar(); - - return ( -
- -
- ); -}; - -const aligns: SnackbarAlign[] = ['left', 'center', 'right']; -export const Aligns = () => { - const { openSnackbar, closeSnackbar } = useSnackbar(); - return ( -
- {aligns.map((align) => ( - - ))} -
- ); -}; - -export const onClick = () => { - const { openSnackbar, closeSnackbar } = useSnackbar(); - const cancelExecution = (id: string) => { - alert('실행 취소 완료'); - closeSnackbar(id); - }; - return ( -
- -
- ); -}; - -export const MultipleButton = () => { - const { openSnackbar, closeSnackbar } = useSnackbar(); - const cancelExecution = (id: string) => { - alert('실행 취소 완료'); - closeSnackbar(id); - }; - return ( -
- - - - ), - }); - }} - > - 스낵바 열기 - -
- ); -}; diff --git a/src/stories/Components/Snackbar/index.stories.mdx b/src/stories/Components/Snackbar/index.stories.mdx deleted file mode 100644 index 10da0c65..00000000 --- a/src/stories/Components/Snackbar/index.stories.mdx +++ /dev/null @@ -1,94 +0,0 @@ -import { Meta, Story, Canvas } from '@storybook/addon-docs/blocks'; -import { Snackbar, Button } from 'src'; -import { - OpenSnackbar, - LongText, - AutoHide, - SnackbarHooks, - Aligns, - MultipleButton, -} from './Components'; - - - -# Snackbar - -스낵바는 일종의 가이드 팝업입니다. 사용자에게 수행된 작업이 무엇인지 알려주거나, 어떤 것을 수행해야 하는지 알려주는데 활용할 수 있습니다. - -> 주의사항 -> -> - '닫기' 또는 '취소'의 구현은 사용자의 선택 사항입니다 -> - 한 번에 하나의 스낵바만 표현됩니다 -> - 스낵바는 모달보다 우선순위가 낮습니다. 따라서 사용자의 경험을 방해하지 않는다는 원칙 하에 배치되며, 시간의 경과에 따라 자동으로 사라집니다 -> - 모바일에서 텍스트는 최대 두 줄까지 입력 가능합니다 - -## Preview - -기본 형태입니다. 또한 스낵바 컴포넌트의 버튼에 이벤트를 할당할 수 있습니다. 해당 버튼 클릭시 별도의 가이드 팝업이 나오는 등 사용자가 원하는 인터렉션을 추가할 수 있습니다. - - - - - console.log('버튼이 클릭되었습니다')}>닫기} - /> - - - -## Open - - - - - - - -## Long Text - - - - - - - -## Auto Hide - -정해진 시간이 지나면 스낵바가 자동으로 사라지도록 만듭니다. - - - - - - - -## With Hooks - -스낵바 컴포넌트는 명령형으로 렌더하는 방법이 있습니다. 별도의 컨텍스트를 제공합니다. 명령형으로 렌더된 스낵바는 3초 뒤 자동으로 사라집니다 - - - - - - - -## Aligns - -스낵바가 나타나는 위치를 결정할 수 있습니다. 기본적으로는 뷰포트의 왼쪽에 렌더됩니다 - - - - - - - -## Multiple Buttons - -스낵바 컴포넌트에 여러 개의 버튼을 할당할 수 있습니다 - - - - - -