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/#550 Confirm Modal 구현 #553

Merged
merged 23 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d3aee9e
feat: confirm provider 뼈대 구현
ukkodeveloper Nov 3, 2023
3a8246e
feat: confirm modal 수락 기능 추가
ukkodeveloper Nov 3, 2023
31ecdd0
feat: confirmModal 스타일 및 분리
ukkodeveloper Nov 4, 2023
580124d
feat: ConfirmProvider 분리
ukkodeveloper Nov 4, 2023
3273fa7
feat: ConfirmModal storybook 추가
ukkodeveloper Nov 4, 2023
c25781d
feat: storybook 예시 추가
ukkodeveloper Nov 5, 2023
d526aa9
refactor: useConfirm 분리
ukkodeveloper Nov 5, 2023
e382050
refactor: confirm modal 로직 응집하기 위해 useEffect 이동
ukkodeveloper Nov 5, 2023
a526142
fix: promise에서 resolve 함수 상태 변경 실패로 인한 ref 사용
ukkodeveloper Nov 5, 2023
1179f35
refactor: createPortal를 ConfirmModal 내부로 이동
ukkodeveloper Nov 5, 2023
dbc1c32
refactor: 의미있는 네이밍으로 변경
ukkodeveloper Nov 5, 2023
2af5e0b
fix: keydown 이벤트 적용되지 않는 현상 수정
ukkodeveloper Nov 6, 2023
6441a12
style: style lint 적용 및 개행
ukkodeveloper Nov 6, 2023
3f60df5
chore: 사용하지 않는 파일 삭제
ukkodeveloper Nov 14, 2023
a225ac0
fix: resolverRef 타입 변경
ukkodeveloper Nov 14, 2023
aab67ba
feat: 닫기, 수락 button에 type 추가
ukkodeveloper Nov 14, 2023
67963b6
refactor: 네이밍 cancel에서 denial으로 변경
ukkodeveloper Nov 14, 2023
b1ad8a7
feat: 모달 열릴 때 바로 title로 포커스 이동할 수 있도록 수정
ukkodeveloper Nov 14, 2023
ac5d64c
refactor: 중복된 createPortal 삭제
ukkodeveloper Nov 16, 2023
f27bd28
refactor: theme 활용하여 색상 코드 변경
ukkodeveloper Nov 17, 2023
a6eb162
refactor: 전반적인 ConfirmModal 네이밍 변경
ukkodeveloper Nov 17, 2023
a2fb196
fix: theme color 사용 시 객체분해할당 오류 수정
ukkodeveloper Nov 17, 2023
b94ea94
Merge branch 'main' into feat/#550
ukkodeveloper Nov 23, 2023
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
1 change: 0 additions & 1 deletion frontend/practice.js

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ const RegisterButton = styled.button`

@media (min-width: ${({ theme }) => theme.breakPoints.md}) {
padding: 11px 15px;

font-size: 18px;
}
`;
Expand Down
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 frontend/src/shared/components/ConfirmModal/ConfirmModal.tsx
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;
Comment on lines +54 to +57
Copy link
Collaborator

Choose a reason for hiding this comment

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

inset 으로 1줄로 줄여볼까용~?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

좋아요!


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 frontend/src/shared/components/ConfirmModal/ConfirmModalProvider.tsx
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;
}
Comment on lines +11 to +16
Copy link
Collaborator

Choose a reason for hiding this comment

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

💬 denial, confirmation 메세지가 옵셔널이네요~
혹시 실제 Modal에 기본값과 옵셔널 처리를 하지않고 여기서 한 이유가 있을까요~?

Copy link
Collaborator

Choose a reason for hiding this comment

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

💬 요거 위쪽의 ConfirmModalProps랑 비슷한 타입이라서 재사용을 해볼 수도 있을것 같기도 하고요~ 의도를 몰라서 우선 남겨봅니다~

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

denial, confirmation 메세지가 옵셔널이네요~
이 부분은 버튼 이름입니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

요거 위쪽의 ConfirmModalProps랑 비슷한 타입이라서 재사용을 해볼 수도 있을것 같기도 하고요~ 의도를 몰라서 우선 남겨봅니다~

제가 조심스럽게 타입을 사용하는 경향이 있어요! 우선 ConfirmModalProps 는 이제 modal 을 사용하는 곳에서 사용할 interface이고, ModalContents 는 내부에서 관리하고 있는 상태에요! 지금은 비슷해 보이면서도 성격이 다른 것 같아서 분리해 놓았습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

이 부분은 버튼 이름입니다!

아 네엡 저도 버튼의 메세지를 말한거였습니당

실제 Modal에 기본값과 옵셔널 처리를 하지않고 여기서 한 이유가 있을까요~?

이게 메인 질문이었어요!


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;
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;
};
Loading