Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 16 commits into from
Feb 28, 2021
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions ui-kit/src/components/LubyconUIKitProvider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { ReactNode } from 'react';
import { ToastProvider } from 'contexts/Toast';
import { PortalProvider } from 'contexts/Portal';
import { SnackbarProvider } from 'src/contexts/Snackbar';
import { ModalProvider } from 'src/contexts/Modal';

interface Props {
children: ReactNode;
Expand All @@ -10,9 +11,11 @@ interface Props {
function LubyconUIKitProvider({ children }: Props) {
return (
<PortalProvider>
<SnackbarProvider>
<ToastProvider>{children}</ToastProvider>
</SnackbarProvider>
<ModalProvider>
<SnackbarProvider>
<ToastProvider>{children}</ToastProvider>
</SnackbarProvider>
</ModalProvider>
</PortalProvider>
);
}
Expand Down
20 changes: 20 additions & 0 deletions ui-kit/src/components/Modal/ModalBackdrop.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React, { Ref, forwardRef } from 'react';
import classnames from 'classnames';

interface ModalBackdropProps {
onClick: (event: React.MouseEvent<HTMLDivElement>) => void;
}

const ModalBackdrop = ({ onClick }: ModalBackdropProps, ref: Ref<HTMLDivElement>) => {
evan-moon marked this conversation as resolved.
Show resolved Hide resolved
return (
<div
ref={ref}
className={classnames('lubycon-modal', 'lubycon-modal__overlay')}
aria-hidden={true}
tabIndex={-1}
onClick={onClick}
/>
);
};

export default forwardRef(ModalBackdrop);
21 changes: 21 additions & 0 deletions ui-kit/src/components/Modal/ModalContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { ReactNode } from 'react';
import classnames from 'classnames';
import Text from 'components/Text';

interface ModalContentProps extends React.HTMLAttributes<HTMLDivElement> {
evan-moon marked this conversation as resolved.
Show resolved Hide resolved
size?: 'small' | 'medium';
children?: ReactNode;
isCustom?: boolean;
}

const ModalContent = ({ children, isCustom = false, size }: ModalContentProps) => {
const typography = size === 'small' ? 'p2' : 'p1';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거 string으로 추론될 것 같은데, Typography 타입으로 어노테이팅해주면 더 좋을 것 같아요!


return (
<div className={classnames('lubycon-modal__content')}>
{isCustom ? children : <Text typography={typography}>{children}</Text>}
evan-moon marked this conversation as resolved.
Show resolved Hide resolved
</div>
);
};

export default ModalContent;
15 changes: 15 additions & 0 deletions ui-kit/src/components/Modal/ModalFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React, { ReactNode } from 'react';

interface ModalFooterProps {
children?: ReactNode;
}

const ModalFooter = ({ children }: ModalFooterProps) => {
return (
<div className="lubycon-modal__footer">
{children}
</div>
);
};

export default ModalFooter;
19 changes: 19 additions & 0 deletions ui-kit/src/components/Modal/ModalHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import Text from 'components/Text';

interface ModalHeaderProps {
size?: 'small' | 'medium';
title: string;
}

const ModalHeader = ({ size, title }: ModalHeaderProps) => {
const typography = size === 'small' ? 'subtitle' : 'h6';
evan-moon marked this conversation as resolved.
Show resolved Hide resolved

return (
<header className="lubycon-modal__title">
<Text typography={typography}>{title}</Text>
</header>
);
};

export default ModalHeader;
17 changes: 17 additions & 0 deletions ui-kit/src/components/Modal/ModalWindow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, { ReactNode } from 'react';
import classnames from 'classnames';

interface ModalWindowProps {
children: ReactNode;
size: 'small' | 'medium';
}

const ModalWindow = ({ children, size }: ModalWindowProps) => {
return (
<div className={classnames('lubycon-modal__window', `lubycon-modal__window--${size}`)}>
{children}
</div>
);
};

export default ModalWindow;
48 changes: 48 additions & 0 deletions ui-kit/src/components/Modal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { ReactElement, cloneElement, useRef } from 'react';
import ModalBackdrop from './ModalBackdrop';
import ModalWindow from './ModalWindow';
import classnames from 'classnames';
import { generateID } from 'utils/index';
import { useEffect } from 'react';

export interface ModalProps extends React.HTMLAttributes<HTMLDivElement> {
show: boolean;
size?: 'small' | 'medium';
children: ReactElement[];
onOpen?: () => void;
onClose?: () => void;
evan-moon marked this conversation as resolved.
Show resolved Hide resolved
}

const Modal = ({ show, size = 'small', children, onClose }: ModalProps) => {
const backdropRef = useRef(null);
const onBackdropClick = (event: React.MouseEvent<HTMLDivElement>) => {
evan-moon marked this conversation as resolved.
Show resolved Hide resolved
if (!backdropRef.current) return;
evan-moon marked this conversation as resolved.
Show resolved Hide resolved
if (event.target === backdropRef.current) onClose?.();
};

const onKeydown = (event: KeyboardEvent) => {
if (event.key === 'Escape') onClose?.();
};
useEffect(() => {
window.addEventListener('keydown', onKeydown);
return () => {
window.removeEventListener('keydown', onKeydown);
};
}, []);

return show ? (
<div className={classnames('lubycon-modal')} tabIndex={-1} aria-hidden={true}>
<ModalBackdrop onClick={onBackdropClick} ref={backdropRef} />
<ModalWindow size={size}>
{children.map((element) => {
return cloneElement(element, { key: generateID('lubycon-modal__children'), size: size });
evan-moon marked this conversation as resolved.
Show resolved Hide resolved
})}
</ModalWindow>
</div>
) : null;
};

export default Modal;
export { default as ModalHeader } from './ModalHeader';
export { default as ModalContent } from './ModalContent';
export { default as ModalFooter } from './ModalFooter';
64 changes: 64 additions & 0 deletions ui-kit/src/contexts/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, { useContext, ReactNode, createContext, useState, useCallback } from 'react';
import Modal, { ModalProps } from 'components/Modal';
import { generateID } from 'src/utils';
import { Portal } from './Portal';

interface ModalOptions extends Omit<ModalProps, 'show'> {
test?: boolean;
}
interface ModalGlobalState {
openModal: (option: ModalOptions) => void;
closeModal: (modalId: string) => void;
}
interface ModalProviderProps {
children: ReactNode;
}

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

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

const openModal = useCallback(
({ id = generateID('lubycon-modal'), ...option }: ModalOptions) => {
const modal = { id, ...option };
setOpenedModalStack([...openedModalStack, modal]);
},
[openedModalStack]
);
const closeModal = useCallback(
(closedModalId: string) => {
setOpenedModalStack(openedModalStack.filter((modal) => modal.id !== closedModalId));
},
[openedModalStack]
);

return (
<ModalContext.Provider
value={{
openModal,
closeModal,
}}
>
{children}
<Portal>
{openedModalStack.map(({ id, handleClick, ...modalProps }) => (
<Modal
show={true}
key={id}
onClose={() => closeModal(id ?? '')}
handleClick={() => handleClick?.()}
{...modalProps}
/>
))}
</Portal>
</ModalContext.Provider>
);
}

export function useModal() {
return useContext(ModalContext);
}
6 changes: 6 additions & 0 deletions ui-kit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ export { default as List, ListItem } from './components/List';
export { default as Input } from './components/Input';
export { default as ProgressBar } from './components/ProgressBar';
export { default as Accordion } from './components/Accordion';
export {
default as Modal,
ModalHeader,
ModalContent,
ModalFooter,
} from './components/Modal';
export { Portal } from './contexts/Portal';
export { useToast } from './contexts/Toast';
export { useSnackbar } from './contexts/Snackbar';
Expand Down
47 changes: 47 additions & 0 deletions ui-kit/src/sass/components/_Modal.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.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 {
background-color: get-color('gray100');
opacity: 0.5;
}
&__window {
background-color: get-color('gray10');
border-radius: 4px;
box-sizing: border-box;
z-index: 1001;
&--small {
width: 280px;
padding: 16px 20px;
}
&--medium {
width: 400px;
padding: 20px 24px;
}
}
&__title {
color: get-color('gray100');
margin-top: 0;
margin-bottom: 12px;
}
&__content {
color: get-color('gray70');
margin-bottom: 24px;
white-space: pre-wrap;
}
&__footer {
display: flex;
align-items: center;
justify-content: flex-end;
}
}
1 change: 1 addition & 0 deletions ui-kit/src/sass/components/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
@import './List';
@import './Input';
@import './ProgressBar';
@import './Modal';
Loading