Skip to content

Commit

Permalink
feat(ui-kit): Modal 컴포넌트 애니메이션 추가 (#57)
Browse files Browse the repository at this point in the history
* feat(ui-kit): Modal 컴포넌트 애니메이션 추가

* 사용하지 않는 Param 제거

* fix type

* fix lint

* 모달 hooks 애니메이션 수정

* 외부에서 closeModal을 호출해도 애니메이션이 종료된 후 모달이 사라지도록 수정

* remove eqeq

* 의미없는 classnames 호출 제거
  • Loading branch information
evan-moon authored Mar 20, 2021
1 parent b057271 commit 3b76bbe
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 81 deletions.
78 changes: 58 additions & 20 deletions ui-kit/src/components/Modal/index.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement> {
show: boolean;
Expand All @@ -14,15 +14,38 @@ export interface ModalProps extends React.HTMLAttributes<HTMLDivElement> {
}

const Modal = ({ show, size = 'small', children, onOpen, onClose }: ModalProps) => {
const [showModal, setShowModal] = useState(show);
const backdropRef = useRef(null);
const handleBackdropClick = (event: React.MouseEvent<HTMLDivElement>) => {
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<HTMLDivElement>) => {
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 () => {
Expand All @@ -31,21 +54,36 @@ const Modal = ({ show, size = 'small', children, onOpen, onClose }: ModalProps)
}, []);

useEffect(() => {
if (show === true) {
onOpen?.();
}
setShowModal(show);
}, [show]);

return show ? (
<div className={classnames('lubycon-modal')} tabIndex={-1} aria-hidden={true}>
<ModalBackdrop onClick={handleBackdropClick} ref={backdropRef} />
<ModalWindow size={size}>
{Children.map(children, (child) =>
cloneElement(child, { size: size, key: generateID('lubycon-modal__children') })
)}
</ModalWindow>
return (
<div className="lubycon-modal" tabIndex={-1} aria-hidden={true}>
{backdropTransition.map(
({ item: show, key, props }) =>
show && (
<animated.div key={key} style={props}>
<ModalBackdrop onClick={handleBackdropClick} ref={backdropRef} />
</animated.div>
)
)}
{modalTransition.map(
({ item: show, key, props }) =>
show && (
<animated.div key={key} style={props} className="lubycon-modal__window-wrapper">
<ModalWindow size={size}>
{Children.map(children, (child) => {
return cloneElement(child, {
key: generateID('lubycon-modal__children'),
size: size,
});
})}
</ModalWindow>
</animated.div>
)
)}
</div>
) : null;
);
};

export default Modal;
Expand Down
2 changes: 1 addition & 1 deletion ui-kit/src/components/Table/TableCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const TableCell = ({ children, align = 'left', as }: TableCellProps) => {
const Component = as ?? isHeadCell;

return (
<Component className={classnames("lubycon-table__cell", `lubycon-table--align-${align}`)}>
<Component className={classnames('lubycon-table__cell', `lubycon-table--align-${align}`)}>
{children}
</Component>
);
Expand Down
4 changes: 1 addition & 3 deletions ui-kit/src/components/Table/TableHead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ const TableHeadContext = createContext({ variant: '' });
const TableHead = ({ children }: TableProps) => {
return (
<TableHeadContext.Provider value={{ variant: 'head' }}>
<thead className="lubycon-table__head">
{children}
</thead>
<thead className="lubycon-table__head">{children}</thead>
</TableHeadContext.Provider>
);
};
Expand Down
7 changes: 1 addition & 6 deletions ui-kit/src/components/Table/TableRow.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import React from 'react';
import classnames from 'classnames';
import { TableProps } from './index';

const TableRow = ({ children }: TableProps) => {
return (
<tr className={classnames('lubycon-table__row')}>
{children}
</tr>
);
return <tr className="lubycon-table__row">{children}</tr>;
};

export default TableRow;
72 changes: 35 additions & 37 deletions ui-kit/src/contexts/Modal.tsx
Original file line number Diff line number Diff line change
@@ -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<ModalProps, 'show' | 'children'> {
header?: ReactElement;
content: ReactElement;
footer: ReactElement;
}
interface ModalStackOptions extends Omit<ModalOptions, 'header' | 'content' | 'footer'> {
reactElements: ReactElement[];
interface ModalHookOption {
header?: ReactNode;
content?: ReactNode;
footer?: ReactNode;
}

type ModalOptions = ModalHookOption & Omit<ModalProps, 'show' | 'children'>;
type ModalStackOptions = ModalHookOption & Omit<ModalProps, 'children'>;

interface ModalGlobalState {
openModal: (option: ModalOptions) => string;
closeModal: (modalId: string) => void;
Expand All @@ -37,20 +29,30 @@ export function ModalProvider({ children }: ModalProviderProps) {
const [openedModalStack, setOpenedModalStack] = useState<ModalStackOptions[]>([]);

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 (
<ModalContext.Provider
Expand All @@ -61,20 +63,16 @@ export function ModalProvider({ children }: ModalProviderProps) {
>
{children}
<Portal>
{openedModalStack.map(({ id, reactElements, size = 'small', ...modalProps }) => (
{openedModalStack.map(({ id, show, title, content, footer, ...modalProps }) => (
<Modal
show={true}
show={show}
key={id}
onClose={() => closeModal(id ?? '')}
size={size}
onClose={() => removeModalFromStack(id ?? '')}
{...modalProps}
>
{reactElements.map((element) => {
return cloneElement(element, {
key: generateID('lubycon-modal__children'),
size: size,
});
})}
<ModalHeader>{title}</ModalHeader>
<ModalContent>{content}</ModalContent>
<ModalFooter>{footer}</ModalFooter>
</Modal>
))}
</Portal>
Expand Down
25 changes: 12 additions & 13 deletions ui-kit/src/sass/components/_Modal.scss
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
2 changes: 1 addition & 1 deletion ui-kit/src/stories/Table.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const Default = () => {
</TableRow>
</TableHead>
<TableBody>
{iterator.map((v, rowIdx) => (
{iterator.map((_, rowIdx) => (
<TableRow key={`tbody-row-${rowIdx}`}>
{contents.map((content, contentIdx) => (
<TableCell key={`td-${contentIdx}`}>{content}</TableCell>
Expand Down

0 comments on commit 3b76bbe

Please sign in to comment.