Skip to content

Commit

Permalink
Feat/modal (#54)
Browse files Browse the repository at this point in the history
* feat(ui-kit): Modal 컴포넌트 추가

* feat(ui-kit): Modal 스토리 추가

* feat(ui-kit): Modal TODO

* feat(ui-kit): Modal 컴포넌트 추가

* feat(ui-kit): 타이틀 부분 태그 타입 수정

* chore: 리뷰 내용 반영-백드롭 forwradRef

* chore: 리뷰 내용 반영-컴포넌트 합성 방식으로 변경

* chore: 모달 컴포넌트 스토리 수정

* chore: 리뷰반영 - 타입 변경, 엘리먼트 유효성 검사

* chore: 리뷰반영 - onOpen 이벤트, 백드롭 이벤트명

* chore: 리뷰반영 - 컴포넌트 제네릭

* chore: 린트 에러

* chore: 스토리 업데이트

* chore: 단일 엘리먼트가 주입된 경우 추가

* chore: 모달 컨텍스트 수정

* chore: 모달 컴포넌트 스토리 추가

* chore: 리뷰 내용 반영 - 변수명, children 배열 처리
  • Loading branch information
Junkim93 authored Mar 6, 2021
1 parent d37bd03 commit 468653f
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 36 deletions.
10 changes: 5 additions & 5 deletions ui-kit/src/components/Modal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ReactElement, cloneElement, useRef } from 'react';
import React, { Children, ReactElement, cloneElement, useRef } from 'react';
import ModalBackdrop from './ModalBackdrop';
import ModalWindow from './ModalWindow';
import classnames from 'classnames';
Expand All @@ -8,7 +8,7 @@ import { useEffect } from 'react';
export interface ModalProps extends React.HTMLAttributes<HTMLDivElement> {
show: boolean;
size?: 'small' | 'medium';
children: ReactElement[];
children: ReactElement | ReactElement[];
onOpen?: () => void;
onClose?: () => void;
}
Expand Down Expand Up @@ -40,9 +40,9 @@ const Modal = ({ show, size = 'small', children, onOpen, onClose }: ModalProps)
<div className={classnames('lubycon-modal')} tabIndex={-1} aria-hidden={true}>
<ModalBackdrop onClick={handleBackdropClick} ref={backdropRef} />
<ModalWindow size={size}>
{children.map((element) => {
return cloneElement(element, { key: generateID('lubycon-modal__children'), size: size });
})}
{Children.map(children, (child) =>
cloneElement(child, { size: size, key: generateID('lubycon-modal__children') })
)}
</ModalWindow>
</div>
) : null;
Expand Down
49 changes: 39 additions & 10 deletions ui-kit/src/contexts/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,47 @@
import React, { useContext, ReactNode, createContext, useState, useCallback } from 'react';
import React, {
ReactElement,
useContext,
ReactNode,
createContext,
useState,
useCallback,
isValidElement,
} from 'react';
import Modal, { ModalProps } from 'components/Modal';
import { generateID } from 'src/utils';
import { Portal } from './Portal';
import { cloneElement } from 'react';

