From 3b76bbe142427000f25ec8c20413f54a3800bfba Mon Sep 17 00:00:00 2001 From: Evan Moon Date: Sat, 20 Mar 2021 16:13:32 +0900 Subject: [PATCH] =?UTF-8?q?feat(ui-kit):=20Modal=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=95=A0=EB=8B=88=EB=A9=94=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ui-kit): Modal 컴포넌트 애니메이션 추가 * 사용하지 않는 Param 제거 * fix type * fix lint * 모달 hooks 애니메이션 수정 * 외부에서 closeModal을 호출해도 애니메이션이 종료된 후 모달이 사라지도록 수정 * remove eqeq * 의미없는 classnames 호출 제거 --- ui-kit/src/components/Modal/index.tsx | 78 +++++++++++++++++------ ui-kit/src/components/Table/TableCell.tsx | 2 +- ui-kit/src/components/Table/TableHead.tsx | 4 +- ui-kit/src/components/Table/TableRow.tsx | 7 +- ui-kit/src/contexts/Modal.tsx | 72 ++++++++++----------- ui-kit/src/sass/components/_Modal.scss | 25 ++++---- ui-kit/src/stories/Table.stories.tsx | 2 +- 7 files changed, 109 insertions(+), 81 deletions(-) diff --git a/ui-kit/src/components/Modal/index.tsx b/ui-kit/src/components/Modal/index.tsx index 482f5a02..dc71b172 100644 --- a/ui-kit/src/components/Modal/index.tsx +++ b/ui-kit/src/components/Modal/index.tsx @@ -1,9 +1,9 @@ -import React, { Children, ReactElement, cloneElement, useRef } from 'react'; +import React, { ReactElement, cloneElement, useRef, useCallback, useEffect, Children } from 'react'; import ModalBackdrop from './ModalBackdrop'; import ModalWindow from './ModalWindow'; -import classnames from 'classnames'; import { generateID } from 'utils/index'; -import { useEffect } from 'react'; +import { animated, useTransition } from 'react-spring'; +import { useState } from 'react'; export interface ModalProps extends React.HTMLAttributes { show: boolean; @@ -14,15 +14,38 @@ export interface ModalProps extends React.HTMLAttributes { } const Modal = ({ show, size = 'small', children, onOpen, onClose }: ModalProps) => { + const [showModal, setShowModal] = useState(show); const backdropRef = useRef(null); - const handleBackdropClick = (event: React.MouseEvent) => { - if (backdropRef.current == null) return; - if (event.target === backdropRef.current) onClose?.(); - }; + const backdropTransition = useTransition(showModal, null, { + from: { opacity: 0 }, + enter: { opacity: 1 }, + leave: { opacity: 0 }, + }); + const modalTransition = useTransition(showModal, null, { + from: { transform: 'translate(-50%, 100%)', opacity: 0 }, + enter: { transform: 'translate(-50%, -50%)', opacity: 1 }, + leave: { transform: 'translate(-50%, 100%)', opacity: 0 }, + onStart: () => onOpen?.(), + onDestroyed: () => onClose?.(), + }); + + const handleBackdropClick = useCallback( + (event: React.MouseEvent) => { + if (backdropRef.current == null) { + return; + } else if (event.target === backdropRef.current) { + setShowModal(false); + } + }, + [onClose] + ); const onKeydown = (event: KeyboardEvent) => { - if (event.key === 'Escape') onClose?.(); + if (event.key === 'Escape') { + setShowModal(false); + } }; + useEffect(() => { window.addEventListener('keydown', onKeydown); return () => { @@ -31,21 +54,36 @@ const Modal = ({ show, size = 'small', children, onOpen, onClose }: ModalProps) }, []); useEffect(() => { - if (show === true) { - onOpen?.(); - } + setShowModal(show); }, [show]); - return show ? ( -
- - - {Children.map(children, (child) => - cloneElement(child, { size: size, key: generateID('lubycon-modal__children') }) - )} - + return ( +
+ {backdropTransition.map( + ({ item: show, key, props }) => + show && ( + + + + ) + )} + {modalTransition.map( + ({ item: show, key, props }) => + show && ( + + + {Children.map(children, (child) => { + return cloneElement(child, { + key: generateID('lubycon-modal__children'), + size: size, + }); + })} + + + ) + )}
- ) : null; + ); }; export default Modal; diff --git a/ui-kit/src/components/Table/TableCell.tsx b/ui-kit/src/components/Table/TableCell.tsx index 3c78d9f1..7811f74f 100644 --- a/ui-kit/src/components/Table/TableCell.tsx +++ b/ui-kit/src/components/Table/TableCell.tsx @@ -13,7 +13,7 @@ const TableCell = ({ children, align = 'left', as }: TableCellProps) => { const Component = as ?? isHeadCell; return ( - + {children} ); diff --git a/ui-kit/src/components/Table/TableHead.tsx b/ui-kit/src/components/Table/TableHead.tsx index 9aea8007..4cc0a923 100644 --- a/ui-kit/src/components/Table/TableHead.tsx +++ b/ui-kit/src/components/Table/TableHead.tsx @@ -7,9 +7,7 @@ const TableHeadContext = createContext({ variant: '' }); const TableHead = ({ children }: TableProps) => { return ( - - {children} - + {children} ); }; diff --git a/ui-kit/src/components/Table/TableRow.tsx b/ui-kit/src/components/Table/TableRow.tsx index 2906dfef..4fbfedae 100644 --- a/ui-kit/src/components/Table/TableRow.tsx +++ b/ui-kit/src/components/Table/TableRow.tsx @@ -1,13 +1,8 @@ import React from 'react'; -import classnames from 'classnames'; import { TableProps } from './index'; const TableRow = ({ children }: TableProps) => { - return ( - - {children} - - ); + return {children}; }; export default TableRow; diff --git a/ui-kit/src/contexts/Modal.tsx b/ui-kit/src/contexts/Modal.tsx index 9ef77cc9..ae84c961 100644 --- a/ui-kit/src/contexts/Modal.tsx +++ b/ui-kit/src/contexts/Modal.tsx @@ -1,25 +1,17 @@ -import React, { - ReactElement, - useContext, - ReactNode, - createContext, - useState, - useCallback, - isValidElement, -} from 'react'; -import Modal, { ModalProps } from 'components/Modal'; +import React, { useContext, ReactNode, createContext, useState, useCallback } from 'react'; +import Modal, { ModalContent, ModalFooter, ModalHeader, ModalProps } from 'components/Modal'; import { generateID } from 'src/utils'; import { Portal } from './Portal'; -import { cloneElement } from 'react'; -interface ModalOptions extends Omit { - header?: ReactElement; - content: ReactElement; - footer: ReactElement; -} -interface ModalStackOptions extends Omit { - reactElements: ReactElement[]; +interface ModalHookOption { + header?: ReactNode; + content?: ReactNode; + footer?: ReactNode; } + +type ModalOptions = ModalHookOption & Omit; +type ModalStackOptions = ModalHookOption & Omit; + interface ModalGlobalState { openModal: (option: ModalOptions) => string; closeModal: (modalId: string) => void; @@ -37,20 +29,30 @@ export function ModalProvider({ children }: ModalProviderProps) { const [openedModalStack, setOpenedModalStack] = useState([]); const openModal = useCallback( - ({ id = generateID('lubycon-modal'), header, content, footer, ...option }: ModalOptions) => { - const reactElements = isValidElement(header) ? [header, content, footer] : [content, footer]; - const modal = { id, reactElements, ...option }; + ({ id = generateID('lubycon-modal'), ...option }: ModalOptions) => { + const modal = { id, show: true, ...option }; setOpenedModalStack([...openedModalStack, modal]); return id; }, [openedModalStack] ); - const closeModal = useCallback( - (closedModalId: string) => { - setOpenedModalStack(openedModalStack.filter((modal) => modal.id !== closedModalId)); - }, - [openedModalStack] - ); + + const closeModal = useCallback((closedModalId: string) => { + setOpenedModalStack((stack) => + stack.map((modal) => { + return modal.id === closedModalId + ? { + ...modal, + show: false, + } + : modal; + }) + ); + }, []); + + const removeModalFromStack = (closedModalId: string) => { + setOpenedModalStack((stack) => stack.filter((modal) => modal.id !== closedModalId)); + }; return ( {children} - {openedModalStack.map(({ id, reactElements, size = 'small', ...modalProps }) => ( + {openedModalStack.map(({ id, show, title, content, footer, ...modalProps }) => ( closeModal(id ?? '')} - size={size} + onClose={() => removeModalFromStack(id ?? '')} {...modalProps} > - {reactElements.map((element) => { - return cloneElement(element, { - key: generateID('lubycon-modal__children'), - size: size, - }); - })} + {title} + {content} + {footer} ))} diff --git a/ui-kit/src/sass/components/_Modal.scss b/ui-kit/src/sass/components/_Modal.scss index d5cc0bfa..16cd0476 100644 --- a/ui-kit/src/sass/components/_Modal.scss +++ b/ui-kit/src/sass/components/_Modal.scss @@ -1,25 +1,24 @@ .lubycon-modal { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - overflow: auto; - z-index: 1000; - &__overlay { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; background-color: get-color('gray100'); opacity: 0.5; + z-index: 1000; + } + &__window-wrapper { + position: fixed; + left: 50%; + top: 50%; + z-index: 1001; } &__window { background-color: get-color('gray10'); border-radius: 4px; box-sizing: border-box; - z-index: 1001; &--small { width: 280px; padding: 16px 20px; diff --git a/ui-kit/src/stories/Table.stories.tsx b/ui-kit/src/stories/Table.stories.tsx index 03578312..d353af00 100644 --- a/ui-kit/src/stories/Table.stories.tsx +++ b/ui-kit/src/stories/Table.stories.tsx @@ -21,7 +21,7 @@ export const Default = () => { - {iterator.map((v, rowIdx) => ( + {iterator.map((_, rowIdx) => ( {contents.map((content, contentIdx) => ( {content}