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] DefaultDialog 컴포넌트 추가 #83

Merged
merged 7 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
94 changes: 94 additions & 0 deletions src/components/Dialog/DefaultDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import Button from '@/components/Button/Button';
import { type DefaultDialogProps } from '@/components/Dialog/Dialog.types';
import Modal from '@/components/Modal/Modal';
import { css } from '@styled-system/css';

/**
* @description DefaultDialog 컴포넌트
* @param onCancel 취소 버튼 클릭 시 실행되는 함수
* @param onConfirm 확인 버튼 클릭 시 실행되는 함수
* @param cancelText 취소 버튼 텍스트 (없을시 버튼 노출 안함)
* @param confirmText 확인 버튼 텍스트 (없을시 버튼 노출 안함)
* @param content 다이얼로그 내용
* @param title 다이얼로그 타이틀
*
* @param onClose 다이얼로그 닫기
* @param isOpen 다이얼로그 오픈 여부
* @constructor
*/
function DefaultDialog({
onCancel,
onConfirm,
cancelText,
confirmText,
title,
content,
onClose,
isOpen,
}: DefaultDialogProps) {
const handleCancel = () => {
onClose();
onCancel && onCancel();
};

const handleConfirm = () => {
onClose();
onConfirm && onConfirm();
};
return (
<Modal isOpen={isOpen} onClose={onClose} padding={'32px 24px 20px 24px'}>
<div className={dialogWrapperCss}>
<div className={textWrapperCss}>
{title && <p className={dialogTitleCss}>{title}</p>}
{content && <p className={dialogContentCss}>{content}</p>}
</div>
<div>
<div className={buttonWrapperCss}>
{cancelText && (
<Button size={'medium'} variant={'ghost'} onClick={handleCancel} className={dialogButtonTextCss}>
{cancelText}
</Button>
)}
<Button size={'medium'} variant={'ghost'} onClick={handleConfirm}>
{confirmText}
</Button>
</div>
</div>
</div>
</Modal>
);
}

export default DefaultDialog;

const dialogTitleCss = css({
textStyle: 'title3',
color: 'text.primary',
});

const dialogContentCss = css({
textStyle: 'body2',
color: 'text.secondary',
});

const dialogButtonTextCss = css({
color: 'text.tertiary',
});

const dialogWrapperCss = css({
display: 'flex',
flexDirection: 'column',
gap: '16px',
});

const textWrapperCss = css({
display: 'flex',
flexDirection: 'column',
gap: '12px',
});

const buttonWrapperCss = css({
display: 'flex',
justifyContent: 'flex-end',
gap: '4px',
});
39 changes: 39 additions & 0 deletions src/components/Dialog/Dialog.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Button from '@/components/Button/Button';
import Dialog from '@/components/Dialog/Dialog';
import { type DefaultDialogProps } from '@/components/Dialog/Dialog.types';
import useModal from '@/hooks/useModal';
import type { Meta, StoryObj } from '@storybook/react';

const DialogStory = (arg: Pick<DefaultDialogProps, 'title' | 'content' | 'cancelText' | 'confirmText'>) => {
const { isOpen, openModal, closeModal } = useModal();
return (
<div>
<Button size={'large'} variant={'cta'} onClick={openModal}>
Open Dialog
</Button>
<Dialog variant={'default'} isOpen={isOpen} onClose={closeModal} {...arg} />
</div>
);
};

const meta = {
title: 'Component/Dialog',
component: DialogStory,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof DialogStory>;

Comment on lines +7 to +27
Copy link
Member

Choose a reason for hiding this comment

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

아니 이거 이런식으로 하는거군여, 이제야 깨달음 😱

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
title: 'Modal Title',
content: 'Modal Content',
cancelText: '취소',
confirmText: '확인',
},
};
16 changes: 16 additions & 0 deletions src/components/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import DefaultDialog from '@/components/Dialog/DefaultDialog';
import { type DialogProps } from '@/components/Dialog/Dialog.types';
import { type ModalProps } from '@/components/Modal/Modal';

/**
* dialog 컴포넌트
* @description 각 dialog variant 타입에 따라 다른 컴포넌트들을 랜더링합니다.
* @param props.variant 'default' | 'list' | 'select'
*/
function Dialog(props: DialogProps & ModalProps) {
if (props.variant === 'default') {
return <DefaultDialog {...props} />;
}
}

export default Dialog;
36 changes: 36 additions & 0 deletions src/components/Dialog/Dialog.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { type ModalProps } from '@/components/Modal/Modal';

interface ButtonGroupProps {
confirmText: string;
onConfirm?: () => void;
cancelText?: string;
onCancel?: () => void;
}

