Skip to content

Commit

Permalink
Merge branch 'main' into feat/#32/accordion
Browse files Browse the repository at this point in the history
  • Loading branch information
easyhyun00 committed Jan 27, 2024
2 parents f9df6b1 + f96c1d2 commit 7f6b79a
Show file tree
Hide file tree
Showing 13 changed files with 436 additions and 1 deletion.
1 change: 1 addition & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Preview } from '@storybook/react';
import 'reset-css';

const preview: Preview = {
parameters: {
Expand Down
1 change: 1 addition & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const App = () => {
<EmotionTestButton size="small">이모션 테스트</EmotionTestButton>
<Link to={ROUTER_PATHS.TEST_CONSTANT}>Test Constant Page</Link>
<Link to={ROUTER_PATHS.TEST_VARIABLE('5')}>Test 5 Page</Link>
<Link to={ROUTER_PATHS.MODAL_TEST}>Modal Test Page</Link>
<Outlet />
<button css={{ width: '100%' }}>하단버튼</button>
</Background>
Expand Down
10 changes: 10 additions & 0 deletions src/assets/icons/caretDown.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/assets/icons/hourGlass.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions src/assets/icons/siren.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/components/IconButton/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const Primary: Story = {
};

export const 아이콘_버튼: StoryObj = {
storyName: '아이콘 버튼(상단, 하단, 오른쪽, 왼쪽 화살표)',
name: '아이콘 버튼(상단, 하단, 오른쪽, 왼쪽 화살표)',
render: () => (
<div css={styles.iconContainer}>
<IconButton>
Expand Down
170 changes: 170 additions & 0 deletions src/components/Modal/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { Fragment, useRef } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { css } from '@emotion/react';
import useModal from '@/hooks/useModal';
import HourGlass from '@/assets/icons/hourGlass.svg?react';
import CaretDown from '@/assets/icons/caretDown.svg?react';
import Siren from '@/assets/icons/siren.svg?react';
import IconButton from '../IconButton';
import Button from '../Button';
import Modal from '.';

const styles = {
container: css`
display: flex;
flex-direction: column;
gap: 1rem;
box-sizing: border-box;
min-height: 100svh;
padding: 1rem;
`,
header: css`
display: flex;
justify-content: space-between;
align-items: center;
margin: 0.25rem 0 0;
color: white;
`,
from: css`
display: inline-block;
margin-right: 0.5rem;
color: var(--gray1, #333);
font-weight: 600;
font-size: 0.875rem;
line-height: 1.5rem;
`,
nickname: css`
color: var(--gray2, #4f4f4f);
font-weight: 500;
font-size: 0.875rem;
line-height: 1rem;
`,
glassLabel: css`
display: flex;
gap: 0.25rem;
justify-content: center;
align-items: center;
padding: 0.5rem 1rem;
border-radius: 6.25rem;
background-color: white;
color: black;
`,
mainSection: css`
flex-grow: 1;
`,
card: css`
display: flex;
flex-direction: column;
gap: 1rem;
box-sizing: border-box;
width: 100%;
padding: 1.25rem 1.25rem 2.5rem;
border-radius: 0.5rem;
background-color: white;
`,
paragraph: css`
font-size: 0.875rem;
line-height: 1.5rem;
`,
date: css`
color: var(--gray4, #bdbdbd);
font-size: 0.75rem;
line-height: 1rem;
text-align: right;
`,
bottomCard: css`
display: flex;
gap: 0.5rem;
justify-content: center;
align-items: center;
padding: 1.25rem;
border-radius: 0.5rem;
background-color: white;
color: var(--gray4, #bdbdbd);
cursor: pointer;
user-select: none;
`,
};

const meta = {
title: 'Components/Modal',
tags: ['autodocs'],
parameters: {
docs: {
description: {
component:
'모달 컴포넌트입니다.\nESC 키를 누르거나, 모달 바깥을 클릭하면 모달이 닫힙니다.',
},
},
},
argTypes: {},
} satisfies Meta<typeof Modal>;

export default meta;

type Story = StoryObj<typeof meta>;

export const 편지_작성_모달: Story = {
render: () => {
const ModalTestPage = () => {
const modal = useModal();
const backgroundRef = useRef<HTMLDivElement>(null);
const { open, close } = modal;

return (
<div>
<Button onClick={open}>모달 열기</Button>

<Modal {...modal}>
<section css={styles.container}>
<div css={styles.header}>
<div css={styles.glassLabel}>
<HourGlass />
<span>26h</span>
</div>
<IconButton variant="header">
<Siren />
</IconButton>
</div>
<section
css={styles.mainSection}
ref={backgroundRef}
onClick={(e) => e.target === backgroundRef.current && close()}
>
<div css={styles.card}>
<h2>
<span css={styles.from}>From.</span>
<span css={styles.nickname}>낯선고양이</span>
</h2>
<p css={styles.paragraph}>
{Array.from({ length: 3 }, (_, i) => (
<Fragment key={i}>
여기까지가 끝인가 보오 이제 나는 돌아서겠소 억지
노력으로 인연을 거슬러 괴롭히지는 않겠소 하고 싶은 말
하려 했던 말 이대로 다 남겨두고서 혹시나 기대도 포기하려
하오 그대 부디 잘 지내시오 기나긴 그대 침묵을 이별로
받아두겠소 행여 이 맘 다칠까 근심은 접어두오 오 사랑한
사람이여 더 이상 못보아도 사실 그대 있음으로 힘겨운
날들을 견뎌 왔음에 감사하오 좋은 사람 만나오 사는 동안
날 잊고 사시오 진정 행복하길 바라겠소 이 맘만 가져가오
기나긴 그대 침묵을 이별로 받아두겠소 행여 이 맘 다칠까
근심은 접어두오 오 사랑한 사람이여
</Fragment>
))}
</p>
<p css={styles.date}>24년 01월 20일</p>
</div>
</section>
<div css={styles.bottomCard} onClick={close}>
<span>접기</span>
<CaretDown />
</div>
</section>
</Modal>
</div>
);
};

return <ModalTestPage />;
},
};
45 changes: 45 additions & 0 deletions src/components/Modal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React, { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import { motion, AnimatePresence } from 'framer-motion';
import useModal from '@/hooks/useModal';
import styles from './styles';

interface ModalProps extends ReturnType<typeof useModal> {
/** 모달 컨텐츠 영역에 보여줄 컨텐츠입니다. */
children: React.ReactNode;
}

const Modal = ({ isOpen, close, children }: ModalProps) => {
const containerRef = useRef<HTMLElement>(null);

useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
close();
}
};

window.addEventListener('keydown', handleEscape);
return () => window.removeEventListener('keydown', handleEscape);
}, [isOpen, close]);

return createPortal(
<AnimatePresence>
{isOpen && (
<motion.aside
css={styles.container}
ref={containerRef}
onClick={(e) => e.target === containerRef.current && close()}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<div css={styles.content}>{children}</div>
</motion.aside>
)}
</AnimatePresence>,
document.body,
);
};

export default Modal;
18 changes: 18 additions & 0 deletions src/components/Modal/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { css } from '@emotion/react';

const styles = {
container: css`
position: absolute;
top: 0;
left: 0;
width: 100%;
min-height: 100%;
background-color: rgb(0 0 0 / 0.6);
`,
content: css`
max-width: 600px;
margin: 0 auto;
`,
};

export default styles;
13 changes: 13 additions & 0 deletions src/hooks/useModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useCallback, useState } from 'react';

const useModal = () => {
const [isOpen, setIsOpen] = useState(false);

const toggle = useCallback(() => setIsOpen(!isOpen), [isOpen]);
const open = useCallback(() => setIsOpen(true), []);
const close = useCallback(() => setIsOpen(false), []);

return { isOpen, open, close, toggle };
};

export default useModal;
70 changes: 70 additions & 0 deletions src/pages/ModalTestPage/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Fragment, useRef } from 'react';
import Modal from '@/components/Modal';
import HourGlass from '@/assets/icons/hourGlass.svg?react';
import CaretDown from '@/assets/icons/caretDown.svg?react';
import Siren from '@/assets/icons/siren.svg?react';
import useModal from '@/hooks/useModal';
import IconButton from '@/components/IconButton';
import Button from '@/components/Button';
import styles from './styles';

