-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: confirm provider 뼈대 구현 * feat: confirm modal 수락 기능 추가 * feat: confirmModal 스타일 및 분리 * feat: ConfirmProvider 분리 * feat: ConfirmModal storybook 추가 * feat: storybook 예시 추가 * refactor: useConfirm 분리 * refactor: confirm modal 로직 응집하기 위해 useEffect 이동 * fix: promise에서 resolve 함수 상태 변경 실패로 인한 ref 사용 * refactor: createPortal를 ConfirmModal 내부로 이동 * refactor: 의미있는 네이밍으로 변경 * fix: keydown 이벤트 적용되지 않는 현상 수정 * style: style lint 적용 및 개행 * chore: 사용하지 않는 파일 삭제 * fix: resolverRef 타입 변경 * feat: 닫기, 수락 button에 type 추가 * refactor: 네이밍 cancel에서 denial으로 변경 * feat: 모달 열릴 때 바로 title로 포커스 이동할 수 있도록 수정 * refactor: 중복된 createPortal 삭제 * refactor: theme 활용하여 색상 코드 변경 * refactor: 전반적인 ConfirmModal 네이밍 변경 - ConfirmProvider > ConfirmModalProvider - useConfirm > useConfirmContext - confirm > confirmPopup * fix: theme color 사용 시 객체분해할당 오류 수정
- Loading branch information
1 parent
ebf9b3d
commit 5b336df
Showing
6 changed files
with
319 additions
and
2 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
89 changes: 89 additions & 0 deletions
89
frontend/src/shared/components/ConfirmModal/ConfirmModal.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import styled from 'styled-components'; | ||
import ConfirmModalProvider from './ConfirmModalProvider'; | ||
import { useConfirmContext } from './hooks/useConfirmContext'; | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
|
||
const meta: Meta<typeof ConfirmModalProvider> = { | ||
title: 'shared/Confirm', | ||
component: ConfirmModalProvider, | ||
decorators: [ | ||
(Story) => ( | ||
<ConfirmModalProvider> | ||
<Story /> | ||
</ConfirmModalProvider> | ||
), | ||
], | ||
}; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof ConfirmModalProvider>; | ||
|
||
export const Example: Story = { | ||
render: () => { | ||
const Modal = () => { | ||
const { confirmPopup } = useConfirmContext(); | ||
|
||
const clickHiByeBtn = async () => { | ||
const isConfirmed = await confirmPopup({ | ||
title: '하이바이 모달', | ||
content: ( | ||
<> | ||
<p>도밥은 정말 도밥입니까?</p> | ||
<p>코난은 정말 코난입니까?</p> | ||
</> | ||
), | ||
denial: '바이', | ||
confirmation: '하이', | ||
}); | ||
|
||
if (isConfirmed) { | ||
alert('confirmed'); | ||
return; | ||
} | ||
|
||
alert('denied'); | ||
}; | ||
|
||
// denial과 confirmation 기본값은 '닫기'와 '확인'입니다. | ||
const clickOpenCloseBtn = async () => { | ||
const isConfirmed = await confirmPopup({ | ||
title: '오쁜클로즈 모달', | ||
content: ( | ||
<> | ||
<p>코난은 정말 코난입니까?</p> | ||
<p>도밥은 정말 도밥입니까?</p> | ||
</> | ||
), | ||
}); | ||
|
||
if (isConfirmed) { | ||
alert('confirmed'); | ||
return; | ||
} | ||
|
||
alert('denied'); | ||
}; | ||
|
||
return ( | ||
<Body> | ||
<Button onClick={clickHiByeBtn}>하이바이 모달열기</Button> | ||
<Button onClick={clickOpenCloseBtn}>닫기확인 모달열기</Button> | ||
</Body> | ||
); | ||
}; | ||
|
||
return <Modal />; | ||
}, | ||
}; | ||
|
||
const Body = styled.div` | ||
height: 2400px; | ||
`; | ||
|
||
const Button = styled.button` | ||
padding: 4px 11px; | ||
color: white; | ||
border: 2px solid white; | ||
border-radius: 4px; | ||
`; |
114 changes: 114 additions & 0 deletions
114
frontend/src/shared/components/ConfirmModal/ConfirmModal.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import { Flex } from 'shook-layout'; | ||
import styled, { css } from 'styled-components'; | ||
import Spacing from '../Spacing'; | ||
import type { ReactNode } from 'react'; | ||
|
||
interface ConfirmModalProps { | ||
title: string; | ||
content: ReactNode; | ||
denial: string; | ||
confirmation: string; | ||
onDeny: () => void; | ||
onConfirm: () => void; | ||
} | ||
|
||
const ConfirmModal = ({ | ||
title, | ||
content, | ||
denial, | ||
confirmation, | ||
onDeny, | ||
onConfirm, | ||
}: ConfirmModalProps) => { | ||
const focusTitle: React.RefCallback<HTMLDivElement> = (dom) => { | ||
dom && dom.focus(); | ||
}; | ||
|
||
return ( | ||
<> | ||
<Backdrop role="dialog" aria-modal="true" /> | ||
<Container> | ||
<Title ref={focusTitle} tabIndex={0}> | ||
{title} | ||
</Title> | ||
<Spacing direction="vertical" size={10} /> | ||
<Content>{content}</Content> | ||
<Spacing direction="vertical" size={10} /> | ||
<ButtonFlex $gap={16}> | ||
<DenialButton type="button" onClick={onDeny}> | ||
{denial} | ||
</DenialButton> | ||
<ConfirmButton type="button" onClick={onConfirm}> | ||
{confirmation} | ||
</ConfirmButton> | ||
</ButtonFlex> | ||
</Container> | ||
</> | ||
); | ||
}; | ||
|
||
export default ConfirmModal; | ||
|
||
const Backdrop = styled.div` | ||
position: fixed; | ||
top: 0; | ||
right: 0; | ||
bottom: 0; | ||
left: 0; | ||
width: 100%; | ||
height: 100%; | ||
margin: 0; | ||
padding: 0; | ||
background-color: rgba(0, 0, 0, 0.7); | ||
`; | ||
|
||
const Container = styled.section` | ||
position: fixed; | ||
top: 50%; | ||
left: 50%; | ||
transform: translate(-50%, -50%); | ||
min-width: 300px; | ||
margin: 0 auto; | ||
padding: 24px; | ||
color: ${({ theme: { color } }) => color.white}; | ||
background-color: ${({ theme: { color } }) => color.black300}; | ||
border: none; | ||
border-radius: 16px; | ||
`; | ||
|
||
const ButtonFlex = styled(Flex)` | ||
width: 100%; | ||
`; | ||
|
||
const Title = styled.header` | ||
font-size: 18px; | ||
text-align: left; | ||
`; | ||
|
||
const Content = styled.div``; | ||
|
||
const buttonStyle = css` | ||
flex: 1; | ||
width: 100%; | ||
height: 36px; | ||
color: ${({ theme: { color } }) => color.white}; | ||
border-radius: 10px; | ||
`; | ||
|
||
const DenialButton = styled.button` | ||
background-color: ${({ theme: { color } }) => color.secondary}; | ||
${buttonStyle} | ||
`; | ||
|
||
const ConfirmButton = styled.button` | ||
background-color: ${({ theme: { color } }) => color.primary}; | ||
${buttonStyle} | ||
`; |
105 changes: 105 additions & 0 deletions
105
frontend/src/shared/components/ConfirmModal/ConfirmModalProvider.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/* eslint-disable react-hooks/exhaustive-deps */ | ||
import { createContext, useCallback, useEffect, useState, useRef } from 'react'; | ||
import { createPortal } from 'react-dom'; | ||
import ConfirmModal from './ConfirmModal'; | ||
import type { ReactNode } from 'react'; | ||
|
||
export const ConfirmContext = createContext<null | { | ||
confirmPopup: (modalState: ModalContents) => Promise<boolean>; | ||
}>(null); | ||
|
||
interface ModalContents { | ||
title: string; | ||
content: ReactNode; | ||
denial?: string; | ||
confirmation?: string; | ||
} | ||
|
||
const ConfirmModalProvider = ({ children }: { children: ReactNode }) => { | ||
const [isOpen, setIsOpen] = useState(false); | ||
const resolverRef = useRef<{ | ||
resolve: (value: boolean) => void; | ||
} | null>(null); | ||
const [modalContents, setModalContents] = useState<ModalContents>({ | ||
title: '', | ||
content: '', | ||
denial: '닫기', | ||
confirmation: '확인', | ||
}); | ||
const { title, content, denial, confirmation } = modalContents; | ||
|
||
// ContextAPI를 통해 confirm 함수만 제공합니다. | ||
const confirmPopup = (contents: ModalContents) => { | ||
openModal(); | ||
setModalContents(contents); | ||
|
||
const promise = new Promise<boolean>((resolve) => { | ||
resolverRef.current = { resolve }; | ||
}); | ||
|
||
return promise; | ||
}; | ||
|
||
const closeModal = () => { | ||
setIsOpen(false); | ||
}; | ||
|
||
const openModal = () => { | ||
setIsOpen(true); | ||
}; | ||
|
||
const resolveConfirmation = (status: boolean) => { | ||
if (resolverRef?.current) { | ||
resolverRef.current.resolve(status); | ||
} | ||
}; | ||
|
||
const onDeny = useCallback(() => { | ||
resolveConfirmation(false); | ||
closeModal(); | ||
}, []); | ||
|
||
const onConfirm = useCallback(() => { | ||
resolveConfirmation(true); | ||
closeModal(); | ||
}, []); | ||
|
||
const onKeyDown = useCallback(({ key }: KeyboardEvent) => { | ||
if (key === 'Escape') { | ||
resolveConfirmation(false); | ||
closeModal(); | ||
} | ||
}, []); | ||
|
||
useEffect(() => { | ||
if (isOpen) { | ||
document.addEventListener('keydown', onKeyDown); | ||
document.body.style.overflow = 'hidden'; | ||
} | ||
|
||
return () => { | ||
document.removeEventListener('keydown', onKeyDown); | ||
document.body.style.overflow = 'auto'; | ||
}; | ||
}, [isOpen]); | ||
|
||
return ( | ||
<ConfirmContext.Provider value={{ confirmPopup }}> | ||
{children} | ||
{isOpen && | ||
createPortal( | ||
<ConfirmModal | ||
title={title} | ||
content={content} | ||
denial={denial ?? '닫기'} | ||
confirmation={confirmation ?? '확인'} | ||
onDeny={onDeny} | ||
onConfirm={onConfirm} | ||
/>, | ||
document.body | ||
)} | ||
</ConfirmContext.Provider> | ||
); | ||
}; | ||
|
||
export default ConfirmModalProvider; |
11 changes: 11 additions & 0 deletions
11
frontend/src/shared/components/ConfirmModal/hooks/useConfirmContext.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { useContext } from 'react'; | ||
import { ConfirmContext } from '../ConfirmModalProvider'; | ||
|
||
export const useConfirmContext = () => { | ||
const contextValue = useContext(ConfirmContext); | ||
if (!contextValue) { | ||
throw new Error('ConfirmContext Provider 내부에서 사용 가능합니다.'); | ||
} | ||
|
||
return contextValue; | ||
}; |