type Value = {
value: string;
text: string;
};

export interface DefaultDialogProps extends ButtonGroupProps, ModalProps {
variant: 'default';
title?: string;
content?: string;
}

export interface ListDialogProps extends ModalProps {
variant: 'list';
title: string;
list: Value[];
onClick: (value: Value) => void;
}

export interface SelectDialogProps extends Omit<ButtonGroupProps, 'onConfirm'>, ModalProps {
variant: 'select';
title?: string;
content?: string;
selects: Value[];
onConfirm: (value: Value) => void;
}

export type DialogProps = DefaultDialogProps | ListDialogProps | SelectDialogProps;
45 changes: 0 additions & 45 deletions src/components/Modal.tsx

This file was deleted.

43 changes: 43 additions & 0 deletions src/components/Modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Button from '@/components/Button/Button';
import Modal from '@/components/Modal/Modal';
import useModal from '@/hooks/useModal';
import type { Meta, StoryObj } from '@storybook/react';
import { css } from '@styled-system/css';

const ModalStory = () => {
const { isOpen, openModal, closeModal } = useModal();
return (
<div>
<Button size={'large'} variant={'cta'} onClick={openModal}>
Open Modal
</Button>
<Modal isOpen={isOpen} onClose={closeModal}>
<div
className={css({
textStyle: 'title3',
color: 'text.primary',
})}
>
Comment on lines +16 to +20
Copy link
Member

Choose a reason for hiding this comment

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

스토리 스타일도 밑으로 분리하는게 좋을까요?

가독성에는 좋을 것 같긴 하지만 조금 번거롭다는 단점이 있을 것 같네요..

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

스토리의 스타일은 그대로 두겠습니다. 저도 스토리에서는 조금 번거로울것같아요

모달입니다
</div>
</Modal>
</div>
);
};

const meta = {
title: 'Component/Modal',
component: ModalStory,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof ModalStory>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Primary: Story = {
args: {},
};
74 changes: 74 additions & 0 deletions src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use client';

import React, { type PropsWithChildren, useRef } from 'react';
import AnimatePortal from '@/components/portal/AnimationPortal';
import useOutsideClick from '@/hooks/lifeCycle/useOutsideClick';
import { modalConstant } from '@/styles/constant';
import { css } from '@styled-system/css';
import { type Property } from '@styled-system/types/csstype';
import { motion } from 'framer-motion';

export interface ModalProps {
isOpen: boolean;
onClose: () => void;
padding?: Property.Padding;
}

/**
* @description Modal 컴포넌트
* @param isOpen 모달 오픈 여부
* @param onClose 모달 닫기
* @param padding 모달 패딩 값 (default: 20px 24px)
* @param children 모달 내부 컨텐츠
*
*/
function Modal({ isOpen, children, onClose, padding = '20px 24px' }: PropsWithChildren<ModalProps>) {
const modalRef = useRef(null);

useOutsideClick({
ref: modalRef,
handler: () => {
onClose();
},
});

return (
<AnimatePortal isShowing={isOpen} mode={'popLayout'}>
<div className={modalOverlayCss}>
<motion.div
key="modal"
className={modalContentCss + ' ' + css({ padding })}
ref={modalRef}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.03 }}
exit={{ opacity: 0 }}
>
{children}
</motion.div>
</div>
</AnimatePortal>
);
}

const modalOverlayCss = css({
position: 'absolute',
top: '0',
left: '0',
width: '100%',
height: '100%',
background: 'scrim.screen',
padding: '28px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
const modalContentCss = css({
background: 'bg.surface4',
borderRadius: '30px',
maxWidth: '400px',
width: '100%',
zIndex: `${modalConstant}`,
});

export default Modal;
31 changes: 31 additions & 0 deletions src/components/portal/AnimationPortal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client';

import { type ComponentProps } from 'react';
import { AnimatePresence } from 'framer-motion';

import Portal from './Portal';

interface Props extends ComponentProps<typeof Portal> {
/**
* children의 렌더링 여부
*/
isShowing: boolean;
/**
* framer-motion AnimatePresence의 mode
* @default 'wait'
*/
mode?: ComponentProps<typeof AnimatePresence>['mode'];
}

/**
* @description Portal을 AnimatePresence 와 함께 사용합니다
*/
const AnimatePortal = ({ children, isShowing, mode = 'wait' }: Props) => {
return (
<Portal>
<AnimatePresence mode={mode}>{isShowing && children}</AnimatePresence>
</Portal>
);
};

export default AnimatePortal;
Loading
Loading