/** TODO: 모달 확인용 페이지입니다. 추후 제거해도 좋습니다. */
const ModalTestPage = () => {
const modal = useModal();
const backgroundRef = useRef<HTMLDivElement>(null);
const { open, close } = modal;

return (
<div>
<Button onClick={open}>모달 열기</Button>

<Modal {...modal}>
<section css={styles.container}>
<div css={styles.header}>
<div css={styles.glassLabel}>
<HourGlass />
<span>26h</span>
</div>
<IconButton variant="header">
<Siren />
</IconButton>
</div>
<section
css={styles.mainSection}
ref={backgroundRef}
onClick={(e) => e.target === backgroundRef.current && close()}
>
<div css={styles.card}>
<h2>
<span css={styles.from}>From.</span>
<span css={styles.nickname}>낯선고양이</span>
</h2>
<p css={styles.paragraph}>
{Array.from({ length: 3 }, (_, i) => (
<Fragment key={i}>
여기까지가 끝인가 보오 이제 나는 돌아서겠소 억지 노력으로
인연을 거슬러 괴롭히지는 않겠소 하고 싶은 말 하려 했던 말
이대로 다 남겨두고서 혹시나 기대도 포기하려 하오 그대 부디
잘 지내시오 기나긴 그대 침묵을 이별로 받아두겠소 행여 이 맘
다칠까 근심은 접어두오 오 사랑한 사람이여 더 이상 못보아도
사실 그대 있음으로 힘겨운 날들을 견뎌 왔음에 감사하오 좋은
사람 만나오 사는 동안 날 잊고 사시오 진정 행복하길 바라겠소
이 맘만 가져가오 기나긴 그대 침묵을 이별로 받아두겠소 행여
이 맘 다칠까 근심은 접어두오 오 사랑한 사람이여
</Fragment>
))}
</p>
<p css={styles.date}>24년 01월 20일</p>
</div>
</section>
<div css={styles.bottomCard} onClick={close}>
<span>접기</span>
<CaretDown />
</div>
</section>
</Modal>
</div>
);
};

export default ModalTestPage;
Loading

0 comments on commit 7f6b79a

Please sign in to comment.