interface ModalOptions extends Omit<ModalProps, 'show'> {
test?: boolean;
interface ModalOptions extends Omit<ModalProps, 'show' | 'children'> {
header?: ReactElement;
content: ReactElement;
footer: ReactElement;
}
interface ModalStackOptions extends Omit<ModalOptions, 'header' | 'content' | 'footer'> {
reactElements: ReactElement[];
}
interface ModalGlobalState {
openModal: (option: ModalOptions) => void;
openModal: (option: ModalOptions) => string;
closeModal: (modalId: string) => void;
}
interface ModalProviderProps {
children: ReactNode;
}

const ModalContext = createContext<ModalGlobalState>({
openModal: () => {},
openModal: () => '',
closeModal: () => {},
});

export function ModalProvider({ children }: ModalProviderProps) {
const [openedModalStack, setOpenedModalStack] = useState<ModalOptions[]>([]);
const [openedModalStack, setOpenedModalStack] = useState<ModalStackOptions[]>([]);

const openModal = useCallback(
({ id = generateID('lubycon-modal'), ...option }: ModalOptions) => {
const modal = { id, ...option };
({ id = generateID('lubycon-modal'), header, content, footer, ...option }: ModalOptions) => {
const reactElements = isValidElement(header) ? [header, content, footer] : [content, footer];
const modal = { id, reactElements, ...option };
setOpenedModalStack([...openedModalStack, modal]);
return id;
},
[openedModalStack]
);
Expand All @@ -45,8 +61,21 @@ export function ModalProvider({ children }: ModalProviderProps) {
>
{children}
<Portal>
{openedModalStack.map(({ id, ...modalProps }) => (
<Modal show={true} key={id} onClose={() => closeModal(id ?? '')} {...modalProps} />
{openedModalStack.map(({ id, reactElements, size = 'small', ...modalProps }) => (
<Modal
show={true}
key={id}
onClose={() => closeModal(id ?? '')}
size={size}
{...modalProps}
>
{reactElements.map((element) => {
return cloneElement(element, {
key: generateID('lubycon-modal__children'),
size: size,
});
})}
</Modal>
))}
</Portal>
</ModalContext.Provider>
Expand Down
213 changes: 192 additions & 21 deletions ui-kit/src/stories/Modal.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,212 @@ import { Meta } from '@storybook/react/types-6-0';
import React, { useState } from 'react';
import { Modal, ModalHeader, ModalContent, ModalFooter } from 'src';
import Button from 'components/Button';
import { colors } from 'constants/colors';
import { useModal } from 'contexts/Modal';
import { Column, Row } from 'src/components/Grid';

interface FooterProps {
size: 'small' | 'medium';
showCancelBtn?: boolean;
closeModal: () => void;
}

const DefaultModalHeader = () => <ModalHeader>타이틀입니다</ModalHeader>;
const DefaultModdalFooter = ({ size, showCancelBtn = true, closeModal }: FooterProps) => {
return (
<ModalFooter>
{showCancelBtn ? (
<Button size={size} onClick={closeModal}>
취소
</Button>
) : null}
<Button size={size} type="informative" onClick={closeModal}>
저장하기
</Button>
</ModalFooter>
);
};

const margin = {
marginRight: 16,
marginBottom: 32,
};

export default {
title: 'Lubycon UI kit/Modal',
component: Modal,
} as Meta;

export const Default = () => {
const [showModal, setShowModal] = useState(false);
const [showModal, setShowSmallModal] = useState(false);
const [showModal2, setShowMediumModal] = useState(false);

const closeModal = () => setShowModal(false);
const closeModal = () => setShowSmallModal(false);
const closeModal2 = () => setShowMediumModal(false);
const handleOpen = () => console.info('open');

return (
<>
<Button onClick={() => setShowModal(true)}>모달 열기</Button>
<Modal show={showModal} onClose={closeModal} onOpen={handleOpen}>
<ModalHeader>타이틀입니다</ModalHeader>
<ModalContent className="Test">
<Column xs={6} style={{ marginBottom: 32 }}>
<Button type="informative" onClick={() => setShowSmallModal(true)} style={margin}>
Small 사이즈 모달 열기
</Button>
<Modal show={showModal} onOpen={handleOpen} onClose={closeModal}>
<DefaultModalHeader />
<ModalContent>
<div>여기에 본문 텍스트가 들어갑니다</div>
<div>여기에 본문 텍스트가 들어갑니다</div>
</ModalContent>
<ModalFooter>
<Button
size="small"
style={{ color: colors.gray80, background: 'transparent', marginRight: '4px' }}
onClick={closeModal}
>
취소
</Button>
<Button size="small" onClick={closeModal}>
저장하기
</Button>
</ModalFooter>
<DefaultModdalFooter size="small" closeModal={closeModal} />
</Modal>
</>

<Button size="medium" type="informative" onClick={() => setShowMediumModal(true)}>
Medium 사이즈 모달 열기
</Button>
<Modal size="medium" show={showModal2} onOpen={handleOpen} onClose={closeModal2}>
<DefaultModalHeader />
<ModalContent>
텍스트 내용이 많을 경우에는 중간 크기의 모달 사용을 권장합니다. 여기에 본문 텍스트를
입력해 주세요.
</ModalContent>
<DefaultModdalFooter size="medium" closeModal={closeModal2} />
</Modal>
</Column>
);
};

export const ModalHooks = () => {
const { openModal, closeModal } = useModal();

return (
<Row>
<Column xs={8} style={{ marginBottom: 32 }}>
<Button
style={margin}
onClick={() => {
const modalId = openModal({
content: (
<ModalContent>
본문 텍스트와 타이틀은 용도에 따라 별도로 구성이 가능합니다
</ModalContent>
),
footer: <DefaultModdalFooter size="small" closeModal={() => closeModal(modalId)} />,
});
}}
>
small 타이틀 제외
</Button>
<Button
style={margin}
onClick={() => {
const modalId = openModal({
header: <DefaultModalHeader />,
content: (
<ModalContent>
<div>여기에 본문 텍스트가 들어갑니다</div>
<div>여기에 본문 텍스트가 들어갑니다</div>
</ModalContent>
),
footer: (
<DefaultModdalFooter
size="small"
showCancelBtn={false}
closeModal={() => closeModal(modalId)}
/>
),
});
}}
>
small 취소 버튼 제외
</Button>
<Button
style={margin}
onClick={() => {
const modalId = openModal({
content: (
<ModalContent>
본문 텍스트와 타이틀은 용도에 따라 별도로 구성이 가능합니다
</ModalContent>
),
footer: (
<DefaultModdalFooter
size="small"
showCancelBtn={false}
closeModal={() => closeModal(modalId)}
/>
),
});
}}
>
small 타이틀, 취소 버튼 제외
</Button>
<Button
style={margin}
onClick={() => {
const modalId = openModal({
content: (
<ModalContent>
텍스트 내용이 많을 경우에는 중간 크기의 모달 사용을 권장합니다. 여기에 본문
텍스트를 입력해주세요.
</ModalContent>
),
footer: (
<DefaultModdalFooter
size="medium"
showCancelBtn={true}
closeModal={() => closeModal(modalId)}
/>
),
size: 'medium',
});
}}
>
medium 타이틀 제외
</Button>
<Button
style={margin}
onClick={() => {
const modalId = openModal({
header: <DefaultModalHeader />,
content: (
<ModalContent>
텍스트 내용이 많을 경우에는 중간 크기의 모달 사용을 권장합니다. 여기에 본문
텍스트를 입력해주세요.
</ModalContent>
),
footer: (
<DefaultModdalFooter
size="medium"
showCancelBtn={false}
closeModal={() => closeModal(modalId)}
/>
),
size: 'medium',
});
}}
>
medium 취소 버튼 제외
</Button>
<Button
style={margin}
onClick={() => {
const modalId = openModal({
content: (
<ModalContent>
본문 텍스트와 타이틀은 용도에 따라 별도로 구성이 가능합니다
</ModalContent>
),
footer: (
<DefaultModdalFooter
size="small"
showCancelBtn={false}
closeModal={() => closeModal(modalId)}
/>
),
});
}}
>
medium 타이틀, 취소 버튼 제외
</Button>
</Column>
</Row>
);
};

0 comments on commit 468653f

Please sign in to comment.