-
Notifications
You must be signed in to change notification settings - Fork 6
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: 공용 모달 컴포넌트 추가 #11
Changes from 5 commits
795f170
be2908e
46962d6
238a283
12fc904
f63caee
3ec583e
6794a9c
fa6c2ce
801369a
a3eaf2a
f325a2f
5799a19
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,32 @@ | ||
import { style } from '@vanilla-extract/css'; | ||
|
||
export const background = style({ | ||
width: '100vw', | ||
height: '100vh', | ||
zIndex: 100, | ||
|
||
position: 'fixed', | ||
top: '0px', | ||
left: '0px', | ||
|
||
display: 'flex', | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
|
||
backgroundColor: 'rgba(25, 25, 27, 0.3)', | ||
}); | ||
|
||
export const container = style({ | ||
width: '326px', | ||
padding: '24px', | ||
|
||
display: 'flex', | ||
flexDirection: 'column', | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
gap: '2.4rem', | ||
|
||
backgroundColor: '#fff', | ||
|
||
borderRadius: '8px', | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { ReactNode} from 'react'; | ||
import ModalPortal from '@/components/ModalPortal'; | ||
import * as styles from './Modal.css'; | ||
import ModalTitle from './ModalTitle'; | ||
import ModalButton from './ModalButton'; | ||
import useOnClickOutside from '@/hooks/useOnClickOutside'; | ||
|
||
interface ModalMainProps { | ||
children?: ReactNode; | ||
handleModalClose: () => void; | ||
} | ||
|
||
function ModalMain({ children, handleModalClose }: ModalMainProps) { | ||
const { ref } = useOnClickOutside(() => { | ||
handleModalClose(); | ||
}); | ||
|
||
return ( | ||
<ModalPortal> | ||
<div className={styles.background}> | ||
<div ref={ref} className={styles.container}> | ||
{children} | ||
</div> | ||
</div> | ||
</ModalPortal> | ||
); | ||
} | ||
|
||
const Modal = Object.assign(ModalMain, { | ||
Title: ModalTitle, | ||
Button: ModalButton, | ||
}); | ||
|
||
export default Modal; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { style } from '@vanilla-extract/css'; | ||
|
||
export const buttonContainer = style({ | ||
width: '100%', | ||
|
||
display: 'flex', | ||
justifyContent: 'flex-end', | ||
gap: '16px', | ||
}); | ||
|
||
export const baseButton = style({ | ||
padding: '12px 16px', | ||
|
||
display: 'flex', | ||
justifyContent: 'center', | ||
alignItems: 'center', | ||
|
||
flexShrink: '0', | ||
|
||
borderRadius: '12px', | ||
fontSize: '1.4rem', | ||
fontWeight: '500', | ||
lineHeight: '20px', | ||
letterSpacing: '-0.4px', | ||
}); | ||
|
||
export const primaryButton = style([ | ||
baseButton, | ||
{ | ||
backgroundColor: '#0047FF', | ||
color: '#fff', | ||
}, | ||
]); | ||
|
||
export const secondaryButton = style([ | ||
baseButton, | ||
{ | ||
backgroundColor: '#EBF4FF', | ||
color: '#0047FF', | ||
}, | ||
]); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { MouseEventHandler, ReactNode } from 'react'; | ||
import * as styles from './ModalButton.css'; | ||
|
||
interface ModalButtonProps { | ||
children: ReactNode; | ||
onCancel: MouseEventHandler<HTMLButtonElement>; | ||
onClick: MouseEventHandler<HTMLButtonElement>; | ||
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. |
||
} | ||
|
||
export default function ModalButton({ children, onCancel, onClick }: ModalButtonProps) { | ||
return ( | ||
<div className={styles.buttonContainer}> | ||
<button type="button" className={styles.secondaryButton} onClick={onCancel}> | ||
취소 | ||
</button> | ||
<button type="button" className={styles.primaryButton} onClick={onClick}> | ||
{children} | ||
</button> | ||
</div> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { style } from '@vanilla-extract/css'; | ||
|
||
export const title = style({ | ||
width: '100%', | ||
fontSize: '1.6rem', | ||
fontWeight: '600', | ||
lineHeight: '150%', | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { ReactNode } from 'react'; | ||
import * as styles from './ModalTitle.css'; | ||
|
||
export default function ModalTitle({ children }: { children: ReactNode }) { | ||
return <div className={styles.title}>{children}</div>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { ReactNode } from 'react'; | ||
import ReactDOM from 'react-dom'; | ||
|
||
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. 서영님 이 부분은 사소하지만 createPortal을 바로 불러와도 좋을 것 같습니다.
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. 꼼꼼하게 봐주셔서 감사합니다 :) |
||
interface Props { | ||
children: ReactNode; | ||
} | ||
|
||
const ModalPortal = ({ children }: Props) => { | ||
const el = document.getElementById('modal-root') as HTMLElement; | ||
|
||
return ReactDOM.createPortal(children, el); | ||
}; | ||
|
||
export default ModalPortal; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { useCallback, useEffect, useState } from 'react'; | ||
|
||
interface useBooleanOutput { | ||
isOn: boolean; | ||
toggle: () => void; | ||
handleSetOn: () => void; | ||
handleSetOff: () => void; | ||
} | ||
|
||
export default function useBooleanOutput(defaultValue?: boolean): useBooleanOutput { | ||
const [isOn, setIsOn] = useState<boolean>(!!defaultValue); | ||
|
||
const toggle = useCallback(() => setIsOn((prev) => !prev), []); | ||
const handleSetOn = useCallback(() => setIsOn(true), []); | ||
const handleSetOff = useCallback(() => setIsOn(false), []); | ||
|
||
useEffect(() => { | ||
setIsOn(false); | ||
}, []); | ||
|
||
return { isOn, toggle, handleSetOn, handleSetOff }; | ||
} | ||
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. 서영님 토글을 useCallback으로 감싸기 너무 좋네요!!
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.
추가했던 이유는 이전에 useToggle 훅을 만들어 모달을 관리한 적이 있는데 vercel 배포과정에서 strictmode가 꺼지면서 모든 모달이 오픈되는 오류가 발생했었습니다. (이유를 정확히는 모르겠지만) 렌더링이 되면서 뭔가 잘못되는 것 같아, 저 코드를 넣었더니 해결이 됐습니다 🤔 (1) 정확한 원인도 모르는 해결책이니 우선 지우고 문제 발생시 다시 생각해본다. useEffect(() => {
if (!!defaultValue === false) {
setIsOn(false);
}
}, []); 저는 1번으로 제거 해볼까 하는데, 소현님 의견도 궁금합니다 :) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { useEffect, useRef, useState } from 'react'; | ||
|
||
const useOnClickOutside = (handler: () => void) => { | ||
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. 꺄 감사합니다 나현님🙇♀️ 사실 저번 팀프로젝트들에서 얻은 것들을 주섬주섬 주워다가 쓰고 있습니다 ㅎㅎ |
||
const ref = useRef<HTMLDivElement>(null); | ||
|
||
useEffect(() => { | ||
const handleClickOutside = (e: Event) => { | ||
if (ref.current !== null && !ref.current.contains(e.target as Node)) { | ||
handler(); | ||
} | ||
}; | ||
document.addEventListener('mousedown', handleClickOutside); | ||
document.addEventListener('touchstart', handleClickOutside); | ||
return () => { | ||
document.removeEventListener('mousedown', handleClickOutside); | ||
document.removeEventListener('touchstart', handleClickOutside); | ||
}; | ||
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. 오! 매번 outSideClick 함수를 버블링으로만 구현했었는데 이렇게 ref를 이용하는 방법도 있군요! 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. 궁금해서 찾아보니 touchstart가 모바일/태블릿 기기 전용 이벤트 였군요!! 서영님 세심함에 감탄하고 갑니다 ㅎㅎ 👍 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. 헉 감사합니다 🙇♀️ 저는 항상 ref로만 구현 해봐서, 버블링으로 구현하는 방법 너무 궁금합니다! 소현님 이전 프로젝트 구경가야겠어요 🏃♀️ |
||
}, [ref]); | ||
|
||
return { ref }; | ||
}; | ||
|
||
export default useOnClickOutside; |
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.
@Nahyun-Kang
이 부분이 추가 됐습니다! 속성값으로 검정바탕 눌렀을때 발생할 핸들러 전달주시면 됩니다~! :)
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.
확인했습니다!! :D 알려주셔서 감사해용!! 🙇♀️