-
Notifications
You must be signed in to change notification settings - Fork 1
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
Changes from all commits
d9fb989
cc11199
db31892
5e085bc
35e69a6
e31dca0
ee1073a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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', | ||
}); |
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>; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof meta>; | ||
|
||
export const Default: Story = { | ||
args: { | ||
title: 'Modal Title', | ||
content: 'Modal Content', | ||
cancelText: '취소', | ||
confirmText: '확인', | ||
}, | ||
}; |
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; |
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; |
This file was deleted.
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스토리 스타일도 밑으로 분리하는게 좋을까요?
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: {}, | ||
}; |
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; |
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아니 이거 이런식으로 하는거군여, 이제야 깨달음 😱